diff --git a/src/Umbraco.Core/Actions/ActionAssignDomain.cs b/src/Umbraco.Core/Actions/ActionAssignDomain.cs index 452ca51549..b8fd8a4650 100644 --- a/src/Umbraco.Core/Actions/ActionAssignDomain.cs +++ b/src/Umbraco.Core/Actions/ActionAssignDomain.cs @@ -9,26 +9,26 @@ namespace Umbraco.Cms.Core.Actions; public class ActionAssignDomain : IAction { /// - /// The unique action letter + /// The unique action letter /// public const char ActionLetter = 'I'; - /// + /// public char Letter => ActionLetter; - /// + /// // This is all lower-case because of case sensitive filesystems, see issue: https://github.com/umbraco/Umbraco-CMS/issues/11670 public string Alias => "assigndomain"; - /// + /// public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; - /// + /// public string Icon => "home"; - /// + /// public bool ShowInNotifier => false; - /// + /// public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionBrowse.cs b/src/Umbraco.Core/Actions/ActionBrowse.cs index 5be16a01c9..2620888a30 100644 --- a/src/Umbraco.Core/Actions/ActionBrowse.cs +++ b/src/Umbraco.Core/Actions/ActionBrowse.cs @@ -1,40 +1,41 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is used as a security constraint that grants a user the ability to view nodes in a tree +/// that has permissions applied to it. +/// +/// +/// This action should not be invoked. It is used as the minimum required permission to view nodes in the content tree. +/// By +/// granting a user this permission, the user is able to see the node in the tree but not edit the document. This may +/// be used by other trees +/// that support permissions in the future. +/// +public class ActionBrowse : IAction { /// - /// This action is used as a security constraint that grants a user the ability to view nodes in a tree - /// that has permissions applied to it. + /// The unique action letter /// - /// - /// This action should not be invoked. It is used as the minimum required permission to view nodes in the content tree. By - /// granting a user this permission, the user is able to see the node in the tree but not edit the document. This may be used by other trees - /// that support permissions in the future. - /// - public class ActionBrowse : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'F'; + public const char ActionLetter = 'F'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public bool ShowInNotifier => false; + /// + public bool ShowInNotifier => false; - /// - public bool CanBePermissionAssigned => true; + /// + public bool CanBePermissionAssigned => true; - /// - public string Icon => string.Empty; + /// + public string Icon => string.Empty; - /// - public string Alias => "browse"; + /// + public string Alias => "browse"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionCollection.cs b/src/Umbraco.Core/Actions/ActionCollection.cs index 1e396952a2..b204075b88 100644 --- a/src/Umbraco.Core/Actions/ActionCollection.cs +++ b/src/Umbraco.Core/Actions/ActionCollection.cs @@ -1,60 +1,56 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// The collection of actions +/// +public class ActionCollection : BuilderCollectionBase { /// - /// The collection of actions + /// Initializes a new instance of the class. /// - public class ActionCollection : BuilderCollectionBase + public ActionCollection(Func> items) + : base(items) { - /// - /// Initializes a new instance of the class. - /// - public ActionCollection(Func> items) - : base(items) - { - } + } - /// - /// Gets the action of the specified type. - /// - /// The specified type to get - /// The action - public T? GetAction() - where T : IAction => this.OfType().FirstOrDefault(); + /// + /// Gets the action of the specified type. + /// + /// The specified type to get + /// The action + public T? GetAction() + where T : IAction => this.OfType().FirstOrDefault(); - /// - /// Gets the actions by the specified letters - /// - public IEnumerable GetByLetters(IEnumerable letters) - { - IAction[] actions = this.ToArray(); // no worry: internally, it's already an array - return letters - .Where(x => x.Length == 1) - .Select(x => actions.FirstOrDefault(y => y.Letter == x[0])) - .WhereNotNull() - .ToList(); - } + /// + /// Gets the actions by the specified letters + /// + public IEnumerable GetByLetters(IEnumerable letters) + { + IAction[] actions = this.ToArray(); // no worry: internally, it's already an array + return letters + .Where(x => x.Length == 1) + .Select(x => actions.FirstOrDefault(y => y.Letter == x[0])) + .WhereNotNull() + .ToList(); + } - /// - /// Gets the actions from an EntityPermission - /// - public IReadOnlyList FromEntityPermission(EntityPermission entityPermission) - { - IAction[] actions = this.ToArray(); // no worry: internally, it's already an array - return entityPermission.AssignedPermissions - .Where(x => x.Length == 1) - .SelectMany(x => actions.Where(y => y.Letter == x[0])) - .WhereNotNull() - .ToList(); - } + /// + /// Gets the actions from an EntityPermission + /// + public IReadOnlyList FromEntityPermission(EntityPermission entityPermission) + { + IAction[] actions = this.ToArray(); // no worry: internally, it's already an array + return entityPermission.AssignedPermissions + .Where(x => x.Length == 1) + .SelectMany(x => actions.Where(y => y.Letter == x[0])) + .WhereNotNull() + .ToList(); } } diff --git a/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs b/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs index 58e70e4a2a..aac1556234 100644 --- a/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs +++ b/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs @@ -1,35 +1,32 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// The action collection builder +/// +public class ActionCollectionBuilder : LazyCollectionBuilderBase { - /// - /// The action collection builder - /// - public class ActionCollectionBuilder : LazyCollectionBuilderBase + /// + protected override ActionCollectionBuilder This => this; + + /// + protected override IEnumerable CreateItems(IServiceProvider factory) { - /// - protected override ActionCollectionBuilder This => this; + var items = base.CreateItems(factory).ToList(); - /// - protected override IEnumerable CreateItems(IServiceProvider factory) + // Validate the items, no actions should exist that do not either expose notifications or permissions + var invalidItems = items.Where(x => !x.CanBePermissionAssigned && !x.ShowInNotifier).ToList(); + if (invalidItems.Count == 0) { - var items = base.CreateItems(factory).ToList(); - - // Validate the items, no actions should exist that do not either expose notifications or permissions - var invalidItems = items.Where(x => !x.CanBePermissionAssigned && !x.ShowInNotifier).ToList(); - if (invalidItems.Count == 0) - { - return items; - } - - var invalidActions = string.Join(", ", invalidItems.Select(x => "'" + x.Alias + "'")); - throw new InvalidOperationException($"Invalid actions {invalidActions}'. All {typeof(IAction)} implementations must be true for either {nameof(IAction.CanBePermissionAssigned)} or {nameof(IAction.ShowInNotifier)}."); + return items; } + + var invalidActions = string.Join(", ", invalidItems.Select(x => "'" + x.Alias + "'")); + throw new InvalidOperationException( + $"Invalid actions {invalidActions}'. All {typeof(IAction)} implementations must be true for either {nameof(IAction.CanBePermissionAssigned)} or {nameof(IAction.ShowInNotifier)}."); } } diff --git a/src/Umbraco.Core/Actions/ActionCopy.cs b/src/Umbraco.Core/Actions/ActionCopy.cs index 83a855d1ff..02bb17166f 100644 --- a/src/Umbraco.Core/Actions/ActionCopy.cs +++ b/src/Umbraco.Core/Actions/ActionCopy.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when copying a document, media, member +/// +public class ActionCopy : IAction { /// - /// This action is invoked when copying a document, media, member + /// The unique action letter /// - public class ActionCopy : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'O'; + public const char ActionLetter = 'O'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "copy"; + /// + public string Alias => "copy"; - /// - public string Category => Constants.Conventions.PermissionCategories.StructureCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.StructureCategory; - /// - public string Icon => "documents"; + /// + public string Icon => "documents"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs b/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs index 806868af40..85490b42f8 100644 --- a/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs +++ b/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs @@ -1,29 +1,28 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when creating a blueprint from a content +/// +public class ActionCreateBlueprintFromContent : IAction { - /// - /// This action is invoked when creating a blueprint from a content - /// - public class ActionCreateBlueprintFromContent : IAction - { - /// - public char Letter => 'ï'; + /// + public char Letter => 'ï'; - /// - public bool ShowInNotifier => false; + /// + public bool ShowInNotifier => false; - /// - public bool CanBePermissionAssigned => true; + /// + public bool CanBePermissionAssigned => true; - /// - public string Icon => "blueprint"; + /// + public string Icon => "blueprint"; - /// - public string Alias => "createblueprint"; + /// + public string Alias => "createblueprint"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionDelete.cs b/src/Umbraco.Core/Actions/ActionDelete.cs index 85c9b39dff..7d9c4e6a03 100644 --- a/src/Umbraco.Core/Actions/ActionDelete.cs +++ b/src/Umbraco.Core/Actions/ActionDelete.cs @@ -1,39 +1,38 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when a document, media, member is deleted +/// +public class ActionDelete : IAction { /// - /// This action is invoked when a document, media, member is deleted + /// The unique action alias /// - public class ActionDelete : IAction - { - /// - /// The unique action alias - /// - public const string ActionAlias = "delete"; + public const string ActionAlias = "delete"; - /// - /// The unique action letter - /// - public const char ActionLetter = 'D'; + /// + /// The unique action letter + /// + public const char ActionLetter = 'D'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => ActionAlias; + /// + public string Alias => ActionAlias; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "delete"; + /// + public string Icon => "delete"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionMove.cs b/src/Umbraco.Core/Actions/ActionMove.cs index 0f8b4b8305..a40d03d096 100644 --- a/src/Umbraco.Core/Actions/ActionMove.cs +++ b/src/Umbraco.Core/Actions/ActionMove.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked upon creation of a document, media, member +/// +public class ActionMove : IAction { /// - /// This action is invoked upon creation of a document, media, member + /// The unique action letter /// - public class ActionMove : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'M'; + public const char ActionLetter = 'M'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "move"; + /// + public string Alias => "move"; - /// - public string Category => Constants.Conventions.PermissionCategories.StructureCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.StructureCategory; - /// - public string Icon => "enter"; + /// + public string Icon => "enter"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionNew.cs b/src/Umbraco.Core/Actions/ActionNew.cs index 25e85cd377..31056632ed 100644 --- a/src/Umbraco.Core/Actions/ActionNew.cs +++ b/src/Umbraco.Core/Actions/ActionNew.cs @@ -1,39 +1,38 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked upon creation of a document +/// +public class ActionNew : IAction { /// - /// This action is invoked upon creation of a document + /// The unique action alias /// - public class ActionNew : IAction - { - /// - /// The unique action alias - /// - public const string ActionAlias = "create"; + public const string ActionAlias = "create"; - /// - /// The unique action letter - /// - public const char ActionLetter = 'C'; + /// + /// The unique action letter + /// + public const char ActionLetter = 'C'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => ActionAlias; + /// + public string Alias => ActionAlias; - /// - public string Icon => "add"; + /// + public string Icon => "add"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; + /// + public bool CanBePermissionAssigned => true; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionNotify.cs b/src/Umbraco.Core/Actions/ActionNotify.cs index 3f1e855cff..9d1975b852 100644 --- a/src/Umbraco.Core/Actions/ActionNotify.cs +++ b/src/Umbraco.Core/Actions/ActionNotify.cs @@ -1,29 +1,28 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked upon modifying the notification of a content +/// +public class ActionNotify : IAction { - /// - /// This action is invoked upon modifying the notification of a content - /// - public class ActionNotify : IAction - { - /// - public char Letter => 'N'; + /// + public char Letter => 'N'; - /// - public bool ShowInNotifier => false; + /// + public bool ShowInNotifier => false; - /// - public bool CanBePermissionAssigned => true; + /// + public bool CanBePermissionAssigned => true; - /// - public string Icon => "megaphone"; + /// + public string Icon => "megaphone"; - /// - public string Alias => "notify"; + /// + public string Alias => "notify"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - } + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; } diff --git a/src/Umbraco.Core/Actions/ActionProtect.cs b/src/Umbraco.Core/Actions/ActionProtect.cs index 10684a69e2..0a5ac8ace8 100644 --- a/src/Umbraco.Core/Actions/ActionProtect.cs +++ b/src/Umbraco.Core/Actions/ActionProtect.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when a document is protected or unprotected +/// +public class ActionProtect : IAction { /// - /// This action is invoked when a document is protected or unprotected + /// The unique action letter /// - public class ActionProtect : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'P'; + public const char ActionLetter = 'P'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "protect"; + /// + public string Alias => "protect"; - /// - public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; - /// - public string Icon => "lock"; + /// + public string Icon => "lock"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionPublish.cs b/src/Umbraco.Core/Actions/ActionPublish.cs index 02f77d6862..e07b0935bc 100644 --- a/src/Umbraco.Core/Actions/ActionPublish.cs +++ b/src/Umbraco.Core/Actions/ActionPublish.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when a document is being published +/// +public class ActionPublish : IAction { /// - /// This action is invoked when a document is being published + /// The unique action letter /// - public class ActionPublish : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'U'; + public const char ActionLetter = 'U'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "publish"; + /// + public string Alias => "publish"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => string.Empty; + /// + public string Icon => string.Empty; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionRestore.cs b/src/Umbraco.Core/Actions/ActionRestore.cs index 164c93e2d5..79e00f4464 100644 --- a/src/Umbraco.Core/Actions/ActionRestore.cs +++ b/src/Umbraco.Core/Actions/ActionRestore.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when the content/media item is to be restored from the recycle bin +/// +public class ActionRestore : IAction { /// - /// This action is invoked when the content/media item is to be restored from the recycle bin + /// The unique action alias /// - public class ActionRestore : IAction - { - /// - /// The unique action alias - /// - public const string ActionAlias = "restore"; + public const string ActionAlias = "restore"; - /// - public char Letter => 'V'; + /// + public char Letter => 'V'; - /// - public string Alias => ActionAlias; + /// + public string Alias => ActionAlias; - /// - public string? Category => null; + /// + public string? Category => null; - /// - public string Icon => "undo"; + /// + public string Icon => "undo"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => false; - } + /// + public bool CanBePermissionAssigned => false; } diff --git a/src/Umbraco.Core/Actions/ActionRights.cs b/src/Umbraco.Core/Actions/ActionRights.cs index fff7cc8652..08afe7e2db 100644 --- a/src/Umbraco.Core/Actions/ActionRights.cs +++ b/src/Umbraco.Core/Actions/ActionRights.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when rights are changed on a document +/// +public class ActionRights : IAction { /// - /// This action is invoked when rights are changed on a document + /// The unique action letter /// - public class ActionRights : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'R'; + public const char ActionLetter = 'R'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "rights"; + /// + public string Alias => "rights"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "vcard"; + /// + public string Icon => "vcard"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionRollback.cs b/src/Umbraco.Core/Actions/ActionRollback.cs index 565a8469c5..5aada555d3 100644 --- a/src/Umbraco.Core/Actions/ActionRollback.cs +++ b/src/Umbraco.Core/Actions/ActionRollback.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when copying a document is being rolled back +/// +public class ActionRollback : IAction { /// - /// This action is invoked when copying a document is being rolled back + /// The unique action letter /// - public class ActionRollback : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'K'; + public const char ActionLetter = 'K'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "rollback"; + /// + public string Alias => "rollback"; - /// - public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; - /// - public string Icon => "undo"; + /// + public string Icon => "undo"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionSort.cs b/src/Umbraco.Core/Actions/ActionSort.cs index 1f87bfcc3c..b77a44c730 100644 --- a/src/Umbraco.Core/Actions/ActionSort.cs +++ b/src/Umbraco.Core/Actions/ActionSort.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when children to a document, media, member is being sorted +/// +public class ActionSort : IAction { /// - /// This action is invoked when children to a document, media, member is being sorted + /// The unique action letter /// - public class ActionSort : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'S'; + public const char ActionLetter = 'S'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "sort"; + /// + public string Alias => "sort"; - /// - public string Category => Constants.Conventions.PermissionCategories.StructureCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.StructureCategory; - /// - public string Icon => "navigation-vertical"; + /// + public string Icon => "navigation-vertical"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionToPublish.cs b/src/Umbraco.Core/Actions/ActionToPublish.cs index 654b71661d..bf15ee4e3b 100644 --- a/src/Umbraco.Core/Actions/ActionToPublish.cs +++ b/src/Umbraco.Core/Actions/ActionToPublish.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when children to a document is being sent to published (by an editor without publishrights) +/// +public class ActionToPublish : IAction { /// - /// This action is invoked when children to a document is being sent to published (by an editor without publishrights) + /// The unique action letter /// - public class ActionToPublish : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'H'; + public const char ActionLetter = 'H'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "sendtopublish"; + /// + public string Alias => "sendtopublish"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "outbox"; + /// + public string Icon => "outbox"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionUnpublish.cs b/src/Umbraco.Core/Actions/ActionUnpublish.cs index 6e9ec8506b..c8a83f002e 100644 --- a/src/Umbraco.Core/Actions/ActionUnpublish.cs +++ b/src/Umbraco.Core/Actions/ActionUnpublish.cs @@ -1,35 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when a document is being unpublished +/// +public class ActionUnpublish : IAction { /// - /// This action is invoked when a document is being unpublished + /// The unique action letter /// - public class ActionUnpublish : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'Z'; + public const char ActionLetter = 'Z'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "unpublish"; + /// + public string Alias => "unpublish"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "circle-dotted"; + /// + public string Icon => "circle-dotted"; - /// - public bool ShowInNotifier => false; - - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool ShowInNotifier => false; + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/ActionUpdate.cs b/src/Umbraco.Core/Actions/ActionUpdate.cs index 3f8092c1fc..4af2410cc4 100644 --- a/src/Umbraco.Core/Actions/ActionUpdate.cs +++ b/src/Umbraco.Core/Actions/ActionUpdate.cs @@ -1,34 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// This action is invoked when copying a document or media +/// +public class ActionUpdate : IAction { /// - /// This action is invoked when copying a document or media + /// The unique action letter /// - public class ActionUpdate : IAction - { - /// - /// The unique action letter - /// - public const char ActionLetter = 'A'; + public const char ActionLetter = 'A'; - /// - public char Letter => ActionLetter; + /// + public char Letter => ActionLetter; - /// - public string Alias => "update"; + /// + public string Alias => "update"; - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; + /// + public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - /// - public string Icon => "save"; + /// + public string Icon => "save"; - /// - public bool ShowInNotifier => true; + /// + public bool ShowInNotifier => true; - /// - public bool CanBePermissionAssigned => true; - } + /// + public bool CanBePermissionAssigned => true; } diff --git a/src/Umbraco.Core/Actions/IAction.cs b/src/Umbraco.Core/Actions/IAction.cs index 2d9876afc6..f57e697a2e 100644 --- a/src/Umbraco.Core/Actions/IAction.cs +++ b/src/Umbraco.Core/Actions/IAction.cs @@ -1,49 +1,48 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Actions +namespace Umbraco.Cms.Core.Actions; + +/// +/// Defines a back office action that can be permission assigned or subscribed to for notifications +/// +/// +/// If an IAction returns false for both ShowInNotifier and CanBePermissionAssigned then the IAction should not exist +/// +public interface IAction : IDiscoverable { /// - /// Defines a back office action that can be permission assigned or subscribed to for notifications + /// Gets the letter used to assign a permission (must be unique) + /// + char Letter { get; } + + /// + /// Gets a value indicating whether whether to allow subscribing to notifications for this action + /// + bool ShowInNotifier { get; } + + /// + /// Gets a value indicating whether whether to allow assigning permissions based on this action + /// + bool CanBePermissionAssigned { get; } + + /// + /// Gets the icon to display for this action + /// + string Icon { get; } + + /// + /// Gets the alias for this action (must be unique) + /// + string Alias { get; } + + /// + /// Gets the category used for this action /// /// - /// If an IAction returns false for both ShowInNotifier and CanBePermissionAssigned then the IAction should not exist + /// Used in the UI when assigning permissions /// - public interface IAction : IDiscoverable - { - /// - /// Gets the letter used to assign a permission (must be unique) - /// - char Letter { get; } - - /// - /// Gets a value indicating whether whether to allow subscribing to notifications for this action - /// - bool ShowInNotifier { get; } - - /// - /// Gets a value indicating whether whether to allow assigning permissions based on this action - /// - bool CanBePermissionAssigned { get; } - - /// - /// Gets the icon to display for this action - /// - string Icon { get; } - - /// - /// Gets the alias for this action (must be unique) - /// - string Alias { get; } - - /// - /// Gets the category used for this action - /// - /// - /// Used in the UI when assigning permissions - /// - string? Category { get; } - } + string? Category { get; } } diff --git a/src/Umbraco.Core/Attempt.cs b/src/Umbraco.Core/Attempt.cs index 71eabd2f0d..7a438dece6 100644 --- a/src/Umbraco.Core/Attempt.cs +++ b/src/Umbraco.Core/Attempt.cs @@ -1,126 +1,108 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Provides ways to create attempts. +/// +public static class Attempt { + // note: + // cannot rely on overloads only to differentiate between with/without status + // in some cases it will always be ambiguous, so be explicit w/ 'WithStatus' methods + /// - /// Provides ways to create attempts. + /// Creates a successful attempt with a result. /// - public static class Attempt - { - // note: - // cannot rely on overloads only to differentiate between with/without status - // in some cases it will always be ambiguous, so be explicit w/ 'WithStatus' methods + /// The type of the attempted operation result. + /// The result of the attempt. + /// The successful attempt. + public static Attempt Succeed(TResult? result) => Attempt.Succeed(result); - /// - /// Creates a successful attempt with a result. - /// - /// The type of the attempted operation result. - /// The result of the attempt. - /// The successful attempt. - public static Attempt Succeed(TResult? result) - { - return Attempt.Succeed(result); - } + /// + /// Creates a successful attempt with a result and a status. + /// + /// The type of the attempted operation result. + /// The type of the attempted operation status. + /// The status of the attempt. + /// The result of the attempt. + /// The successful attempt. + public static Attempt SucceedWithStatus(TStatus status, TResult result) => + Attempt.Succeed(status, result); - /// - /// Creates a successful attempt with a result and a status. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - /// The status of the attempt. - /// The result of the attempt. - /// The successful attempt. - public static Attempt SucceedWithStatus(TStatus status, TResult result) - { - return Attempt.Succeed(status, result); - } + /// + /// Creates a failed attempt. + /// + /// The type of the attempted operation result. + /// The failed attempt. + public static Attempt Fail() => Attempt.Fail(); - /// - /// Creates a failed attempt. - /// - /// The type of the attempted operation result. - /// The failed attempt. - public static Attempt Fail() - { - return Attempt.Fail(); - } + /// + /// Creates a failed attempt with a result. + /// + /// The type of the attempted operation result. + /// The result of the attempt. + /// The failed attempt. + public static Attempt Fail(TResult result) => Attempt.Fail(result); - /// - /// Creates a failed attempt with a result. - /// - /// The type of the attempted operation result. - /// The result of the attempt. - /// The failed attempt. - public static Attempt Fail(TResult result) - { - return Attempt.Fail(result); - } + /// + /// Creates a failed attempt with a result and a status. + /// + /// The type of the attempted operation result. + /// The type of the attempted operation status. + /// The status of the attempt. + /// The result of the attempt. + /// The failed attempt. + public static Attempt FailWithStatus(TStatus status, TResult result) => + Attempt.Fail(status, result); - /// - /// Creates a failed attempt with a result and a status. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - /// The status of the attempt. - /// The result of the attempt. - /// The failed attempt. - public static Attempt FailWithStatus(TStatus status, TResult result) - { - return Attempt.Fail(status, result); - } + /// + /// Creates a failed attempt with a result and an exception. + /// + /// The type of the attempted operation result. + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(TResult result, Exception exception) => + Attempt.Fail(result, exception); - /// - /// Creates a failed attempt with a result and an exception. - /// - /// The type of the attempted operation result. - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(TResult result, Exception exception) - { - return Attempt.Fail(result, exception); - } + /// + /// Creates a failed attempt with a result, an exception and a status. + /// + /// The type of the attempted operation result. + /// The type of the attempted operation status. + /// The status of the attempt. + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt FailWithStatus(TStatus status, TResult result, Exception exception) => Attempt.Fail(status, result, exception); + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// The type of the attempted operation result. + /// A value indicating whether the attempt is successful. + /// The result of the attempt. + /// The attempt. + public static Attempt If(bool condition, TResult result) => + Attempt.If(condition, result); - /// - /// Creates a failed attempt with a result, an exception and a status. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - /// The status of the attempt. - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt FailWithStatus(TStatus status, TResult result, Exception exception) - { - return Attempt.Fail(status, result, exception); - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// The type of the attempted operation result. - /// A value indicating whether the attempt is successful. - /// The result of the attempt. - /// The attempt. - public static Attempt If(bool condition, TResult result) - { - return Attempt.If(condition, result); - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - /// A value indicating whether the attempt is successful. - /// The status of the successful attempt. - /// The status of the failed attempt. - /// The result of the attempt. - /// The attempt. - public static Attempt IfWithStatus(bool condition, TStatus succStatus, TStatus failStatus, TResult result) - { - return Attempt.If(condition, succStatus, failStatus, result); - } - } + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// The type of the attempted operation result. + /// The type of the attempted operation status. + /// A value indicating whether the attempt is successful. + /// The status of the successful attempt. + /// The status of the failed attempt. + /// The result of the attempt. + /// The attempt. + public static Attempt IfWithStatus( + bool condition, + TStatus succStatus, + TStatus failStatus, + TResult result) => + Attempt.If( + condition, + succStatus, + failStatus, + result); } diff --git a/src/Umbraco.Core/AttemptOfTResult.cs b/src/Umbraco.Core/AttemptOfTResult.cs index 5cf85964cc..2969755d94 100644 --- a/src/Umbraco.Core/AttemptOfTResult.cs +++ b/src/Umbraco.Core/AttemptOfTResult.cs @@ -1,141 +1,111 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Represents the result of an operation attempt. +/// +/// The type of the attempted operation result. +[Serializable] +public struct Attempt { - /// - /// Represents the result of an operation attempt. - /// - /// The type of the attempted operation result. - [Serializable] - public struct Attempt + // optimize, use a singleton failed attempt + private static readonly Attempt Failed = new(false, default, null); + + // private - use Succeed() or Fail() methods to create attempts + private Attempt(bool success, TResult? result, Exception? exception) { - // private - use Succeed() or Fail() methods to create attempts - private Attempt(bool success, TResult? result, Exception? exception) - { - Success = success; - Result = result; - Exception = exception; - } - - /// - /// Gets a value indicating whether this was successful. - /// - public bool Success { get; } - - /// - /// Gets the exception associated with an unsuccessful attempt. - /// - public Exception? Exception { get; } - - /// - /// Gets the attempt result. - /// - public TResult? Result { get; } - - /// - /// Gets the attempt result, if successful, else a default value. - /// - public TResult ResultOr(TResult value) - { - if (Success && Result is not null) - { - return Result; - } - - return value; - } - - // optimize, use a singleton failed attempt - private static readonly Attempt Failed = new Attempt(false, default(TResult), null); - - /// - /// Creates a successful attempt. - /// - /// The successful attempt. - public static Attempt Succeed() - { - return new Attempt(true, default(TResult), null); - } - - /// - /// Creates a successful attempt with a result. - /// - /// The result of the attempt. - /// The successful attempt. - public static Attempt Succeed(TResult? result) - { - return new Attempt(true, result, null); - } - - /// - /// Creates a failed attempt. - /// - /// The failed attempt. - public static Attempt Fail() - { - return Failed; - } - - /// - /// Creates a failed attempt with an exception. - /// - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(Exception? exception) - { - return new Attempt(false, default(TResult), exception); - } - - /// - /// Creates a failed attempt with a result. - /// - /// The result of the attempt. - /// The failed attempt. - public static Attempt Fail(TResult result) - { - return new Attempt(false, result, null); - } - - /// - /// Creates a failed attempt with a result and an exception. - /// - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(TResult result, Exception exception) - { - return new Attempt(false, result, exception); - } - - /// - /// Creates a successful or a failed attempt. - /// - /// A value indicating whether the attempt is successful. - /// The attempt. - public static Attempt If(bool condition) - { - return condition ? new Attempt(true, default(TResult), null) : Failed; - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// A value indicating whether the attempt is successful. - /// The result of the attempt. - /// The attempt. - public static Attempt If(bool condition, TResult? result) - { - return new Attempt(condition, result, null); - } - - /// - /// Implicitly operator to check if the attempt was successful without having to access the 'success' property - /// - /// - /// - public static implicit operator bool(Attempt a) - { - return a.Success; - } + Success = success; + Result = result; + Exception = exception; } + + /// + /// Gets a value indicating whether this was successful. + /// + public bool Success { get; } + + /// + /// Gets the exception associated with an unsuccessful attempt. + /// + public Exception? Exception { get; } + + /// + /// Gets the attempt result. + /// + public TResult? Result { get; } + + /// + /// Implicitly operator to check if the attempt was successful without having to access the 'success' property + /// + /// + /// + public static implicit operator bool(Attempt a) => a.Success; + + /// + /// Gets the attempt result, if successful, else a default value. + /// + public TResult ResultOr(TResult value) + { + if (Success && Result is not null) + { + return Result; + } + + return value; + } + + /// + /// Creates a successful attempt. + /// + /// The successful attempt. + public static Attempt Succeed() => new(true, default, null); + + /// + /// Creates a successful attempt with a result. + /// + /// The result of the attempt. + /// The successful attempt. + public static Attempt Succeed(TResult? result) => new(true, result, null); + + /// + /// Creates a failed attempt. + /// + /// The failed attempt. + public static Attempt Fail() => Failed; + + /// + /// Creates a failed attempt with an exception. + /// + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(Exception? exception) => new(false, default, exception); + + /// + /// Creates a failed attempt with a result. + /// + /// The result of the attempt. + /// The failed attempt. + public static Attempt Fail(TResult result) => new(false, result, null); + + /// + /// Creates a failed attempt with a result and an exception. + /// + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(TResult result, Exception exception) => new(false, result, exception); + + /// + /// Creates a successful or a failed attempt. + /// + /// A value indicating whether the attempt is successful. + /// The attempt. + public static Attempt If(bool condition) => condition ? new Attempt(true, default, null) : Failed; + + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// A value indicating whether the attempt is successful. + /// The result of the attempt. + /// The attempt. + public static Attempt If(bool condition, TResult? result) => new(condition, result, null); } diff --git a/src/Umbraco.Core/AttemptOfTResultTStatus.cs b/src/Umbraco.Core/AttemptOfTResultTStatus.cs index 65a3e48334..e88465b3ad 100644 --- a/src/Umbraco.Core/AttemptOfTResultTStatus.cs +++ b/src/Umbraco.Core/AttemptOfTResultTStatus.cs @@ -1,142 +1,121 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Represents the result of an operation attempt. +/// +/// The type of the attempted operation result. +/// The type of the attempted operation status. +[Serializable] +public struct Attempt { - /// - /// Represents the result of an operation attempt. - /// - /// The type of the attempted operation result. - /// The type of the attempted operation status. - [Serializable] - public struct Attempt + // private - use Succeed() or Fail() methods to create attempts + private Attempt(bool success, TResult result, TStatus status, Exception? exception) { - /// - /// Gets a value indicating whether this was successful. - /// - public bool Success { get; } - - /// - /// Gets the exception associated with an unsuccessful attempt. - /// - public Exception? Exception { get; } - - /// - /// Gets the attempt result. - /// - public TResult Result { get; } - - /// - /// Gets the attempt status. - /// - public TStatus Status { get; } - - // private - use Succeed() or Fail() methods to create attempts - private Attempt(bool success, TResult result, TStatus status, Exception? exception) - { - Success = success; - Result = result; - Status = status; - Exception = exception; - } - - /// - /// Creates a successful attempt. - /// - /// The status of the attempt. - /// The successful attempt. - public static Attempt Succeed(TStatus status) - { - return new Attempt(true, default(TResult), status, null); - } - - /// - /// Creates a successful attempt with a result. - /// - /// The status of the attempt. - /// The result of the attempt. - /// The successful attempt. - public static Attempt Succeed(TStatus status, TResult result) - { - return new Attempt(true, result, status, null); - } - - /// - /// Creates a failed attempt. - /// - /// The status of the attempt. - /// The failed attempt. - public static Attempt Fail(TStatus status) - { - return new Attempt(false, default(TResult), status, null); - } - - /// - /// Creates a failed attempt with an exception. - /// - /// The status of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(TStatus status, Exception exception) - { - return new Attempt(false, default(TResult), status, exception); - } - - /// - /// Creates a failed attempt with a result. - /// - /// The status of the attempt. - /// The result of the attempt. - /// The failed attempt. - public static Attempt Fail(TStatus status, TResult result) - { - return new Attempt(false, result, status, null); - } - - /// - /// Creates a failed attempt with a result and an exception. - /// - /// The status of the attempt. - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(TStatus status, TResult result, Exception exception) - { - return new Attempt(false, result, status, exception); - } - - /// - /// Creates a successful or a failed attempt. - /// - /// A value indicating whether the attempt is successful. - /// The status of the successful attempt. - /// The status of the failed attempt. - /// The attempt. - public static Attempt If(bool condition, TStatus succStatus, TStatus failStatus) - { - return new Attempt(condition, default(TResult), condition ? succStatus : failStatus, null); - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// A value indicating whether the attempt is successful. - /// The status of the successful attempt. - /// The status of the failed attempt. - /// The result of the attempt. - /// The attempt. - public static Attempt If(bool condition, TStatus succStatus, TStatus failStatus, TResult result) - { - return new Attempt(condition, result, condition ? succStatus : failStatus, null); - } - - /// - /// Implicitly operator to check if the attempt was successful without having to access the 'success' property - /// - /// - /// - public static implicit operator bool(Attempt a) - { - return a.Success; - } + Success = success; + Result = result; + Status = status; + Exception = exception; } + + /// + /// Gets a value indicating whether this was successful. + /// + public bool Success { get; } + + /// + /// Gets the exception associated with an unsuccessful attempt. + /// + public Exception? Exception { get; } + + /// + /// Gets the attempt result. + /// + public TResult Result { get; } + + /// + /// Gets the attempt status. + /// + public TStatus Status { get; } + + /// + /// Implicitly operator to check if the attempt was successful without having to access the 'success' property + /// + /// + /// + public static implicit operator bool(Attempt a) => a.Success; + + /// + /// Creates a successful attempt. + /// + /// The status of the attempt. + /// The successful attempt. + public static Attempt Succeed(TStatus status) => + new Attempt(true, default, status, null); + + /// + /// Creates a successful attempt with a result. + /// + /// The status of the attempt. + /// The result of the attempt. + /// The successful attempt. + public static Attempt Succeed(TStatus status, TResult result) => + new Attempt(true, result, status, null); + + /// + /// Creates a failed attempt. + /// + /// The status of the attempt. + /// The failed attempt. + public static Attempt Fail(TStatus status) => + new Attempt(false, default, status, null); + + /// + /// Creates a failed attempt with an exception. + /// + /// The status of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(TStatus status, Exception exception) => + new Attempt(false, default, status, exception); + + /// + /// Creates a failed attempt with a result. + /// + /// The status of the attempt. + /// The result of the attempt. + /// The failed attempt. + public static Attempt Fail(TStatus status, TResult result) => + new Attempt(false, result, status, null); + + /// + /// Creates a failed attempt with a result and an exception. + /// + /// The status of the attempt. + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(TStatus status, TResult result, Exception exception) => + new Attempt(false, result, status, exception); + + /// + /// Creates a successful or a failed attempt. + /// + /// A value indicating whether the attempt is successful. + /// The status of the successful attempt. + /// The status of the failed attempt. + /// The attempt. + public static Attempt If(bool condition, TStatus succStatus, TStatus failStatus) => + new Attempt(condition, default, condition ? succStatus : failStatus, null); + + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// A value indicating whether the attempt is successful. + /// The status of the successful attempt. + /// The status of the failed attempt. + /// The result of the attempt. + /// The attempt. + public static Attempt + If(bool condition, TStatus succStatus, TStatus failStatus, TResult result) => + new Attempt(condition, result, condition ? succStatus : failStatus, null); } diff --git a/src/Umbraco.Core/Cache/AppCacheExtensions.cs b/src/Umbraco.Core/Cache/AppCacheExtensions.cs index f5e92cc116..0f1f242ed0 100644 --- a/src/Umbraco.Core/Cache/AppCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/AppCacheExtensions.cs @@ -1,66 +1,64 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extensions for strongly typed access +/// +public static class AppCacheExtensions { - /// - /// Extensions for strongly typed access - /// - public static class AppCacheExtensions + public static T? GetCacheItem( + this IAppPolicyCache provider, + string cacheKey, + Func getCacheItem, + TimeSpan? timeout, + bool isSliding = false, + string[]? dependentFiles = null) { - public static T? GetCacheItem(this IAppPolicyCache provider, - string cacheKey, - Func getCacheItem, - TimeSpan? timeout, - bool isSliding = false, - string[]? dependentFiles = null) + var result = provider.Get(cacheKey, () => getCacheItem(), timeout, isSliding, dependentFiles); + return result == null ? default : result.TryConvertTo().Result; + } + + public static void InsertCacheItem( + this IAppPolicyCache provider, + string cacheKey, + Func getCacheItem, + TimeSpan? timeout = null, + bool isSliding = false, + string[]? dependentFiles = null) => + provider.Insert(cacheKey, () => getCacheItem(), timeout, isSliding, dependentFiles); + + public static IEnumerable GetCacheItemsByKeySearch(this IAppCache provider, string keyStartsWith) + { + IEnumerable result = provider.SearchByKey(keyStartsWith); + return result.Select(x => x.TryConvertTo().Result); + } + + public static IEnumerable GetCacheItemsByKeyExpression(this IAppCache provider, string regexString) + { + IEnumerable result = provider.SearchByRegex(regexString); + return result.Select(x => x.TryConvertTo().Result); + } + + public static T? GetCacheItem(this IAppCache provider, string cacheKey) + { + var result = provider.Get(cacheKey); + if (result == null) { - var result = provider.Get(cacheKey, () => getCacheItem(), timeout, isSliding, dependentFiles); - return result == null ? default(T) : result.TryConvertTo().Result; + return default; } - public static void InsertCacheItem(this IAppPolicyCache provider, - string cacheKey, - Func getCacheItem, - TimeSpan? timeout = null, - bool isSliding = false, - string[]? dependentFiles = null) + return result.TryConvertTo().Result; + } + + public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) + { + var result = provider.Get(cacheKey, () => getCacheItem()); + if (result == null) { - provider.Insert(cacheKey, () => getCacheItem(), timeout, isSliding, dependentFiles); + return default; } - public static IEnumerable GetCacheItemsByKeySearch(this IAppCache provider, string keyStartsWith) - { - var result = provider.SearchByKey(keyStartsWith); - return result.Select(x => x.TryConvertTo().Result); - } - - public static IEnumerable GetCacheItemsByKeyExpression(this IAppCache provider, string regexString) - { - var result = provider.SearchByRegex(regexString); - return result.Select(x => x.TryConvertTo().Result); - } - - public static T? GetCacheItem(this IAppCache provider, string cacheKey) - { - var result = provider.Get(cacheKey); - if (result == null) - { - return default(T); - } - return result.TryConvertTo().Result; - } - - public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) - { - var result = provider.Get(cacheKey, () => getCacheItem()); - if (result == null) - { - return default(T); - } - return result.TryConvertTo().Result; - } + return result.TryConvertTo().Result; } } diff --git a/src/Umbraco.Core/Cache/AppCaches.cs b/src/Umbraco.Core/Cache/AppCaches.cs index a04ece0d04..faca2e14f4 100644 --- a/src/Umbraco.Core/Cache/AppCaches.cs +++ b/src/Umbraco.Core/Cache/AppCaches.cs @@ -1,100 +1,97 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents the application caches. +/// +public class AppCaches : IDisposable { + private bool _disposedValue; + /// - /// Represents the application caches. + /// Initializes a new instance of the with cache providers. /// - public class AppCaches : IDisposable + public AppCaches( + IAppPolicyCache runtimeCache, + IRequestCache requestCache, + IsolatedCaches isolatedCaches) { - private bool _disposedValue; + RuntimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); + RequestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + IsolatedCaches = isolatedCaches ?? throw new ArgumentNullException(nameof(isolatedCaches)); + } - /// - /// Initializes a new instance of the with cache providers. - /// - public AppCaches( - IAppPolicyCache runtimeCache, - IRequestCache requestCache, - IsolatedCaches isolatedCaches) + /// + /// Gets the special disabled instance. + /// + /// + /// When used by repositories, all cache policies apply, but the underlying caches do not cache anything. + /// Used by tests. + /// + public static AppCaches Disabled { get; } = new(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); + + /// + /// Gets the special no-cache instance. + /// + /// + /// When used by repositories, all cache policies are bypassed. + /// Used by repositories that do no cache. + /// + public static AppCaches NoCache { get; } = new(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); + + /// + /// Gets the per-request cache. + /// + /// + /// The per-request caches works on top of the current HttpContext items. + /// Outside a web environment, the behavior of that cache is unspecified. + /// + public IRequestCache RequestCache { get; } + + /// + /// Gets the runtime cache. + /// + /// + /// The runtime cache is the main application cache. + /// + public IAppPolicyCache RuntimeCache { get; } + + /// + /// Gets the isolated caches. + /// + /// + /// + /// Isolated caches are used by e.g. repositories, to ensure that each cached entity + /// type has its own cache, so that lookups are fast and the repository does not need to + /// search through all keys on a global scale. + /// + /// + public IsolatedCaches IsolatedCaches { get; } + + public static AppCaches Create(IRequestCache requestCache) => + new( + new DeepCloneAppCache(new ObjectCacheAppCache()), + requestCache, + new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - RuntimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); - RequestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - IsolatedCaches = isolatedCaches ?? throw new ArgumentNullException(nameof(isolatedCaches)); - } - - /// - /// Gets the special disabled instance. - /// - /// - /// When used by repositories, all cache policies apply, but the underlying caches do not cache anything. - /// Used by tests. - /// - public static AppCaches Disabled { get; } = new AppCaches(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); - - /// - /// Gets the special no-cache instance. - /// - /// - /// When used by repositories, all cache policies are bypassed. - /// Used by repositories that do no cache. - /// - public static AppCaches NoCache { get; } = new AppCaches(NoAppCache.Instance, NoAppCache.Instance, new IsolatedCaches(_ => NoAppCache.Instance)); - - /// - /// Gets the per-request cache. - /// - /// - /// The per-request caches works on top of the current HttpContext items. - /// Outside a web environment, the behavior of that cache is unspecified. - /// - public IRequestCache RequestCache { get; } - - /// - /// Gets the runtime cache. - /// - /// - /// The runtime cache is the main application cache. - /// - public IAppPolicyCache RuntimeCache { get; } - - /// - /// Gets the isolated caches. - /// - /// - /// Isolated caches are used by e.g. repositories, to ensure that each cached entity - /// type has its own cache, so that lookups are fast and the repository does not need to - /// search through all keys on a global scale. - /// - public IsolatedCaches IsolatedCaches { get; } - - public static AppCaches Create(IRequestCache requestCache) - { - return new AppCaches( - new DeepCloneAppCache(new ObjectCacheAppCache()), - requestCache, - new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - RuntimeCache.DisposeIfDisposable(); - RequestCache.DisposeIfDisposable(); - IsolatedCaches.Dispose(); - } - - _disposedValue = true; + RuntimeCache.DisposeIfDisposable(); + RequestCache.DisposeIfDisposable(); + IsolatedCaches.Dispose(); } - } - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + _disposedValue = true; } } } diff --git a/src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs b/src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs index 53e45bbb2e..1cf3b1461e 100644 --- a/src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs +++ b/src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs @@ -1,99 +1,93 @@ -using System; using System.Collections.Concurrent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Provides a base class for implementing a dictionary of . +/// +/// The type of the dictionary key. +public abstract class AppPolicedCacheDictionary : IDisposable + where TKey : notnull { /// - /// Provides a base class for implementing a dictionary of . + /// Gets the internal cache factory, for tests only! /// - /// The type of the dictionary key. - public abstract class AppPolicedCacheDictionary : IDisposable - where TKey : notnull + private readonly Func _cacheFactory; + + private readonly ConcurrentDictionary _caches = new(); + private bool _disposedValue; + + /// + /// Initializes a new instance of the class. + /// + /// + protected AppPolicedCacheDictionary(Func cacheFactory) => _cacheFactory = cacheFactory; + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + /// + /// Gets or creates a cache. + /// + public IAppPolicyCache GetOrCreate(TKey key) + => _caches.GetOrAdd(key, k => _cacheFactory(k)); + + /// + /// Removes a cache. + /// + public void Remove(TKey key) => _caches.TryRemove(key, out _); + + /// + /// Removes all caches. + /// + public void RemoveAll() => _caches.Clear(); + + /// + /// Clears all caches. + /// + public void ClearAllCaches() { - private readonly ConcurrentDictionary _caches = new ConcurrentDictionary(); - - /// - /// Initializes a new instance of the class. - /// - /// - protected AppPolicedCacheDictionary(Func cacheFactory) + foreach (IAppPolicyCache cache in _caches.Values) { - _cacheFactory = cacheFactory; + cache.Clear(); } + } - /// - /// Gets the internal cache factory, for tests only! - /// - private readonly Func _cacheFactory; - private bool _disposedValue; + /// + /// Tries to get a cache. + /// + protected Attempt Get(TKey key) + => _caches.TryGetValue(key, out IAppPolicyCache? cache) + ? Attempt.Succeed(cache) + : Attempt.Fail(); - /// - /// Gets or creates a cache. - /// - public IAppPolicyCache GetOrCreate(TKey key) - => _caches.GetOrAdd(key, k => _cacheFactory(k)); - - /// - /// Tries to get a cache. - /// - protected Attempt Get(TKey key) - => _caches.TryGetValue(key, out var cache) ? Attempt.Succeed(cache) : Attempt.Fail(); - - /// - /// Removes a cache. - /// - public void Remove(TKey key) + /// + /// Clears a cache. + /// + protected void ClearCache(TKey key) + { + if (_caches.TryGetValue(key, out IAppPolicyCache? cache)) { - _caches.TryRemove(key, out _); + cache.Clear(); } + } - /// - /// Removes all caches. - /// - public void RemoveAll() + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - _caches.Clear(); - } - - /// - /// Clears a cache. - /// - protected void ClearCache(TKey key) - { - if (_caches.TryGetValue(key, out var cache)) - cache.Clear(); - } - - /// - /// Clears all caches. - /// - public void ClearAllCaches() - { - foreach (var cache in _caches.Values) - cache.Clear(); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) + if (disposing) { - if (disposing) + foreach (IAppPolicyCache value in _caches.Values) { - foreach(var value in _caches.Values) - { - value.DisposeIfDisposable(); - } + value.DisposeIfDisposable(); } - - _disposedValue = true; } - } - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + _disposedValue = true; } } } diff --git a/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs b/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs index 582915fb2e..11ddd8a183 100644 --- a/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ApplicationCacheRefresher.cs @@ -1,46 +1,36 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class ApplicationCacheRefresher : CacheRefresherBase { - public sealed class ApplicationCacheRefresher : CacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("B15F34A1-BC1D-4F8B-8369-3222728AB4C8"); + + public ApplicationCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public ApplicationCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { - } + } - #region Define + public override Guid RefresherUniqueId => UniqueId; - public static readonly Guid UniqueId = Guid.Parse("B15F34A1-BC1D-4F8B-8369-3222728AB4C8"); + public override string Name => "Application Cache Refresher"; - public override Guid RefresherUniqueId => UniqueId; + public override void RefreshAll() + { + AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); + base.RefreshAll(); + } - public override string Name => "Application Cache Refresher"; + public override void Refresh(int id) + { + Remove(id); + base.Refresh(id); + } - #endregion - - #region Refresher - - public override void RefreshAll() - { - AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); - base.Remove(id); - } - - #endregion + public override void Remove(int id) + { + AppCaches.RuntimeCache.Clear(CacheKeys.ApplicationsCacheKey); + base.Remove(id); } } diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index acabe0fcc4..04ae44a647 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -1,26 +1,25 @@ -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Constants storing cache keys used in caching +/// +public static class CacheKeys { - /// - /// Constants storing cache keys used in caching - /// - public static class CacheKeys - { - public const string ApplicationsCacheKey = "ApplicationCache"; // used by SectionService + public const string ApplicationsCacheKey = "ApplicationCache"; // used by SectionService - // TODO: this one can probably be removed - public const string TemplateFrontEndCacheKey = "template"; + // TODO: this one can probably be removed + public const string TemplateFrontEndCacheKey = "template"; - public const string MacroContentCacheKey = "macroContent_"; // used in MacroRenderers - public const string MacroFromAliasCacheKey = "macroFromAlias_"; + public const string MacroContentCacheKey = "macroContent_"; // used in MacroRenderers + public const string MacroFromAliasCacheKey = "macroFromAlias_"; - public const string UserGroupGetByAliasCacheKeyPrefix = "UserGroupRepository_GetByAlias_"; + public const string UserGroupGetByAliasCacheKeyPrefix = "UserGroupRepository_GetByAlias_"; - public const string UserAllContentStartNodesPrefix = "AllContentStartNodes"; - public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; - public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; - public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; - - public const string ContentRecycleBinCacheKey = "recycleBin_content"; - public const string MediaRecycleBinCacheKey = "recycleBin_media"; - } + public const string UserAllContentStartNodesPrefix = "AllContentStartNodes"; + public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; + public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; + public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; + + public const string ContentRecycleBinCacheKey = "recycleBin_content"; + public const string MediaRecycleBinCacheKey = "recycleBin_media"; } diff --git a/src/Umbraco.Core/Cache/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/CacheRefresherBase.cs index 7b962065c5..849d42309a 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherBase.cs @@ -1,122 +1,104 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for cache refreshers that handles events. +/// +/// The actual cache refresher type is used for strongly typed events. +public abstract class CacheRefresherBase : ICacheRefresher + where TNotification : CacheRefresherNotification { /// - /// A base class for cache refreshers that handles events. + /// Initializes a new instance of the . /// - /// The actual cache refresher type. - /// The actual cache refresher type is used for strongly typed events. - public abstract class CacheRefresherBase : ICacheRefresher - where TNotification : CacheRefresherNotification + protected CacheRefresherBase(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) { - /// - /// Initializes a new instance of the . - /// - /// A cache helper. - protected CacheRefresherBase(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - { - AppCaches = appCaches; - EventAggregator = eventAggregator; - NotificationFactory = factory; - } - - #region Define - - /// - /// Gets the unique identifier of the refresher. - /// - public abstract Guid RefresherUniqueId { get; } - - /// - /// Gets the name of the refresher. - /// - public abstract string Name { get; } - - /// - /// Gets the for - /// - protected ICacheRefresherNotificationFactory NotificationFactory { get; } - - #endregion - - #region Refresher - - /// - /// Refreshes all entities. - /// - public virtual void RefreshAll() - { - // NOTE: We pass in string.Empty here because if we pass in NULL this causes problems with - // the underlying ActivatorUtilities.CreateInstance which doesn't seem to support passing in - // null to an 'object' parameter and we end up with "A suitable constructor for type 'ZYZ' could not be located." - // In this case, all cache refreshers should be checking for the type first before checking for a msg value - // so this shouldn't cause any issues. - OnCacheUpdated(NotificationFactory.Create(string.Empty, MessageType.RefreshAll)); - } - - /// - /// Refreshes an entity. - /// - /// The entity's identifier. - public virtual void Refresh(int id) - { - OnCacheUpdated(NotificationFactory.Create(id, MessageType.RefreshById)); - } - - /// - /// Refreshes an entity. - /// - /// The entity's identifier. - public virtual void Refresh(Guid id) - { - OnCacheUpdated(NotificationFactory.Create(id, MessageType.RefreshById)); - } - - /// - /// Removes an entity. - /// - /// The entity's identifier. - public virtual void Remove(int id) - { - OnCacheUpdated(NotificationFactory.Create(id, MessageType.RemoveById)); - } - - #endregion - - #region Protected - - /// - /// Gets the cache helper. - /// - protected AppCaches AppCaches { get; } - - protected IEventAggregator EventAggregator { get; } - - /// - /// Clears the cache for all repository entities of a specified type. - /// - /// The type of the entities. - protected void ClearAllIsolatedCacheByEntityType() - where TEntity : class, IEntity - { - AppCaches.IsolatedCaches.ClearCache(); - } - - /// - /// Raises the CacheUpdated event. - /// - /// The event sender. - /// The event arguments. - protected void OnCacheUpdated(CacheRefresherNotification notification) - { - EventAggregator.Publish(notification); - } - - #endregion + AppCaches = appCaches; + EventAggregator = eventAggregator; + NotificationFactory = factory; } + + #region Define + + /// + /// Gets the unique identifier of the refresher. + /// + public abstract Guid RefresherUniqueId { get; } + + /// + /// Gets the name of the refresher. + /// + public abstract string Name { get; } + + /// + /// Gets the for + /// + protected ICacheRefresherNotificationFactory NotificationFactory { get; } + + #endregion + + #region Refresher + + /// + /// Refreshes all entities. + /// + public virtual void RefreshAll() => + + // NOTE: We pass in string.Empty here because if we pass in NULL this causes problems with + // the underlying ActivatorUtilities.CreateInstance which doesn't seem to support passing in + // null to an 'object' parameter and we end up with "A suitable constructor for type 'ZYZ' could not be located." + // In this case, all cache refreshers should be checking for the type first before checking for a msg value + // so this shouldn't cause any issues. + OnCacheUpdated(NotificationFactory.Create(string.Empty, MessageType.RefreshAll)); + + /// + /// Refreshes an entity. + /// + /// The entity's identifier. + public virtual void Refresh(int id) => + OnCacheUpdated(NotificationFactory.Create(id, MessageType.RefreshById)); + + /// + /// Refreshes an entity. + /// + /// The entity's identifier. + public virtual void Refresh(Guid id) => + OnCacheUpdated(NotificationFactory.Create(id, MessageType.RefreshById)); + + /// + /// Removes an entity. + /// + /// The entity's identifier. + public virtual void Remove(int id) => + OnCacheUpdated(NotificationFactory.Create(id, MessageType.RemoveById)); + + #endregion + + #region Protected + + /// + /// Gets the cache helper. + /// + protected AppCaches AppCaches { get; } + + protected IEventAggregator EventAggregator { get; } + + /// + /// Clears the cache for all repository entities of a specified type. + /// + /// The type of the entities. + protected void ClearAllIsolatedCacheByEntityType() + where TEntity : class, IEntity => + AppCaches.IsolatedCaches.ClearCache(); + + /// + /// Raises the CacheUpdated event. + /// + protected void OnCacheUpdated(CacheRefresherNotification notification) => EventAggregator.Publish(notification); + + #endregion } diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollection.cs b/src/Umbraco.Core/Cache/CacheRefresherCollection.cs index b9dc7f5984..301f6bbdaf 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherCollection.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherCollection.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Cache -{ - public class CacheRefresherCollection : BuilderCollectionBase - { - public CacheRefresherCollection(Func> items) : base(items) - { - } +namespace Umbraco.Cms.Core.Cache; - public ICacheRefresher? this[Guid id] - => this.FirstOrDefault(x => x.RefresherUniqueId == id); +public class CacheRefresherCollection : BuilderCollectionBase +{ + public CacheRefresherCollection(Func> items) + : base(items) + { } + + public ICacheRefresher? this[Guid id] + => this.FirstOrDefault(x => x.RefresherUniqueId == id); } diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs b/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs index 34a274a177..79b44ab53d 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public class CacheRefresherCollectionBuilder : LazyCollectionBuilderBase { - public class CacheRefresherCollectionBuilder : LazyCollectionBuilderBase - { - protected override CacheRefresherCollectionBuilder This => this; - } + protected override CacheRefresherCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs b/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs index bd41ee9d9b..40bab16b12 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherNotificationFactory.cs @@ -1,24 +1,24 @@ -using System; using Umbraco.Cms.Core.Notifications; -using Umbraco.Extensions; using Umbraco.Cms.Core.Sync; +using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A that uses ActivatorUtilities to create the +/// instances +/// +public sealed class CacheRefresherNotificationFactory : ICacheRefresherNotificationFactory { + private readonly IServiceProvider _serviceProvider; + + public CacheRefresherNotificationFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + /// - /// A that uses ActivatorUtilities to create the instances + /// Create a using ActivatorUtilities /// - public sealed class CacheRefresherNotificationFactory : ICacheRefresherNotificationFactory - { - private readonly IServiceProvider _serviceProvider; - - public CacheRefresherNotificationFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; - - /// - /// Create a using ActivatorUtilities - /// - /// The to create - public TNotification Create(object msgObject, MessageType type) where TNotification : CacheRefresherNotification - => _serviceProvider.CreateInstance(new object[] { msgObject, type }); - } + /// The to create + public TNotification Create(object msgObject, MessageType type) + where TNotification : CacheRefresherNotification + => _serviceProvider.CreateInstance(msgObject, type); } diff --git a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/ContentCacheRefresher.cs index ff55a201f5..a515d5c5d1 100644 --- a/src/Umbraco.Core/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ContentCacheRefresher.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -11,168 +8,170 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class ContentCacheRefresher : PayloadCacheRefresherBase { - public sealed class ContentCacheRefresher : PayloadCacheRefresherBase + private readonly IDomainService _domainService; + private readonly IIdKeyMap _idKeyMap; + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public ContentCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IIdKeyMap idKeyMap, + IDomainService domainService, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IIdKeyMap _idKeyMap; - private readonly IDomainService _domainService; - - public ContentCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IIdKeyMap idKeyMap, - IDomainService domainService, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - _publishedSnapshotService = publishedSnapshotService; - _idKeyMap = idKeyMap; - _domainService = domainService; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "ContentCacheRefresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - AppCaches.RuntimeCache.ClearOfType(); - AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); - - var idsRemoved = new HashSet(); - var isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); - - foreach (var payload in payloads.Where(x => x.Id != default)) - { - //By INT Id - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - //By GUID Key - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); - - _idKeyMap.ClearCache(payload.Id); - - // remove those that are in the branch - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) - { - var pathid = "," + payload.Id + ","; - isolatedCache.ClearOfType((k, v) => v.Path?.Contains(pathid) ?? false); - } - - //if the item is being completely removed, we need to refresh the domains cache if any domain was assigned to the content - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) - { - idsRemoved.Add(payload.Id); - } - } - - if (idsRemoved.Count > 0) - { - var assignedDomains = _domainService.GetAll(true)?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList(); - - if (assignedDomains?.Count > 0) - { - // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, - // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the - // DomainCacheRefresher? - - ClearAllIsolatedCacheByEntityType(); - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - // notify - _publishedSnapshotService.Notify(assignedDomains.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); - } - } - - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - - // TODO: what about this? - // should rename it, and then, this is only for Deploy, and then, ??? - //if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) - // ... - - NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); - - base.Refresh(payloads); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() => throw new NotSupportedException(); - - public override void Refresh(int id) => throw new NotSupportedException(); - - public override void Refresh(Guid id) => throw new NotSupportedException(); - - public override void Remove(int id) => throw new NotSupportedException(); - - #endregion - - #region Json - - /// - /// Refreshes the publish snapshot service and if there are published changes ensures that partial view caches are refreshed too - /// - /// - /// - /// - internal static void NotifyPublishedSnapshotService(IPublishedSnapshotService service, AppCaches appCaches, JsonPayload[] payloads) - { - service.Notify(payloads, out _, out var publishedChanged); - - if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) - { - // when a public version changes - appCaches.ClearPartialViewCache(); - } - } - - public class JsonPayload - { - public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) - { - Id = id; - Key = key; - ChangeTypes = changeTypes; - } - - public int Id { get; } - public Guid? Key { get; } - public TreeChangeTypes ChangeTypes { get; } - } - - #endregion - - #region Indirect - - public static void RefreshContentTypes(AppCaches appCaches) - { - // we could try to have a mechanism to notify the PublishedCachesService - // and figure out whether published items were modified or not... keep it - // simple for now, just clear the whole thing - - appCaches.ClearPartialViewCache(); - - appCaches.IsolatedCaches.ClearCache(); - appCaches.IsolatedCaches.ClearCache(); - } - - #endregion - + _publishedSnapshotService = publishedSnapshotService; + _idKeyMap = idKeyMap; + _domainService = domainService; } + + #region Indirect + + public static void RefreshContentTypes(AppCaches appCaches) + { + // we could try to have a mechanism to notify the PublishedCachesService + // and figure out whether published items were modified or not... keep it + // simple for now, just clear the whole thing + appCaches.ClearPartialViewCache(); + + appCaches.IsolatedCaches.ClearCache(); + appCaches.IsolatedCaches.ClearCache(); + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "ContentCacheRefresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + AppCaches.RuntimeCache.ClearOfType(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); + + var idsRemoved = new HashSet(); + IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); + + foreach (JsonPayload payload in payloads.Where(x => x.Id != default)) + { + // By INT Id + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + + // By GUID Key + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + + _idKeyMap.ClearCache(payload.Id); + + // remove those that are in the branch + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) + { + var pathid = "," + payload.Id + ","; + isolatedCache.ClearOfType((k, v) => v.Path?.Contains(pathid) ?? false); + } + + // if the item is being completely removed, we need to refresh the domains cache if any domain was assigned to the content + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) + { + idsRemoved.Add(payload.Id); + } + } + + if (idsRemoved.Count > 0) + { + var assignedDomains = _domainService.GetAll(true) + ?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList(); + + if (assignedDomains?.Count > 0) + { + // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, + // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the + // DomainCacheRefresher? + ClearAllIsolatedCacheByEntityType(); + + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + // notify + _publishedSnapshotService.Notify(assignedDomains + .Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); + } + } + + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + + // TODO: what about this? + // should rename it, and then, this is only for Deploy, and then, ??? + // if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + // ... + NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); + + base.Refresh(payloads); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion + + #region Json + + /// + /// Refreshes the publish snapshot service and if there are published changes ensures that partial view caches are + /// refreshed too + /// + /// + /// + /// + internal static void NotifyPublishedSnapshotService(IPublishedSnapshotService service, AppCaches appCaches, JsonPayload[] payloads) + { + service.Notify(payloads, out _, out var publishedChanged); + + if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) + { + // when a public version changes + appCaches.ClearPartialViewCache(); + } + } + + public class JsonPayload + { + public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) + { + Id = id; + Key = key; + ChangeTypes = changeTypes; + } + + public int Id { get; } + + public Guid? Key { get; } + + public TreeChangeTypes ChangeTypes { get; } + } + + #endregion } diff --git a/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs index 9a709e9a9f..e1a82d6108 100644 --- a/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ContentTypeCacheRefresher.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -11,136 +9,127 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase { - public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase + private readonly IContentTypeCommonRepository _contentTypeCommonRepository; + private readonly IIdKeyMap _idKeyMap; + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public ContentTypeCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IPublishedModelFactory publishedModelFactory, + IIdKeyMap idKeyMap, + IContentTypeCommonRepository contentTypeCommonRepository, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IContentTypeCommonRepository _contentTypeCommonRepository; - private readonly IIdKeyMap _idKeyMap; - - public ContentTypeCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IPublishedModelFactory publishedModelFactory, - IIdKeyMap idKeyMap, - IContentTypeCommonRepository contentTypeCommonRepository, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - _publishedSnapshotService = publishedSnapshotService; - _publishedModelFactory = publishedModelFactory; - _idKeyMap = idKeyMap; - _contentTypeCommonRepository = contentTypeCommonRepository; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("6902E22C-9C10-483C-91F3-66B7CAE9E2F5"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Content Type Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - // TODO: refactor - // we should NOT directly clear caches here, but instead ask whatever class - // is managing the cache to please clear that cache properly - - _contentTypeCommonRepository.ClearCache(); // always - - if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) - { - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - } - - if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) - { - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - } - - if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) - { - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - } - - foreach (var id in payloads.Select(x => x.Id)) - { - _idKeyMap.ClearCache(id); - } - - if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) - // don't try to be clever - refresh all - ContentCacheRefresher.RefreshContentTypes(AppCaches); - - if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) - // don't try to be clever - refresh all - MediaCacheRefresher.RefreshMediaTypes(AppCaches); - - if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) - // don't try to be clever - refresh all - MemberCacheRefresher.RefreshMemberTypes(AppCaches); - - // refresh the models and cache - _publishedModelFactory.WithSafeLiveFactoryReset(() => - _publishedSnapshotService.Notify(payloads)); - - // now we can trigger the event - base.Refresh(payloads); - } - - - public override void RefreshAll() - { - throw new NotSupportedException(); - } - - public override void Refresh(int id) - { - throw new NotSupportedException(); - } - - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } - - public override void Remove(int id) - { - throw new NotSupportedException(); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(string itemType, int id, ContentTypeChangeTypes changeTypes) - { - ItemType = itemType; - Id = id; - ChangeTypes = changeTypes; - } - - public string ItemType { get; } - - public int Id { get; } - - public ContentTypeChangeTypes ChangeTypes { get; } - } - - #endregion + _publishedSnapshotService = publishedSnapshotService; + _publishedModelFactory = publishedModelFactory; + _idKeyMap = idKeyMap; + _contentTypeCommonRepository = contentTypeCommonRepository; } + + #region Json + + public class JsonPayload + { + public JsonPayload(string itemType, int id, ContentTypeChangeTypes changeTypes) + { + ItemType = itemType; + Id = id; + ChangeTypes = changeTypes; + } + + public string ItemType { get; } + + public int Id { get; } + + public ContentTypeChangeTypes ChangeTypes { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("6902E22C-9C10-483C-91F3-66B7CAE9E2F5"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Content Type Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + // TODO: refactor + // we should NOT directly clear caches here, but instead ask whatever class + // is managing the cache to please clear that cache properly + _contentTypeCommonRepository.ClearCache(); // always + + if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) + { + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + } + + if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) + { + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + } + + if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) + { + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + } + + foreach (var id in payloads.Select(x => x.Id)) + { + _idKeyMap.ClearCache(id); + } + + if (payloads.Any(x => x.ItemType == typeof(IContentType).Name)) + { + // don't try to be clever - refresh all + ContentCacheRefresher.RefreshContentTypes(AppCaches); + } + + if (payloads.Any(x => x.ItemType == typeof(IMediaType).Name)) + { + // don't try to be clever - refresh all + MediaCacheRefresher.RefreshMediaTypes(AppCaches); + } + + if (payloads.Any(x => x.ItemType == typeof(IMemberType).Name)) + { + // don't try to be clever - refresh all + MemberCacheRefresher.RefreshMemberTypes(AppCaches); + } + + // refresh the models and cache + _publishedModelFactory.WithSafeLiveFactoryReset(() => + _publishedSnapshotService.Notify(payloads)); + + // now we can trigger the event + base.Refresh(payloads); + } + + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs index 44d730be83..ea661c5498 100644 --- a/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -10,121 +9,105 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase { - public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase + private readonly IIdKeyMap _idKeyMap; + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public DataTypeCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IPublishedModelFactory publishedModelFactory, + IIdKeyMap idKeyMap, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IIdKeyMap _idKeyMap; - - public DataTypeCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IPublishedModelFactory publishedModelFactory, - IIdKeyMap idKeyMap, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - _publishedSnapshotService = publishedSnapshotService; - _publishedModelFactory = publishedModelFactory; - _idKeyMap = idKeyMap; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Data Type Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - //we need to clear the ContentType runtime cache since that is what caches the - // db data type to store the value against and anytime a datatype changes, this also might change - // we basically need to clear all sorts of runtime caches here because so many things depend upon a data type - - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - - var dataTypeCache = AppCaches.IsolatedCaches.Get(); - - foreach (var payload in payloads) - { - _idKeyMap.ClearCache(payload.Id); - - if (dataTypeCache.Success) - { - dataTypeCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - } - } - - // TODO: not sure I like these? - TagsValueConverter.ClearCaches(); - SliderValueConverter.ClearCaches(); - - // refresh the models and cache - - _publishedModelFactory.WithSafeLiveFactoryReset(() => - _publishedSnapshotService.Notify(payloads)); - - base.Refresh(payloads); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() - { - throw new NotSupportedException(); - } - - public override void Refresh(int id) - { - throw new NotSupportedException(); - } - - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } - - public override void Remove(int id) - { - throw new NotSupportedException(); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, Guid key, bool removed) - { - Id = id; - Key = key; - Removed = removed; - } - - public int Id { get; } - - public Guid Key { get; } - - public bool Removed { get; } - } - - #endregion + _publishedSnapshotService = publishedSnapshotService; + _publishedModelFactory = publishedModelFactory; + _idKeyMap = idKeyMap; } + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, Guid key, bool removed) + { + Id = id; + Key = key; + Removed = removed; + } + + public int Id { get; } + + public Guid Key { get; } + + public bool Removed { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Data Type Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + // we need to clear the ContentType runtime cache since that is what caches the + // db data type to store the value against and anytime a datatype changes, this also might change + // we basically need to clear all sorts of runtime caches here because so many things depend upon a data type + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + + Attempt dataTypeCache = AppCaches.IsolatedCaches.Get(); + + foreach (JsonPayload payload in payloads) + { + _idKeyMap.ClearCache(payload.Id); + + if (dataTypeCache.Success) + { + dataTypeCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + } + } + + // TODO: not sure I like these? + TagsValueConverter.ClearCaches(); + SliderValueConverter.ClearCaches(); + + // refresh the models and cache + _publishedModelFactory.WithSafeLiveFactoryReset(() => + _publishedSnapshotService.Notify(payloads)); + + base.Refresh(payloads); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/DeepCloneAppCache.cs b/src/Umbraco.Core/Cache/DeepCloneAppCache.cs index 60a0d8d7b3..da86be4b70 100644 --- a/src/Umbraco.Core/Cache/DeepCloneAppCache.cs +++ b/src/Umbraco.Core/Cache/DeepCloneAppCache.cs @@ -1,178 +1,163 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements by wrapping an inner other +/// instance, and ensuring that all inserts and returns are deep cloned copies of the cache item, +/// when the item is deep-cloneable. +/// +public class DeepCloneAppCache : IAppPolicyCache, IDisposable { + private bool _disposedValue; + /// - /// Implements by wrapping an inner other - /// instance, and ensuring that all inserts and returns are deep cloned copies of the cache item, - /// when the item is deep-cloneable. + /// Initializes a new instance of the class. /// - public class DeepCloneAppCache : IAppPolicyCache, IDisposable + public DeepCloneAppCache(IAppPolicyCache innerCache) { - private bool _disposedValue; + Type type = typeof(DeepCloneAppCache); - /// - /// Initializes a new instance of the class. - /// - public DeepCloneAppCache(IAppPolicyCache innerCache) + if (innerCache.GetType() == type) { - var type = typeof (DeepCloneAppCache); - - if (innerCache.GetType() == type) - throw new InvalidOperationException($"A {type} cannot wrap another instance of itself."); - - InnerCache = innerCache; + throw new InvalidOperationException($"A {type} cannot wrap another instance of itself."); } - /// - /// Gets the inner cache. - /// - private IAppPolicyCache InnerCache { get; } + InnerCache = innerCache; + } - /// - public object? Get(string key) + /// + /// Gets the inner cache. + /// + private IAppPolicyCache InnerCache { get; } + + /// + public object? Get(string key) + { + var item = InnerCache.Get(key); + return CheckCloneableAndTracksChanges(item); + } + + /// + public object? Get(string key, Func factory) + { + var cached = InnerCache.Get(key, () => { - var item = InnerCache.Get(key); - return CheckCloneableAndTracksChanges(item); - } + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - /// - public object? Get(string key, Func factory) + // do not store null values (backward compat), clone / reset to go into the cache + return value == null ? null : CheckCloneableAndTracksChanges(value); + }); + return CheckCloneableAndTracksChanges(cached); + } + + /// + public IEnumerable SearchByKey(string keyStartsWith) => + InnerCache.SearchByKey(keyStartsWith) + .Select(CheckCloneableAndTracksChanges); + + /// + public IEnumerable SearchByRegex(string regex) => + InnerCache.SearchByRegex(regex) + .Select(CheckCloneableAndTracksChanges); + + /// + public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) + { + var cached = InnerCache.Get( + key, + () => { - var cached = InnerCache.Get(key, () => - { - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - // do not store null values (backward compat), clone / reset to go into the cache - return value == null ? null : CheckCloneableAndTracksChanges(value); - }); - return CheckCloneableAndTracksChanges(cached); - } + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result + .Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - /// - public IEnumerable SearchByKey(string keyStartsWith) - { - return InnerCache.SearchByKey(keyStartsWith) - .Select(CheckCloneableAndTracksChanges); - } - - /// - public IEnumerable SearchByRegex(string regex) - { - return InnerCache.SearchByRegex(regex) - .Select(CheckCloneableAndTracksChanges); - } - - /// - public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) - { - var cached = InnerCache.Get(key, () => - { - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - // do not store null values (backward compat), clone / reset to go into the cache - return value == null ? null : CheckCloneableAndTracksChanges(value); - - // clone / reset to go into the cache - }, timeout, isSliding, dependentFiles); + // do not store null values (backward compat), clone / reset to go into the cache + return value == null ? null : CheckCloneableAndTracksChanges(value); // clone / reset to go into the cache - return CheckCloneableAndTracksChanges(cached); - } + }, + timeout, + isSliding, + dependentFiles); - /// - public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + // clone / reset to go into the cache + return CheckCloneableAndTracksChanges(cached); + } + + /// + public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) => + InnerCache.Insert( + key, + () => { - InnerCache.Insert(key, () => + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result + .Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache + + // do not store null values (backward compat), clone / reset to go into the cache + return value == null ? null : CheckCloneableAndTracksChanges(value); + }, + timeout, + isSliding, + dependentFiles); + + /// + public void Clear() => InnerCache.Clear(); + + /// + public void Clear(string key) => InnerCache.Clear(key); + + /// + public void ClearOfType(Type type) => InnerCache.ClearOfType(type); + + /// + public void ClearOfType() => InnerCache.ClearOfType(); + + /// + public void ClearOfType(Func predicate) => InnerCache.ClearOfType(predicate); + + /// + public void ClearByKey(string keyStartsWith) => InnerCache.ClearByKey(keyStartsWith); + + /// + public void ClearByRegex(string regex) => InnerCache.ClearByRegex(regex); + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) { - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache - // do not store null values (backward compat), clone / reset to go into the cache - return value == null ? null : CheckCloneableAndTracksChanges(value); - }, timeout, isSliding, dependentFiles); - } - - /// - public void Clear() - { - InnerCache.Clear(); - } - - /// - public void Clear(string key) - { - InnerCache.Clear(key); - } - - /// - public void ClearOfType(Type type) - { - InnerCache.ClearOfType(type); - } - - /// - public void ClearOfType() - { - InnerCache.ClearOfType(); - } - - /// - public void ClearOfType(Func predicate) - { - InnerCache.ClearOfType(predicate); - } - - /// - public void ClearByKey(string keyStartsWith) - { - InnerCache.ClearByKey(keyStartsWith); - } - - /// - public void ClearByRegex(string regex) - { - InnerCache.ClearByRegex(regex); - } - - private static object? CheckCloneableAndTracksChanges(object? input) - { - if (input is IDeepCloneable cloneable) - { - input = cloneable.DeepClone(); + InnerCache.DisposeIfDisposable(); } - // reset dirty initial properties - if (input is IRememberBeingDirty tracksChanges) - { - tracksChanges.ResetDirtyProperties(false); - input = tracksChanges; - } - - return input; - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - InnerCache.DisposeIfDisposable(); - } - - _disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); + _disposedValue = true; } } + + private static object? CheckCloneableAndTracksChanges(object? input) + { + if (input is IDeepCloneable cloneable) + { + input = cloneable.DeepClone(); + } + + // reset dirty initial properties + if (input is IRememberBeingDirty tracksChanges) + { + tracksChanges.ResetDirtyProperties(false); + input = tracksChanges; + } + + return input; + } } diff --git a/src/Umbraco.Core/Cache/DictionaryAppCache.cs b/src/Umbraco.Core/Cache/DictionaryAppCache.cs index 296050a361..5bf5848309 100644 --- a/src/Umbraco.Core/Cache/DictionaryAppCache.cs +++ b/src/Umbraco.Core/Cache/DictionaryAppCache.cs @@ -1,111 +1,103 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements on top of a concurrent dictionary. +/// +public class DictionaryAppCache : IRequestCache { /// - /// Implements on top of a concurrent dictionary. + /// Gets the internal items dictionary, for tests only! /// - public class DictionaryAppCache : IRequestCache + private readonly ConcurrentDictionary _items = new(); + + public int Count => _items.Count; + + /// + public bool IsAvailable => true; + + /// + public virtual object? Get(string key) => _items.TryGetValue(key, out var value) ? value : null; + + /// + public virtual object? Get(string key, Func factory) => _items.GetOrAdd(key, _ => factory()); + + public bool Set(string key, object? value) => _items.TryAdd(key, value); + + public bool Remove(string key) => _items.TryRemove(key, out _); + + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) { - /// - /// Gets the internal items dictionary, for tests only! - /// - private readonly ConcurrentDictionary _items = new ConcurrentDictionary(); - - public int Count => _items.Count; - - /// - public bool IsAvailable => true; - - /// - public virtual object? Get(string key) + var items = new List(); + foreach ((string key, object? value) in _items) { - return _items.TryGetValue(key, out var value) ? value : null; + if (key.InvariantStartsWith(keyStartsWith)) + { + items.Add(value); + } } - /// - public virtual object? Get(string key, Func factory) - { - return _items.GetOrAdd(key, _ => factory()); - } - - public bool Set(string key, object? value) => _items.TryAdd(key, value); - - public bool Remove(string key) => _items.TryRemove(key, out _); - - /// - public virtual IEnumerable SearchByKey(string keyStartsWith) - { - var items = new List(); - foreach (var (key, value) in _items) - if (key.InvariantStartsWith(keyStartsWith)) - items.Add(value); - return items; - } - - /// - public IEnumerable SearchByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - var items = new List(); - foreach (var (key, value) in _items) - if (compiled.IsMatch(key)) - items.Add(value); - return items; - } - - /// - public virtual void Clear() - { - _items.Clear(); - } - - /// - public virtual void Clear(string key) - { - _items.TryRemove(key, out _); - } - - /// - public virtual void ClearOfType(Type type) - { - _items.RemoveAll(kvp => kvp.Value != null && kvp.Value.GetType() == type); - } - - /// - public virtual void ClearOfType() - { - var typeOfT = typeof(T); - ClearOfType(typeOfT); - } - - /// - public virtual void ClearOfType(Func predicate) - { - var typeOfT = typeof(T); - _items.RemoveAll(kvp => kvp.Value != null && kvp.Value.GetType() == typeOfT && predicate(kvp.Key, (T)kvp.Value)); - } - - /// - public virtual void ClearByKey(string keyStartsWith) - { - _items.RemoveAll(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)); - } - - /// - public virtual void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - _items.RemoveAll(kvp => compiled.IsMatch(kvp.Key)); - } - - public IEnumerator> GetEnumerator() => _items.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return items; } + + /// + public IEnumerable SearchByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + var items = new List(); + foreach ((string key, object? value) in _items) + { + if (compiled.IsMatch(key)) + { + items.Add(value); + } + } + + return items; + } + + /// + public virtual void Clear() => _items.Clear(); + + /// + public virtual void Clear(string key) => _items.TryRemove(key, out _); + + /// + public virtual void ClearOfType(Type type) => + _items.RemoveAll(kvp => kvp.Value != null && kvp.Value.GetType() == type); + + /// + public virtual void ClearOfType() + { + Type typeOfT = typeof(T); + ClearOfType(typeOfT); + } + + /// + public virtual void ClearOfType(Func predicate) + { + Type typeOfT = typeof(T); + _items.RemoveAll(kvp => + kvp.Value != null && kvp.Value.GetType() == typeOfT && predicate(kvp.Key, (T)kvp.Value)); + } + + /// + public virtual void ClearByKey(string keyStartsWith) => + _items.RemoveAll(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)); + + /// + public virtual void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + _items.RemoveAll(kvp => compiled.IsMatch(kvp.Key)); + } + + public IEnumerator> GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs b/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs index dbe84b114e..c10640986c 100644 --- a/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/DictionaryCacheRefresher.cs @@ -1,40 +1,31 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class DictionaryCacheRefresher : CacheRefresherBase { - public sealed class DictionaryCacheRefresher : CacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("D1D7E227-F817-4816-BFE9-6C39B6152884"); + + public DictionaryCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public DictionaryCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator , factory) - { } + } - #region Define + public override Guid RefresherUniqueId => UniqueId; - public static readonly Guid UniqueId = Guid.Parse("D1D7E227-F817-4816-BFE9-6C39B6152884"); + public override string Name => "Dictionary Cache Refresher"; - public override Guid RefresherUniqueId => UniqueId; + public override void Refresh(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(id); + } - public override string Name => "Dictionary Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } - - public override void Remove(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Remove(id); - } - - #endregion + public override void Remove(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Remove(id); } } diff --git a/src/Umbraco.Core/Cache/DistributedCache.cs b/src/Umbraco.Core/Cache/DistributedCache.cs index 95c17b946d..0adb0ea370 100644 --- a/src/Umbraco.Core/Cache/DistributedCache.cs +++ b/src/Umbraco.Core/Cache/DistributedCache.cs @@ -1,176 +1,192 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents the entry point into Umbraco's distributed cache infrastructure. +/// +/// +/// +/// The distributed cache infrastructure ensures that distributed caches are +/// invalidated properly in load balancing environments. +/// +/// +/// Distribute caches include static (in-memory) cache, runtime cache, front-end content cache, Examine/Lucene +/// indexes +/// +/// +public sealed class DistributedCache { - /// - /// Represents the entry point into Umbraco's distributed cache infrastructure. - /// - /// - /// - /// The distributed cache infrastructure ensures that distributed caches are - /// invalidated properly in load balancing environments. - /// - /// - /// Distribute caches include static (in-memory) cache, runtime cache, front-end content cache, Examine/Lucene indexes - /// - /// - public sealed class DistributedCache + private readonly CacheRefresherCollection _cacheRefreshers; + private readonly IServerMessenger _serverMessenger; + + public DistributedCache(IServerMessenger serverMessenger, CacheRefresherCollection cacheRefreshers) { - private readonly IServerMessenger _serverMessenger; - private readonly CacheRefresherCollection _cacheRefreshers; - - public DistributedCache(IServerMessenger serverMessenger, CacheRefresherCollection cacheRefreshers) - { - _serverMessenger = serverMessenger; - _cacheRefreshers = cacheRefreshers; - } - - #region Core notification methods - - /// - /// Notifies the distributed cache of specified item invalidation, for a specified . - /// - /// The type of the invalidated items. - /// The unique identifier of the ICacheRefresher. - /// A function returning the unique identifier of items. - /// The invalidated items. - /// - /// This method is much better for performance because it does not need to re-lookup object instances. - /// - public void Refresh(Guid refresherGuid, Func getNumericId, params T[] instances) - { - if (refresherGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - getNumericId, - instances); - } - - /// - /// Notifies the distributed cache of a specified item invalidation, for a specified . - /// - /// The unique identifier of the ICacheRefresher. - /// The unique identifier of the invalidated item. - public void Refresh(Guid refresherGuid, int id) - { - if (refresherGuid == Guid.Empty || id == default(int)) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - id); - } - - /// - /// Notifies the distributed cache of a specified item invalidation, for a specified . - /// - /// The unique identifier of the ICacheRefresher. - /// The unique identifier of the invalidated item. - public void Refresh(Guid refresherGuid, Guid id) - { - if (refresherGuid == Guid.Empty || id == Guid.Empty) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - id); - } - - // payload should be an object, or array of objects, NOT a - // Linq enumerable of some sort (IEnumerable, query...) - public void RefreshByPayload(Guid refresherGuid, TPayload[] payload) - { - if (refresherGuid == Guid.Empty || payload == null) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - payload); - } - - // so deal with IEnumerable - public void RefreshByPayload(Guid refresherGuid, IEnumerable payloads) - where TPayload : class - { - if (refresherGuid == Guid.Empty || payloads == null) return; - - _serverMessenger.QueueRefresh( - GetRefresherById(refresherGuid), - payloads.ToArray()); - } - - ///// - ///// Notifies the distributed cache, for a specified . - ///// - ///// The unique identifier of the ICacheRefresher. - ///// The notification content. - //internal void Notify(Guid refresherId, object payload) - //{ - // if (refresherId == Guid.Empty || payload == null) return; - - // _serverMessenger.Notify( - // Current.ServerRegistrar.Registrations, - // GetRefresherById(refresherId), - // json); - //} - - /// - /// Notifies the distributed cache of a global invalidation for a specified . - /// - /// The unique identifier of the ICacheRefresher. - public void RefreshAll(Guid refresherGuid) - { - if (refresherGuid == Guid.Empty) return; - - _serverMessenger.QueueRefreshAll( - GetRefresherById(refresherGuid)); - } - - /// - /// Notifies the distributed cache of a specified item removal, for a specified . - /// - /// The unique identifier of the ICacheRefresher. - /// The unique identifier of the removed item. - public void Remove(Guid refresherGuid, int id) - { - if (refresherGuid == Guid.Empty || id == default(int)) return; - - _serverMessenger.QueueRemove( - GetRefresherById(refresherGuid), - id); - } - - /// - /// Notifies the distributed cache of specified item removal, for a specified . - /// - /// The type of the removed items. - /// The unique identifier of the ICacheRefresher. - /// A function returning the unique identifier of items. - /// The removed items. - /// - /// This method is much better for performance because it does not need to re-lookup object instances. - /// - public void Remove(Guid refresherGuid, Func getNumericId, params T[] instances) - { - _serverMessenger.QueueRemove( - GetRefresherById(refresherGuid), - getNumericId, - instances); - } - - #endregion - - // helper method to get an ICacheRefresher by its unique identifier - private ICacheRefresher GetRefresherById(Guid refresherGuid) - { - ICacheRefresher? refresher = _cacheRefreshers[refresherGuid]; - if (refresher == null) - { - throw new InvalidOperationException($"No cache refresher found with id {refresherGuid}"); - } - - return refresher; - } + _serverMessenger = serverMessenger; + _cacheRefreshers = cacheRefreshers; } + + #region Core notification methods + + /// + /// Notifies the distributed cache of specified item invalidation, for a specified . + /// + /// The type of the invalidated items. + /// The unique identifier of the ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. + /// + /// This method is much better for performance because it does not need to re-lookup object instances. + /// + public void Refresh(Guid refresherGuid, Func getNumericId, params T[] instances) + { + if (refresherGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + getNumericId, + instances); + } + + // helper method to get an ICacheRefresher by its unique identifier + private ICacheRefresher GetRefresherById(Guid refresherGuid) + { + ICacheRefresher? refresher = _cacheRefreshers[refresherGuid]; + if (refresher == null) + { + throw new InvalidOperationException($"No cache refresher found with id {refresherGuid}"); + } + + return refresher; + } + + /// + /// Notifies the distributed cache of a specified item invalidation, for a specified . + /// + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the invalidated item. + public void Refresh(Guid refresherGuid, int id) + { + if (refresherGuid == Guid.Empty || id == default) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + id); + } + + /// + /// Notifies the distributed cache of a specified item invalidation, for a specified . + /// + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the invalidated item. + public void Refresh(Guid refresherGuid, Guid id) + { + if (refresherGuid == Guid.Empty || id == Guid.Empty) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + id); + } + + // payload should be an object, or array of objects, NOT a + // Linq enumerable of some sort (IEnumerable, query...) + public void RefreshByPayload(Guid refresherGuid, TPayload[] payload) + { + if (refresherGuid == Guid.Empty || payload == null) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + payload); + } + + // so deal with IEnumerable + public void RefreshByPayload(Guid refresherGuid, IEnumerable payloads) + where TPayload : class + { + if (refresherGuid == Guid.Empty || payloads == null) + { + return; + } + + _serverMessenger.QueueRefresh( + GetRefresherById(refresherGuid), + payloads.ToArray()); + } + + ///// + ///// Notifies the distributed cache, for a specified . + ///// + ///// The unique identifier of the ICacheRefresher. + ///// The notification content. + // internal void Notify(Guid refresherId, object payload) + // { + // if (refresherId == Guid.Empty || payload == null) return; + + // _serverMessenger.Notify( + // Current.ServerRegistrar.Registrations, + // GetRefresherById(refresherId), + // json); + // } + + /// + /// Notifies the distributed cache of a global invalidation for a specified . + /// + /// The unique identifier of the ICacheRefresher. + public void RefreshAll(Guid refresherGuid) + { + if (refresherGuid == Guid.Empty) + { + return; + } + + _serverMessenger.QueueRefreshAll( + GetRefresherById(refresherGuid)); + } + + /// + /// Notifies the distributed cache of a specified item removal, for a specified . + /// + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the removed item. + public void Remove(Guid refresherGuid, int id) + { + if (refresherGuid == Guid.Empty || id == default) + { + return; + } + + _serverMessenger.QueueRemove( + GetRefresherById(refresherGuid), + id); + } + + /// + /// Notifies the distributed cache of specified item removal, for a specified . + /// + /// The type of the removed items. + /// The unique identifier of the ICacheRefresher. + /// A function returning the unique identifier of items. + /// The removed items. + /// + /// This method is much better for performance because it does not need to re-lookup object instances. + /// + public void Remove(Guid refresherGuid, Func getNumericId, params T[] instances) => + _serverMessenger.QueueRemove( + GetRefresherById(refresherGuid), + getNumericId, + instances); + + #endregion } diff --git a/src/Umbraco.Core/Cache/DomainCacheRefresher.cs b/src/Umbraco.Core/Cache/DomainCacheRefresher.cs index 28e62c854d..a6e46ee2e4 100644 --- a/src/Umbraco.Core/Cache/DomainCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/DomainCacheRefresher.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -6,78 +5,74 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class DomainCacheRefresher : PayloadCacheRefresherBase { - public sealed class DomainCacheRefresher : PayloadCacheRefresherBase + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public DomainCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) => + _publishedSnapshotService = publishedSnapshotService; + + #region Json + + public class JsonPayload { - private readonly IPublishedSnapshotService _publishedSnapshotService; - - public DomainCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) + public JsonPayload(int id, DomainChangeTypes changeType) { - _publishedSnapshotService = publishedSnapshotService; + Id = id; + ChangeType = changeType; } - #region Define - - public static readonly Guid UniqueId = Guid.Parse("11290A79-4B57-4C99-AD72-7748A3CF38AF"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Domain Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - ClearAllIsolatedCacheByEntityType(); - - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - - // notify - _publishedSnapshotService.Notify(payloads); - // then trigger event - base.Refresh(payloads); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() => throw new NotSupportedException(); - - public override void Refresh(int id) => throw new NotSupportedException(); - - public override void Refresh(Guid id) => throw new NotSupportedException(); - - public override void Remove(int id) => throw new NotSupportedException(); - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, DomainChangeTypes changeType) - { - Id = id; - ChangeType = changeType; - } - - public int Id { get; } - - public DomainChangeTypes ChangeType { get; } - } - - #endregion + public int Id { get; } + public DomainChangeTypes ChangeType { get; } } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("11290A79-4B57-4C99-AD72-7748A3CF38AF"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Domain Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + ClearAllIsolatedCacheByEntityType(); + + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + + // notify + _publishedSnapshotService.Notify(payloads); + + // then trigger event + base.Refresh(payloads); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs index 6c3b8855d2..6476c76f96 100644 --- a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs +++ b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs @@ -1,166 +1,172 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements a fast on top of a concurrent dictionary. +/// +public class FastDictionaryAppCache : IAppCache { /// - /// Implements a fast on top of a concurrent dictionary. + /// Gets the internal items dictionary, for tests only! /// - public class FastDictionaryAppCache : IAppCache + private readonly ConcurrentDictionary> _items = new(); + + public IEnumerable Keys => _items.Keys; + + public int Count => _items.Count; + + /// + public object? Get(string cacheKey) { + _items.TryGetValue(cacheKey, out Lazy? result); // else null + return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null + } - /// - /// Gets the internal items dictionary, for tests only! - /// - private readonly ConcurrentDictionary> _items = new ConcurrentDictionary>(); + /// + public object? Get(string cacheKey, Func getCacheItem) + { + Lazy? result = _items.GetOrAdd(cacheKey, k => SafeLazy.GetSafeLazy(getCacheItem)); - public IEnumerable Keys => _items.Keys; - - public int Count => _items.Count; - - /// - public object? Get(string cacheKey) + var value = result.Value; // will not throw (safe lazy) + if (!(value is SafeLazy.ExceptionHolder eh)) { - _items.TryGetValue(cacheKey, out var result); // else null - return result == null ? null : SafeLazy.GetSafeLazyValue(result!); // return exceptions as null + return value; } - /// - public object? Get(string cacheKey, Func getCacheItem) + // and... it's in the cache anyway - so contrary to other cache providers, + // which would trick with GetSafeLazyValue, we need to remove by ourselves, + // in order NOT to cache exceptions + _items.TryRemove(cacheKey, out result); + eh.Exception.Throw(); // throw once! + return null; // never reached + } + + /// + public IEnumerable SearchByKey(string keyStartsWith) => + _items + .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)) + .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) + .Where(x => x != null); + + /// + public IEnumerable SearchByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + return _items + .Where(kvp => compiled.IsMatch(kvp.Key)) + .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) + .Where(x => x != null); + } + + /// + public void Clear() => _items.Clear(); + + /// + public void Clear(string key) => _items.TryRemove(key, out _); + + /// + public void ClearOfType(Type? type) + { + if (type == null) { - var result = _items.GetOrAdd(cacheKey, k => SafeLazy.GetSafeLazy(getCacheItem)); - - var value = result.Value; // will not throw (safe lazy) - if (!(value is SafeLazy.ExceptionHolder eh)) - return value; - - // and... it's in the cache anyway - so contrary to other cache providers, - // which would trick with GetSafeLazyValue, we need to remove by ourselves, - // in order NOT to cache exceptions - - _items.TryRemove(cacheKey, out result); - eh.Exception.Throw(); // throw once! - return null; // never reached + return; } - /// - public IEnumerable SearchByKey(string keyStartsWith) + var isInterface = type.IsInterface; + + foreach (KeyValuePair> kvp in _items + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue(x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? type.IsInstanceOfType(value) : value.GetType() == type); + })) { - return _items - .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)) - .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) - .Where(x => x != null); + _items.TryRemove(kvp.Key, out _); } + } - /// - public IEnumerable SearchByRegex(string regex) + /// + public void ClearOfType() + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + + foreach (KeyValuePair> kvp in _items + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue(x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? value is T : value.GetType() == typeOfT); + })) { - var compiled = new Regex(regex, RegexOptions.Compiled); - return _items - .Where(kvp => compiled.IsMatch(kvp.Key)) - .Select(kvp => SafeLazy.GetSafeLazyValue(kvp.Value)) - .Where(x => x != null); + _items.TryRemove(kvp.Key, out _); } + } - /// - public void Clear() + /// + public void ClearOfType(Func predicate) + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + + foreach (KeyValuePair> kvp in _items + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue(x.Value, true); + if (value == null) + { + return true; + } + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? value is T : value.GetType() == typeOfT) + + // run predicate on the 'public key' part only, ie without prefix + && predicate(x.Key, (T)value); + })) { - _items.Clear(); + _items.TryRemove(kvp.Key, out _); } + } - /// - public void Clear(string key) + /// + public void ClearByKey(string keyStartsWith) + { + foreach (KeyValuePair> ikvp in _items + .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith))) { - _items.TryRemove(key, out _); + _items.TryRemove(ikvp.Key, out _); } + } - /// - public void ClearOfType(Type type) + /// + public void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + foreach (KeyValuePair> ikvp in _items + .Where(kvp => compiled.IsMatch(kvp.Key))) { - if (type == null) return; - var isInterface = type.IsInterface; - - foreach (var kvp in _items - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue(x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); - })) - _items.TryRemove(kvp.Key, out _); - } - - /// - public void ClearOfType() - { - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - - foreach (var kvp in _items - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // compare on exact type, don't use "is" - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue(x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); - })) - _items.TryRemove(kvp.Key, out _); - } - - /// - public void ClearOfType(Func predicate) - { - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - - foreach (var kvp in _items - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // compare on exact type, don't use "is" - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue(x.Value, true); - if (value == null) return true; - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return (isInterface ? (value is T) : (value.GetType() == typeOfT)) - // run predicate on the 'public key' part only, ie without prefix - && predicate(x.Key, (T)value); - })) - _items.TryRemove(kvp.Key, out _); - } - - /// - public void ClearByKey(string keyStartsWith) - { - foreach (var ikvp in _items - .Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith))) - _items.TryRemove(ikvp.Key, out _); - } - - /// - public void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - foreach (var ikvp in _items - .Where(kvp => compiled.IsMatch(kvp.Key))) - _items.TryRemove(ikvp.Key, out _); + _items.TryRemove(ikvp.Key, out _); } } } diff --git a/src/Umbraco.Core/Cache/FastDictionaryAppCacheBase.cs b/src/Umbraco.Core/Cache/FastDictionaryAppCacheBase.cs index e0bbd57397..967d5aa5a7 100644 --- a/src/Umbraco.Core/Cache/FastDictionaryAppCacheBase.cs +++ b/src/Umbraco.Core/Cache/FastDictionaryAppCacheBase.cs @@ -1,281 +1,290 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Provides a base class to fast, dictionary-based implementations. +/// +public abstract class FastDictionaryAppCacheBase : IAppCache { - /// - /// Provides a base class to fast, dictionary-based implementations. - /// - public abstract class FastDictionaryAppCacheBase : IAppCache + // prefix cache keys so we know which one are ours + protected const string CacheItemPrefix = "umbrtmche"; + + #region IAppCache + + /// + public virtual object? Get(string key) { - // prefix cache keys so we know which one are ours - protected const string CacheItemPrefix = "umbrtmche"; - - #region IAppCache - - /// - public virtual object? Get(string key) + key = GetCacheKey(key); + Lazy? result; + try { - key = GetCacheKey(key); - Lazy? result; - try - { - EnterReadLock(); - result = GetEntry(key) as Lazy; // null if key not found - } - finally - { - ExitReadLock(); - } - return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null + EnterReadLock(); + result = GetEntry(key) as Lazy; // null if key not found + } + finally + { + ExitReadLock(); } - /// - public abstract object? Get(string key, Func factory); - - /// - public virtual IEnumerable SearchByKey(string keyStartsWith) - { - var plen = CacheItemPrefix.Length + 1; - IEnumerable> entries; - try - { - EnterReadLock(); - entries = GetDictionaryEntries() - .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) - .ToArray(); // evaluate while locked - } - finally - { - ExitReadLock(); - } - - return entries - .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null - .Where(x => x != null)!; // backward compat, don't store null values in the cache - } - - /// - public virtual IEnumerable SearchByRegex(string regex) - { - const string prefix = CacheItemPrefix + "-"; - var compiled = new Regex(regex, RegexOptions.Compiled); - var plen = prefix.Length; - IEnumerable> entries; - try - { - EnterReadLock(); - entries = GetDictionaryEntries() - .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) - .ToArray(); // evaluate while locked - } - finally - { - ExitReadLock(); - } - return entries - .Select(x => SafeLazy.GetSafeLazyValue( (Lazy)x.Value)) // return exceptions as null - .Where(x => x != null); // backward compatible, don't store null values in the cache - } - - /// - public virtual void Clear() - { - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries().ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void Clear(string key) - { - var cacheKey = GetCacheKey(key); - try - { - EnterWriteLock(); - RemoveEntry(cacheKey); - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType(Type type) - { - if (type == null) return; - var isInterface = type.IsInterface; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy) x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); - }) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType() - { - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // compare on exact type, don't use "is" - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy) x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); - }) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType(Func predicate) - { - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - var plen = CacheItemPrefix.Length + 1; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => - { - // entry.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // compare on exact type, don't use "is" - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy) x.Value, true); - if (value == null) return true; - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return (isInterface ? (value is T) : (value.GetType() == typeOfT)) - // run predicate on the 'public key' part only, ie without prefix - && predicate(((string) x.Key).Substring(plen), (T) value); - })) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearByKey(string keyStartsWith) - { - var plen = CacheItemPrefix.Length + 1; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - /// - public virtual void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - var plen = CacheItemPrefix.Length + 1; - try - { - EnterWriteLock(); - foreach (var entry in GetDictionaryEntries() - .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) - .ToArray()) - { - RemoveEntry((string) entry.Key); - } - } - finally - { - ExitWriteLock(); - } - } - - #endregion - - #region Dictionary - - // manipulate the underlying cache entries - // these *must* be called from within the appropriate locks - // and use the full prefixed cache keys - protected abstract IEnumerable> GetDictionaryEntries(); - protected abstract void RemoveEntry(string key); - protected abstract object? GetEntry(string key); - - // read-write lock the underlying cache - //protected abstract IDisposable ReadLock { get; } - //protected abstract IDisposable WriteLock { get; } - - protected abstract void EnterReadLock(); - protected abstract void ExitReadLock(); - protected abstract void EnterWriteLock(); - protected abstract void ExitWriteLock(); - - protected string GetCacheKey(string key) => $"{CacheItemPrefix}-{key}"; - - - - #endregion + return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null } + + /// + public abstract object? Get(string key, Func factory); + + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) + { + var plen = CacheItemPrefix.Length + 1; + IEnumerable> entries; + try + { + EnterReadLock(); + entries = GetDictionaryEntries() + .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) + .ToArray(); // evaluate while locked + } + finally + { + ExitReadLock(); + } + + return entries + .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null)!; // backward compat, don't store null values in the cache + } + + /// + public virtual IEnumerable SearchByRegex(string regex) + { + const string prefix = CacheItemPrefix + "-"; + var compiled = new Regex(regex, RegexOptions.Compiled); + var plen = prefix.Length; + IEnumerable> entries; + try + { + EnterReadLock(); + entries = GetDictionaryEntries() + .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) + .ToArray(); // evaluate while locked + } + finally + { + ExitReadLock(); + } + + return entries + .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null); // backward compatible, don't store null values in the cache + } + + /// + public virtual void Clear() + { + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries().ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void Clear(string key) + { + var cacheKey = GetCacheKey(key); + try + { + EnterWriteLock(); + RemoveEntry(cacheKey); + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearOfType(Type? type) + { + if (type == null) + { + return; + } + + var isInterface = type.IsInterface; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || + (isInterface ? type.IsInstanceOfType(value) : value.GetType() == type); + }) + .ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearOfType() + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? value is T : value.GetType() == typeOfT); + }) + .ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearOfType(Func predicate) + { + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + var plen = CacheItemPrefix.Length + 1; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + if (value == null) + { + return true; + } + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? value is T : value.GetType() == typeOfT) + + // run predicate on the 'public key' part only, ie without prefix + && predicate(((string) x.Key).Substring(plen), (T) value); + })) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearByKey(string keyStartsWith) + { + var plen = CacheItemPrefix.Length + 1; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) + .ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + /// + public virtual void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + var plen = CacheItemPrefix.Length + 1; + try + { + EnterWriteLock(); + foreach (KeyValuePair entry in GetDictionaryEntries() + .Where(x => compiled.IsMatch(((string)x.Key).Substring(plen))) + .ToArray()) + { + RemoveEntry((string)entry.Key); + } + } + finally + { + ExitWriteLock(); + } + } + + #endregion + + #region Dictionary + + // manipulate the underlying cache entries + // these *must* be called from within the appropriate locks + // and use the full prefixed cache keys + protected abstract IEnumerable> GetDictionaryEntries(); + + protected abstract void RemoveEntry(string key); + + protected abstract object? GetEntry(string key); + + // read-write lock the underlying cache + // protected abstract IDisposable ReadLock { get; } + // protected abstract IDisposable WriteLock { get; } + protected abstract void EnterReadLock(); + + protected abstract void ExitReadLock(); + + protected abstract void EnterWriteLock(); + + protected abstract void ExitWriteLock(); + + protected string GetCacheKey(string key) => $"{CacheItemPrefix}-{key}"; + + #endregion } diff --git a/src/Umbraco.Core/Cache/IAppCache.cs b/src/Umbraco.Core/Cache/IAppCache.cs index 81cfc2e114..187ff6fc11 100644 --- a/src/Umbraco.Core/Cache/IAppCache.cs +++ b/src/Umbraco.Core/Cache/IAppCache.cs @@ -1,94 +1,96 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +/// +/// Defines an application cache. +/// +public interface IAppCache { /// - /// Defines an application cache. + /// Gets an item identified by its key. /// - public interface IAppCache - { - /// - /// Gets an item identified by its key. - /// - /// The key of the item. - /// The item, or null if the item was not found. - object? Get(string key); + /// The key of the item. + /// The item, or null if the item was not found. + object? Get(string key); - /// - /// Gets or creates an item identified by its key. - /// - /// The key of the item. - /// A factory function that can create the item. - /// The item. - object? Get(string key, Func factory); + /// + /// Gets or creates an item identified by its key. + /// + /// The key of the item. + /// A factory function that can create the item. + /// The item. + object? Get(string key, Func factory); - /// - /// Gets items with a key starting with the specified value. - /// - /// The StartsWith value to use in the search. - /// Items matching the search. - IEnumerable SearchByKey(string keyStartsWith); + /// + /// Gets items with a key starting with the specified value. + /// + /// The StartsWith value to use in the search. + /// Items matching the search. + IEnumerable SearchByKey(string keyStartsWith); - /// - /// Gets items with a key matching a regular expression. - /// - /// The regular expression. - /// Items matching the search. - IEnumerable SearchByRegex(string regex); + /// + /// Gets items with a key matching a regular expression. + /// + /// The regular expression. + /// Items matching the search. + IEnumerable SearchByRegex(string regex); - /// - /// Removes all items from the cache. - /// - void Clear(); + /// + /// Removes all items from the cache. + /// + void Clear(); - /// - /// Removes an item identified by its key from the cache. - /// - /// The key of the item. - void Clear(string key); + /// + /// Removes an item identified by its key from the cache. + /// + /// The key of the item. + void Clear(string key); - /// - /// Removes items of a specified type from the cache. - /// - /// The type to remove. - /// - /// If the type is an interface, then all items of a type implementing that interface are - /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from - /// the specified type are not removed). - /// Performs a case-sensitive search. - /// - void ClearOfType(Type type); + /// + /// Removes items of a specified type from the cache. + /// + /// The type to remove. + /// + /// + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). + /// + /// Performs a case-sensitive search. + /// + void ClearOfType(Type type); - /// - /// Removes items of a specified type from the cache. - /// - /// The type of the items to remove. - /// If the type is an interface, then all items of a type implementing that interface are - /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from - /// the specified type are not removed). - void ClearOfType(); + /// + /// Removes items of a specified type from the cache. + /// + /// The type of the items to remove. + /// + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). + /// + void ClearOfType(); - /// - /// Removes items of a specified type from the cache. - /// - /// The type of the items to remove. - /// The predicate to satisfy. - /// If the type is an interface, then all items of a type implementing that interface are - /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from - /// the specified type are not removed). - void ClearOfType(Func predicate); + /// + /// Removes items of a specified type from the cache. + /// + /// The type of the items to remove. + /// The predicate to satisfy. + /// + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). + /// + void ClearOfType(Func predicate); - /// - /// Clears items with a key starting with the specified value. - /// - /// The StartsWith value to use in the search. - void ClearByKey(string keyStartsWith); + /// + /// Clears items with a key starting with the specified value. + /// + /// The StartsWith value to use in the search. + void ClearByKey(string keyStartsWith); - /// - /// Clears items with a key matching a regular expression. - /// - /// The regular expression. - void ClearByRegex(string regex); - } + /// + /// Clears items with a key matching a regular expression. + /// + /// The regular expression. + void ClearByRegex(string regex); } diff --git a/src/Umbraco.Core/Cache/IAppPolicyCache.cs b/src/Umbraco.Core/Cache/IAppPolicyCache.cs index ec59bf390b..1d0044c057 100644 --- a/src/Umbraco.Core/Cache/IAppPolicyCache.cs +++ b/src/Umbraco.Core/Cache/IAppPolicyCache.cs @@ -1,43 +1,42 @@ -using System; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +/// +/// Defines an application cache that support cache policies. +/// +/// +/// A cache policy can be used to cache with timeouts, +/// or depending on files, and with a remove callback, etc. +/// +public interface IAppPolicyCache : IAppCache { /// - /// Defines an application cache that support cache policies. + /// Gets an item identified by its key. /// - /// A cache policy can be used to cache with timeouts, - /// or depending on files, and with a remove callback, etc. - public interface IAppPolicyCache : IAppCache - { - /// - /// Gets an item identified by its key. - /// - /// The key of the item. - /// A factory function that can create the item. - /// An optional cache timeout. - /// An optional value indicating whether the cache timeout is sliding (default is false). - /// Files the cache entry depends on. - /// The item. - object? Get( - string key, - Func factory, - TimeSpan? timeout, - bool isSliding = false, - string[]? dependentFiles = null); + /// The key of the item. + /// A factory function that can create the item. + /// An optional cache timeout. + /// An optional value indicating whether the cache timeout is sliding (default is false). + /// Files the cache entry depends on. + /// The item. + object? Get( + string key, + Func factory, + TimeSpan? timeout, + bool isSliding = false, + string[]? dependentFiles = null); - /// - /// Inserts an item. - /// - /// The key of the item. - /// A factory function that can create the item. - /// An optional cache timeout. - /// An optional value indicating whether the cache timeout is sliding (default is false). - /// Files the cache entry depends on. - void Insert( - string key, - Func factory, - TimeSpan? timeout = null, - bool isSliding = false, - string[]? dependentFiles = null); - } + /// + /// Inserts an item. + /// + /// The key of the item. + /// A factory function that can create the item. + /// An optional cache timeout. + /// An optional value indicating whether the cache timeout is sliding (default is false). + /// Files the cache entry depends on. + void Insert( + string key, + Func factory, + TimeSpan? timeout = null, + bool isSliding = false, + string[]? dependentFiles = null); } diff --git a/src/Umbraco.Core/Cache/ICacheRefresher.cs b/src/Umbraco.Core/Cache/ICacheRefresher.cs index 97a3bf08eb..dba0cd3b3f 100644 --- a/src/Umbraco.Core/Cache/ICacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ICacheRefresher.cs @@ -1,33 +1,37 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Cache -{ - /// - /// The IcacheRefresher Interface is used for load balancing. - /// - /// - public interface ICacheRefresher : IDiscoverable - { - Guid RefresherUniqueId { get; } - string Name { get; } - void RefreshAll(); - void Refresh(int id); - void Remove(int id); - void Refresh(Guid id); - } +namespace Umbraco.Cms.Core.Cache; - /// - /// Strongly type cache refresher that is able to refresh cache of real instances of objects as well as IDs - /// - /// - /// - /// This is much better for performance when we're not running in a load balanced environment so we can refresh the cache - /// against a already resolved object instead of looking the object back up by id. - /// - public interface ICacheRefresher : ICacheRefresher - { - void Refresh(T instance); - void Remove(T instance); - } +/// +/// The IcacheRefresher Interface is used for load balancing. +/// +public interface ICacheRefresher : IDiscoverable +{ + Guid RefresherUniqueId { get; } + + string Name { get; } + + void RefreshAll(); + + void Refresh(int id); + + void Remove(int id); + + void Refresh(Guid id); +} + +/// +/// Strongly type cache refresher that is able to refresh cache of real instances of objects as well as IDs +/// +/// +/// +/// This is much better for performance when we're not running in a load balanced environment so we can refresh the +/// cache +/// against a already resolved object instead of looking the object back up by id. +/// +public interface ICacheRefresher : ICacheRefresher +{ + void Refresh(T instance); + + void Remove(T instance); } diff --git a/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs b/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs index 04b91e43d8..35eb7a279c 100644 --- a/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs +++ b/src/Umbraco.Core/Cache/ICacheRefresherNotificationFactory.cs @@ -1,17 +1,17 @@ -using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Factory for creating cache refresher notification instances +/// +public interface ICacheRefresherNotificationFactory { /// - /// Factory for creating cache refresher notification instances + /// Creates a /// - public interface ICacheRefresherNotificationFactory - { - /// - /// Creates a - /// - /// The to create - TNotification Create(object msgObject, MessageType type) where TNotification : CacheRefresherNotification; - } + /// The to create + TNotification Create(object msgObject, MessageType type) + where TNotification : CacheRefresherNotification; } diff --git a/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs b/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs index 619fc1eb56..d01bf617fd 100644 --- a/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/IJsonCacheRefresher.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A cache refresher that supports refreshing or removing cache based on a custom Json payload +/// +public interface IJsonCacheRefresher : ICacheRefresher { /// - /// A cache refresher that supports refreshing or removing cache based on a custom Json payload + /// Refreshes, clears, etc... any cache based on the information provided in the json /// - public interface IJsonCacheRefresher : ICacheRefresher - { - /// - /// Refreshes, clears, etc... any cache based on the information provided in the json - /// - /// - void Refresh(string json); - } + /// + void Refresh(string json); } diff --git a/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs b/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs index 21dfdd840d..426481ea0a 100644 --- a/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/IPayloadCacheRefresher.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A cache refresher that supports refreshing cache based on a custom payload +/// +public interface IPayloadCacheRefresher : IJsonCacheRefresher { /// - /// A cache refresher that supports refreshing cache based on a custom payload + /// Refreshes, clears, etc... any cache based on the information provided in the payload /// - public interface IPayloadCacheRefresher : IJsonCacheRefresher - { - /// - /// Refreshes, clears, etc... any cache based on the information provided in the payload - /// - /// - void Refresh(TPayload[] payloads); - } + /// + void Refresh(TPayload[] payloads); } diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs index af44f2c085..4352f9be31 100644 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -1,76 +1,73 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public interface IRepositoryCachePolicy + where TEntity : class, IEntity { - public interface IRepositoryCachePolicy - where TEntity : class, IEntity - { - /// - /// Gets an entity from the cache, else from the repository. - /// - /// The identifier. - /// The repository PerformGet method. - /// The repository PerformGetAll method. - /// The entity with the specified identifier, if it exits, else null. - /// First considers the cache then the repository. - TEntity? Get(TId? id, Func performGet, Func?> performGetAll); + /// + /// Gets an entity from the cache, else from the repository. + /// + /// The identifier. + /// The repository PerformGet method. + /// The repository PerformGetAll method. + /// The entity with the specified identifier, if it exits, else null. + /// First considers the cache then the repository. + TEntity? Get(TId? id, Func performGet, Func?> performGetAll); - /// - /// Gets an entity from the cache. - /// - /// The identifier. - /// The entity with the specified identifier, if it is in the cache already, else null. - /// Does not consider the repository at all. - TEntity? GetCached(TId id); + /// + /// Gets an entity from the cache. + /// + /// The identifier. + /// The entity with the specified identifier, if it is in the cache already, else null. + /// Does not consider the repository at all. + TEntity? GetCached(TId id); - /// - /// Gets a value indicating whether an entity with a specified identifier exists. - /// - /// The identifier. - /// The repository PerformExists method. - /// The repository PerformGetAll method. - /// A value indicating whether an entity with the specified identifier exists. - /// First considers the cache then the repository. - bool Exists(TId id, Func performExists, Func?> performGetAll); + /// + /// Gets a value indicating whether an entity with a specified identifier exists. + /// + /// The identifier. + /// The repository PerformExists method. + /// The repository PerformGetAll method. + /// A value indicating whether an entity with the specified identifier exists. + /// First considers the cache then the repository. + bool Exists(TId id, Func performExists, Func?> performGetAll); - /// - /// Creates an entity. - /// - /// The entity. - /// The repository PersistNewItem method. - /// Creates the entity in the repository, and updates the cache accordingly. - void Create(TEntity entity, Action persistNew); + /// + /// Creates an entity. + /// + /// The entity. + /// The repository PersistNewItem method. + /// Creates the entity in the repository, and updates the cache accordingly. + void Create(TEntity entity, Action persistNew); - /// - /// Updates an entity. - /// - /// The entity. - /// The repository PersistUpdatedItem method. - /// Updates the entity in the repository, and updates the cache accordingly. - void Update(TEntity entity, Action persistUpdated); + /// + /// Updates an entity. + /// + /// The entity. + /// The repository PersistUpdatedItem method. + /// Updates the entity in the repository, and updates the cache accordingly. + void Update(TEntity entity, Action persistUpdated); - /// - /// Removes an entity. - /// - /// The entity. - /// The repository PersistDeletedItem method. - /// Removes the entity from the repository and clears the cache. - void Delete(TEntity entity, Action persistDeleted); + /// + /// Removes an entity. + /// + /// The entity. + /// The repository PersistDeletedItem method. + /// Removes the entity from the repository and clears the cache. + void Delete(TEntity entity, Action persistDeleted); - /// - /// Gets entities. - /// - /// The identifiers. - /// The repository PerformGetAll method. - /// If is empty, all entities, else the entities with the specified identifiers. - /// Get all the entities. Either from the cache or the repository depending on the implementation. - TEntity[] GetAll(TId[]? ids, Func> performGetAll); + /// + /// Gets entities. + /// + /// The identifiers. + /// The repository PerformGetAll method. + /// If is empty, all entities, else the entities with the specified identifiers. + /// Get all the entities. Either from the cache or the repository depending on the implementation. + TEntity[] GetAll(TId[]? ids, Func> performGetAll); - /// - /// Clears the entire cache. - /// - void ClearAll(); - } + /// + /// Clears the entire cache. + /// + void ClearAll(); } diff --git a/src/Umbraco.Core/Cache/IRequestCache.cs b/src/Umbraco.Core/Cache/IRequestCache.cs index 02f37e6ea9..f88bc3bb24 100644 --- a/src/Umbraco.Core/Cache/IRequestCache.cs +++ b/src/Umbraco.Core/Cache/IRequestCache.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +public interface IRequestCache : IAppCache, IEnumerable> { - public interface IRequestCache : IAppCache, IEnumerable> - { - bool Set(string key, object? value); - bool Remove(string key); + /// + /// Returns true if the request cache is available otherwise false + /// + bool IsAvailable { get; } - /// - /// Returns true if the request cache is available otherwise false - /// - bool IsAvailable { get; } - } + bool Set(string key, object? value); + + bool Remove(string key); } diff --git a/src/Umbraco.Core/Cache/IValueEditorCache.cs b/src/Umbraco.Core/Cache/IValueEditorCache.cs index f283d730b5..790907c750 100644 --- a/src/Umbraco.Core/Cache/IValueEditorCache.cs +++ b/src/Umbraco.Core/Cache/IValueEditorCache.cs @@ -1,12 +1,11 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public interface IValueEditorCache { - public interface IValueEditorCache - { - public IDataValueEditor GetValueEditor(IDataEditor dataEditor, IDataType dataType); - public void ClearCache(IEnumerable dataTypeIds); - } + public IDataValueEditor GetValueEditor(IDataEditor dataEditor, IDataType dataType); + + public void ClearCache(IEnumerable dataTypeIds); } diff --git a/src/Umbraco.Core/Cache/IsolatedCaches.cs b/src/Umbraco.Core/Cache/IsolatedCaches.cs index 7c273c9136..31dc6fe095 100644 --- a/src/Umbraco.Core/Cache/IsolatedCaches.cs +++ b/src/Umbraco.Core/Cache/IsolatedCaches.cs @@ -1,41 +1,41 @@ -using System; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +/// +/// Represents a dictionary of for types. +/// +/// +/// +/// Isolated caches are used by e.g. repositories, to ensure that each cached entity +/// type has its own cache, so that lookups are fast and the repository does not need to +/// search through all keys on a global scale. +/// +/// +public class IsolatedCaches : AppPolicedCacheDictionary { /// - /// Represents a dictionary of for types. + /// Initializes a new instance of the class. /// - /// - /// Isolated caches are used by e.g. repositories, to ensure that each cached entity - /// type has its own cache, so that lookups are fast and the repository does not need to - /// search through all keys on a global scale. - /// - public class IsolatedCaches : AppPolicedCacheDictionary + /// + public IsolatedCaches(Func cacheFactory) + : base(cacheFactory) { - /// - /// Initializes a new instance of the class. - /// - /// - public IsolatedCaches(Func cacheFactory) - : base(cacheFactory) - { } - - /// - /// Gets a cache. - /// - public IAppPolicyCache GetOrCreate() - => GetOrCreate(typeof(T)); - - /// - /// Tries to get a cache. - /// - public Attempt Get() - => Get(typeof(T)); - - /// - /// Clears a cache. - /// - public void ClearCache() - => ClearCache(typeof(T)); } + + /// + /// Gets a cache. + /// + public IAppPolicyCache GetOrCreate() + => GetOrCreate(typeof(T)); + + /// + /// Tries to get a cache. + /// + public Attempt Get() + => Get(typeof(T)); + + /// + /// Clears a cache. + /// + public void ClearCache() + => ClearCache(typeof(T)); } diff --git a/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs b/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs index a6b705ae5d..b22cff56d2 100644 --- a/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/JsonCacheRefresherBase.cs @@ -3,58 +3,46 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for "json" cache refreshers. +/// +/// The actual cache refresher type is used for strongly typed events. +public abstract class JsonCacheRefresherBase : CacheRefresherBase, + IJsonCacheRefresher + where TNotification : CacheRefresherNotification { /// - /// A base class for "json" cache refreshers. + /// Initializes a new instance of the . /// - /// The actual cache refresher type. - /// The actual cache refresher type is used for strongly typed events. - public abstract class JsonCacheRefresherBase : CacheRefresherBase, IJsonCacheRefresher - where TNotification : CacheRefresherNotification - { - protected IJsonSerializer JsonSerializer { get; } + protected JsonCacheRefresherBase( + AppCaches appCaches, + IJsonSerializer jsonSerializer, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) => + JsonSerializer = jsonSerializer; - /// - /// Initializes a new instance of the . - /// - /// A cache helper. - protected JsonCacheRefresherBase( - AppCaches appCaches, - IJsonSerializer jsonSerializer, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { - JsonSerializer = jsonSerializer; - } + protected IJsonSerializer JsonSerializer { get; } - /// - /// Refreshes as specified by a json payload. - /// - /// The json payload. - public virtual void Refresh(string json) - { - OnCacheUpdated(NotificationFactory.Create(json, MessageType.RefreshByJson)); - } + /// + /// Refreshes as specified by a json payload. + /// + /// The json payload. + public virtual void Refresh(string json) => + OnCacheUpdated(NotificationFactory.Create(json, MessageType.RefreshByJson)); - #region Json - /// - /// Deserializes a json payload into an object payload. - /// - /// The json payload. - /// The deserialized object payload. - public TJsonPayload[]? Deserialize(string json) - { - return JsonSerializer.Deserialize(json); - } + #region Json + /// + /// Deserializes a json payload into an object payload. + /// + /// The json payload. + /// The deserialized object payload. + public TJsonPayload[]? Deserialize(string json) => JsonSerializer.Deserialize(json); - public string Serialize(params TJsonPayload[] jsonPayloads) - { - return JsonSerializer.Serialize(jsonPayloads); - } - #endregion + public string Serialize(params TJsonPayload[] jsonPayloads) => JsonSerializer.Serialize(jsonPayloads); - } + #endregion } diff --git a/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs index 414c51c186..2ff447246b 100644 --- a/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/LanguageCacheRefresher.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -7,146 +6,152 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services.Changes; using static Umbraco.Cms.Core.Cache.LanguageCacheRefresher.JsonPayload; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase { - public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase + public LanguageCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) => + _publishedSnapshotService = publishedSnapshotService; + + /// + /// Clears all domain caches + /// + private void RefreshDomains() { - public LanguageCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IPublishedSnapshotService publishedSnapshotService, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) + ClearAllIsolatedCacheByEntityType(); + + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + DomainCacheRefresher.JsonPayload[] payloads = new[] { - _publishedSnapshotService = publishedSnapshotService; - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); - private readonly IPublishedSnapshotService _publishedSnapshotService; - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Language Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - if (payloads.Length == 0) return; - - var clearDictionary = false; - var clearContent = false; - - //clear all no matter what type of payload - ClearAllIsolatedCacheByEntityType(); - - foreach (var payload in payloads) - { - switch (payload.ChangeType) - { - case LanguageChangeType.Update: - clearDictionary = true; - break; - case LanguageChangeType.Remove: - case LanguageChangeType.ChangeCulture: - clearDictionary = true; - clearContent = true; - break; - } - } - - if (clearDictionary) - { - ClearAllIsolatedCacheByEntityType(); - } - - //if this flag is set, we will tell the published snapshot service to refresh ALL content and evict ALL IContent items - if (clearContent) - { - //clear all domain caches - RefreshDomains(); - ContentCacheRefresher.RefreshContentTypes(AppCaches); // we need to evict all IContent items - //now refresh all nucache - var clearContentPayload = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; - ContentCacheRefresher.NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, clearContentPayload); - } - - // then trigger event - base.Refresh(payloads); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() => throw new NotSupportedException(); - - public override void Refresh(int id) => throw new NotSupportedException(); - - public override void Refresh(Guid id) => throw new NotSupportedException(); - - public override void Remove(int id) => throw new NotSupportedException(); - - #endregion - - /// - /// Clears all domain caches - /// - private void RefreshDomains() - { - ClearAllIsolatedCacheByEntityType(); - - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - - var payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; - _publishedSnapshotService.Notify(payloads); - } - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, string isoCode, LanguageChangeType changeType) - { - Id = id; - IsoCode = isoCode; - ChangeType = changeType; - } - - public int Id { get; } - public string IsoCode { get; } - public LanguageChangeType ChangeType { get; } - - public enum LanguageChangeType - { - /// - /// A new languages has been added - /// - Add = 0, - - /// - /// A language has been deleted - /// - Remove = 1, - - /// - /// A language has been updated - but it's culture remains the same - /// - Update = 2, - - /// - /// A language has been updated - it's culture has changed - /// - ChangeCulture = 3 - } - } - - #endregion + new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll), + }; + _publishedSnapshotService.Notify(payloads); } + + #region Json + + public class JsonPayload + { + public enum LanguageChangeType + { + /// + /// A new languages has been added + /// + Add = 0, + + /// + /// A language has been deleted + /// + Remove = 1, + + /// + /// A language has been updated - but it's culture remains the same + /// + Update = 2, + + /// + /// A language has been updated - it's culture has changed + /// + ChangeCulture = 3, + } + + public JsonPayload(int id, string isoCode, LanguageChangeType changeType) + { + Id = id; + IsoCode = isoCode; + ChangeType = changeType; + } + + public int Id { get; } + + public string IsoCode { get; } + + public LanguageChangeType ChangeType { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Language Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[] payloads) + { + if (payloads.Length == 0) + { + return; + } + + var clearDictionary = false; + var clearContent = false; + + // clear all no matter what type of payload + ClearAllIsolatedCacheByEntityType(); + + foreach (JsonPayload payload in payloads) + { + switch (payload.ChangeType) + { + case LanguageChangeType.Update: + clearDictionary = true; + break; + case LanguageChangeType.Remove: + case LanguageChangeType.ChangeCulture: + clearDictionary = true; + clearContent = true; + break; + } + } + + if (clearDictionary) + { + ClearAllIsolatedCacheByEntityType(); + } + + // if this flag is set, we will tell the published snapshot service to refresh ALL content and evict ALL IContent items + if (clearContent) + { + // clear all domain caches + RefreshDomains(); + ContentCacheRefresher.RefreshContentTypes(AppCaches); // we need to evict all IContent items + + // now refresh all nucache + ContentCacheRefresher.JsonPayload[] clearContentPayload = + new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + ContentCacheRefresher.NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, clearContentPayload); + } + + // then trigger event + base.Refresh(payloads); + } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs index 8f49ce134c..9975abae8c 100644 --- a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs @@ -1,112 +1,108 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class MacroCacheRefresher : PayloadCacheRefresherBase { - public sealed class MacroCacheRefresher : PayloadCacheRefresherBase + public MacroCacheRefresher( + AppCaches appCaches, + IJsonSerializer jsonSerializer, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, jsonSerializer, eventAggregator, factory) { - public MacroCacheRefresher( - AppCaches appCaches, - IJsonSerializer jsonSerializer, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory) - : base(appCaches, jsonSerializer, eventAggregator, factory) - { - - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("7B1E683C-5F34-43dd-803D-9699EA1E98CA"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Macro Cache Refresher"; - - #endregion - - #region Refresher - - public override void RefreshAll() - { - foreach (var prefix in GetAllMacroCacheKeys()) - AppCaches.RuntimeCache.ClearByKey(prefix); - - ClearAllIsolatedCacheByEntityType(); - - base.RefreshAll(); - } - - public override void Refresh(string json) - { - var payloads = Deserialize(json); - - if (payloads is not null) - { - Refresh(payloads); - } - } - - public override void Refresh(JsonPayload[] payloads) - { - foreach (var payload in payloads) - { - foreach (var alias in GetCacheKeysForAlias(payload.Alias)) - { - AppCaches.RuntimeCache.ClearByKey(alias); - } - - Attempt macroRepoCache = AppCaches.IsolatedCaches.Get(); - if (macroRepoCache) - { - macroRepoCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - macroRepoCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Alias)); // Repository caching of macro definition by alias - } - } - - base.Refresh(payloads); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, string alias) - { - Id = id; - Alias = alias; - } - - public int Id { get; } - - public string Alias { get; } - } - - #endregion - - #region Helpers - - internal static string[] GetAllMacroCacheKeys() - { - return new[] - { - CacheKeys.MacroContentCacheKey, // macro render cache - CacheKeys.MacroFromAliasCacheKey, // lookup macro by alias - }; - } - - internal static string[] GetCacheKeysForAlias(string alias) - { - return GetAllMacroCacheKeys().Select(x => x + alias).ToArray(); - } - - #endregion } + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, string alias) + { + Id = id; + Alias = alias; + } + + public int Id { get; } + + public string Alias { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("7B1E683C-5F34-43dd-803D-9699EA1E98CA"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Macro Cache Refresher"; + + #endregion + + #region Refresher + + public override void RefreshAll() + { + foreach (var prefix in GetAllMacroCacheKeys()) + { + AppCaches.RuntimeCache.ClearByKey(prefix); + } + + ClearAllIsolatedCacheByEntityType(); + + base.RefreshAll(); + } + + public override void Refresh(string json) + { + JsonPayload[]? payloads = Deserialize(json); + + if (payloads is not null) + { + Refresh(payloads); + } + } + + public override void Refresh(JsonPayload[] payloads) + { + foreach (JsonPayload payload in payloads) + { + foreach (var alias in GetCacheKeysForAlias(payload.Alias)) + { + AppCaches.RuntimeCache.ClearByKey(alias); + } + + Attempt macroRepoCache = AppCaches.IsolatedCaches.Get(); + if (macroRepoCache) + { + macroRepoCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + macroRepoCache.Result?.Clear( + RepositoryCacheKeys + .GetKey(payload.Alias)); // Repository caching of macro definition by alias + } + } + + base.Refresh(payloads); + } + + #endregion + + #region Helpers + + internal static string[] GetAllMacroCacheKeys() => + new[] + { + CacheKeys.MacroContentCacheKey, // macro render cache + CacheKeys.MacroFromAliasCacheKey, // lookup macro by alias + }; + + internal static string[] GetCacheKeysForAlias(string alias) => + GetAllMacroCacheKeys().Select(x => x + alias).ToArray(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/MediaCacheRefresher.cs index 2efd23d71f..43e6a7ce47 100644 --- a/src/Umbraco.Core/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MediaCacheRefresher.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -9,120 +8,119 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache -{ - public sealed class MediaCacheRefresher : PayloadCacheRefresherBase - { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IIdKeyMap _idKeyMap; +namespace Umbraco.Cms.Core.Cache; - public MediaCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) +public sealed class MediaCacheRefresher : PayloadCacheRefresherBase +{ + private readonly IIdKeyMap _idKeyMap; + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public MediaCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IPublishedSnapshotService publishedSnapshotService, + IIdKeyMap idKeyMap, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) + { + _publishedSnapshotService = publishedSnapshotService; + _idKeyMap = idKeyMap; + } + + #region Indirect + + public static void RefreshMediaTypes(AppCaches appCaches) => appCaches.IsolatedCaches.ClearCache(); + + #endregion + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) { - _publishedSnapshotService = publishedSnapshotService; - _idKeyMap = idKeyMap; + Id = id; + Key = key; + ChangeTypes = changeTypes; } - #region Define + public int Id { get; } - public static readonly Guid UniqueId = Guid.Parse("B29286DD-2D40-4DDB-B325-681226589FEC"); + public Guid? Key { get; } - public override Guid RefresherUniqueId => UniqueId; + public TreeChangeTypes ChangeTypes { get; } + } - public override string Name => "Media Cache Refresher"; + #endregion - #endregion + #region Define - #region Refresher + public static readonly Guid UniqueId = Guid.Parse("B29286DD-2D40-4DDB-B325-681226589FEC"); - public override void Refresh(JsonPayload[] payloads) + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Media Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(JsonPayload[]? payloads) + { + if (payloads == null) { - if (payloads == null) return; + return; + } - _publishedSnapshotService.Notify(payloads, out var anythingChanged); + _publishedSnapshotService.Notify(payloads, out var anythingChanged); - if (anythingChanged) + if (anythingChanged) + { + AppCaches.ClearPartialViewCache(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey); + + Attempt mediaCache = AppCaches.IsolatedCaches.Get(); + + foreach (JsonPayload payload in payloads) { - AppCaches.ClearPartialViewCache(); - AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey); - - var mediaCache = AppCaches.IsolatedCaches.Get(); - - foreach (var payload in payloads) + if (payload.ChangeTypes == TreeChangeTypes.Remove) { - if (payload.ChangeTypes == TreeChangeTypes.Remove) - _idKeyMap.ClearCache(payload.Id); + _idKeyMap.ClearCache(payload.Id); + } - if (!mediaCache.Success) continue; + if (!mediaCache.Success) + { + continue; + } - // repository cache - // it *was* done for each pathId but really that does not make sense - // only need to do it for the current media - mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + // repository cache + // it *was* done for each pathId but really that does not make sense + // only need to do it for the current media + mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Key)); - // remove those that are in the branch - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) - { - var pathid = "," + payload.Id + ","; - mediaCache.Result?.ClearOfType((_, v) => v.Path?.Contains(pathid) ?? false); - } + // remove those that are in the branch + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) + { + var pathid = "," + payload.Id + ","; + mediaCache.Result?.ClearOfType((_, v) => v.Path?.Contains(pathid) ?? false); } } - - base.Refresh(payloads); } - // these events should never trigger - // everything should be JSON - - public override void RefreshAll() - { - throw new NotSupportedException(); - } - - public override void Refresh(int id) - { - throw new NotSupportedException(); - } - - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } - - public override void Remove(int id) - { - throw new NotSupportedException(); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) - { - Id = id; - Key = key; - ChangeTypes = changeTypes; - } - - public int Id { get; } - public Guid? Key { get; } - public TreeChangeTypes ChangeTypes { get; } - } - - #endregion - - #region Indirect - - public static void RefreshMediaTypes(AppCaches appCaches) - { - appCaches.IsolatedCaches.ClearCache(); - } - - #endregion + base.Refresh(payloads); } + + // these events should never trigger + // everything should be JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs index 9869f226b9..ac9dac5a09 100644 --- a/src/Umbraco.Core/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MemberCacheRefresher.cs @@ -1,6 +1,5 @@ -//using Newtonsoft.Json; +// using Newtonsoft.Json; -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -9,89 +8,76 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class MemberCacheRefresher : PayloadCacheRefresherBase { - public sealed class MemberCacheRefresher : PayloadCacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA"); + + private readonly IIdKeyMap _idKeyMap; + + public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) => + _idKeyMap = idKeyMap; + + #region Indirect + + public static void RefreshMemberTypes(AppCaches appCaches) => appCaches.IsolatedCaches.ClearCache(); + + #endregion + + public class JsonPayload { - private readonly IIdKeyMap _idKeyMap; - - public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) + // [JsonConstructor] + public JsonPayload(int id, string? username, bool removed) { - _idKeyMap = idKeyMap; + Id = id; + Username = username; + Removed = removed; } - public class JsonPayload + public int Id { get; } + + public string? Username { get; } + + public bool Removed { get; } + } + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Member Cache Refresher"; + + public override void Refresh(JsonPayload[] payloads) + { + ClearCache(payloads); + base.Refresh(payloads); + } + + public override void Refresh(int id) + { + ClearCache(new JsonPayload(id, null, false)); + base.Refresh(id); + } + + public override void Remove(int id) + { + ClearCache(new JsonPayload(id, null, false)); + base.Remove(id); + } + + private void ClearCache(params JsonPayload[] payloads) + { + AppCaches.ClearPartialViewCache(); + Attempt memberCache = AppCaches.IsolatedCaches.Get(); + + foreach (JsonPayload p in payloads) { - //[JsonConstructor] - public JsonPayload(int id, string? username, bool removed) + _idKeyMap.ClearCache(p.Id); + if (memberCache.Success) { - Id = id; - Username = username; - Removed = removed; + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Id)); + memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Username)); } - - public int Id { get; } - public string? Username { get; } - public bool Removed { get; } } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Member Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(JsonPayload[] payloads) - { - ClearCache(payloads); - base.Refresh(payloads); - } - - public override void Refresh(int id) - { - ClearCache(new JsonPayload(id, null, false)); - base.Refresh(id); - } - - public override void Remove(int id) - { - ClearCache(new JsonPayload(id, null, false)); - base.Remove(id); - } - - private void ClearCache(params JsonPayload[] payloads) - { - AppCaches.ClearPartialViewCache(); - var memberCache = AppCaches.IsolatedCaches.Get(); - - foreach (var p in payloads) - { - _idKeyMap.ClearCache(p.Id); - if (memberCache.Success) - { - memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Id)); - memberCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Username)); - } - } - - } - - #endregion - - #region Indirect - - public static void RefreshMemberTypes(AppCaches appCaches) - { - appCaches.IsolatedCaches.ClearCache(); - } - - #endregion } } diff --git a/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs index 0866f7b39a..05bd6049c8 100644 --- a/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MemberGroupCacheRefresher.cs @@ -1,74 +1,70 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class MemberGroupCacheRefresher : PayloadCacheRefresherBase { - public sealed class MemberGroupCacheRefresher : PayloadCacheRefresherBase + public MemberGroupCacheRefresher(AppCaches appCaches, IJsonSerializer jsonSerializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, jsonSerializer, eventAggregator, factory) { - public MemberGroupCacheRefresher(AppCaches appCaches, IJsonSerializer jsonSerializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, jsonSerializer, eventAggregator, factory) - { - - } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("187F236B-BD21-4C85-8A7C-29FBA3D6C00C"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Member Group Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(string json) - { - ClearCache(); - base.Refresh(json); - } - - public override void Refresh(int id) - { - ClearCache(); - base.Refresh(id); - } - - public override void Remove(int id) - { - ClearCache(); - base.Remove(id); - } - - private void ClearCache() - { - // Since we cache by group name, it could be problematic when renaming to - // previously existing names - see http://issues.umbraco.org/issue/U4-10846. - // To work around this, just clear all the cache items - AppCaches.IsolatedCaches.ClearCache(); - } - - #endregion - - #region Json - - public class JsonPayload - { - public JsonPayload(int id, string name) - { - Id = id; - Name = name; - } - - public string Name { get; } - public int Id { get; } - } - - - #endregion } + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, string name) + { + Id = id; + Name = name; + } + + public string Name { get; } + + public int Id { get; } + } + + #endregion + + #region Define + + public static readonly Guid UniqueId = Guid.Parse("187F236B-BD21-4C85-8A7C-29FBA3D6C00C"); + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Member Group Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(string json) + { + ClearCache(); + base.Refresh(json); + } + + public override void Refresh(int id) + { + ClearCache(); + base.Refresh(id); + } + + public override void Remove(int id) + { + ClearCache(); + base.Remove(id); + } + + private void ClearCache() => + + // Since we cache by group name, it could be problematic when renaming to + // previously existing names - see http://issues.umbraco.org/issue/U4-10846. + // To work around this, just clear all the cache items + AppCaches.IsolatedCaches.ClearCache(); + + #endregion } diff --git a/src/Umbraco.Core/Cache/NoAppCache.cs b/src/Umbraco.Core/Cache/NoAppCache.cs index ef22a51ab0..70edbcf61d 100644 --- a/src/Umbraco.Core/Cache/NoAppCache.cs +++ b/src/Umbraco.Core/Cache/NoAppCache.cs @@ -1,93 +1,85 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Implements and do not cache. +/// +public class NoAppCache : IAppPolicyCache, IRequestCache { - /// - /// Implements and do not cache. - /// - public class NoAppCache : IAppPolicyCache, IRequestCache + protected NoAppCache() { - protected NoAppCache() { } - - /// - /// Gets the singleton instance. - /// - public static NoAppCache Instance { get; } = new NoAppCache(); - - /// - public bool IsAvailable => false; - - /// - public virtual object? Get(string cacheKey) - { - return null; - } - - /// - public virtual object? Get(string cacheKey, Func factory) - { - return factory(); - } - - public bool Set(string key, object? value) => false; - - public bool Remove(string key) => false; - - /// - public virtual IEnumerable SearchByKey(string keyStartsWith) - { - return Enumerable.Empty(); - } - - /// - public IEnumerable SearchByRegex(string regex) - { - return Enumerable.Empty(); - } - - /// - public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) - { - return factory(); - } - - /// - public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) - { } - - /// - public virtual void Clear() - { } - - /// - public virtual void Clear(string key) - { } - - /// - public virtual void ClearOfType(Type type) - { } - - /// - public virtual void ClearOfType() - { } - - /// - public virtual void ClearOfType(Func predicate) - { } - - /// - public virtual void ClearByKey(string keyStartsWith) - { } - - /// - public virtual void ClearByRegex(string regex) - { } - - public IEnumerator> GetEnumerator() => new Dictionary().GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + /// + /// Gets the singleton instance. + /// + public static NoAppCache Instance { get; } = new(); + + /// + public bool IsAvailable => false; + + /// + public virtual object? Get(string cacheKey) => null; + + /// + public virtual object? Get(string cacheKey, Func factory) => factory(); + + /// + public virtual IEnumerable SearchByKey(string keyStartsWith) => Enumerable.Empty(); + + /// + public IEnumerable SearchByRegex(string regex) => Enumerable.Empty(); + + /// + public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) => factory(); + + /// + public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + { + } + + /// + public virtual void Clear() + { + } + + /// + public virtual void Clear(string key) + { + } + + /// + public virtual void ClearOfType(Type type) + { + } + + /// + public virtual void ClearOfType() + { + } + + /// + public virtual void ClearOfType(Func predicate) + { + } + + /// + public virtual void ClearByKey(string keyStartsWith) + { + } + + /// + public virtual void ClearByRegex(string regex) + { + } + + public bool Set(string key, object? value) => false; + + public bool Remove(string key) => false; + + public IEnumerator> GetEnumerator() => + new Dictionary().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs index b99975e0e4..2b662d4c2c 100644 --- a/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/NoCacheRepositoryCachePolicy.cs @@ -1,53 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public class NoCacheRepositoryCachePolicy : IRepositoryCachePolicy + where TEntity : class, IEntity { - public class NoCacheRepositoryCachePolicy : IRepositoryCachePolicy - where TEntity : class, IEntity + private NoCacheRepositoryCachePolicy() { - private NoCacheRepositoryCachePolicy() { } + } - public static NoCacheRepositoryCachePolicy Instance { get; } = new NoCacheRepositoryCachePolicy(); + public static NoCacheRepositoryCachePolicy Instance { get; } = new(); - public TEntity? Get(TId? id, Func performGet, Func?> performGetAll) - { - return performGet(id); - } + public TEntity? Get(TId? id, Func performGet, Func?> performGetAll) => + performGet(id); - public TEntity? GetCached(TId id) - { - return null; - } + public TEntity? GetCached(TId id) => null; - public bool Exists(TId id, Func performExists, Func?> performGetAll) - { - return performExists(id); - } + public bool Exists(TId id, Func performExists, Func?> performGetAll) => + performExists(id); - public void Create(TEntity entity, Action persistNew) - { - persistNew(entity); - } + public void Create(TEntity entity, Action persistNew) => persistNew(entity); - public void Update(TEntity entity, Action persistUpdated) - { - persistUpdated(entity); - } + public void Update(TEntity entity, Action persistUpdated) => persistUpdated(entity); - public void Delete(TEntity entity, Action persistDeleted) - { - persistDeleted(entity); - } + public void Delete(TEntity entity, Action persistDeleted) => persistDeleted(entity); - public TEntity[] GetAll(TId[]? ids, Func?> performGetAll) - { - return performGetAll(ids)?.ToArray() ?? Array.Empty(); - } + public TEntity[] GetAll(TId[]? ids, Func?> performGetAll) => + performGetAll(ids)?.ToArray() ?? Array.Empty(); - public void ClearAll() - { } + public void ClearAll() + { } } diff --git a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs index 4ec91c4933..dcd83ece94 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs @@ -1,367 +1,423 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Caching; using System.Text.RegularExpressions; -using System.Threading; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache -{ - /// - /// Implements on top of a . - /// - public class ObjectCacheAppCache : IAppPolicyCache, IDisposable - { - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - private bool _disposedValue; +namespace Umbraco.Cms.Core.Cache; - /// - /// Initializes a new instance of the . - /// - public ObjectCacheAppCache() +/// +/// Implements on top of a . +/// +public class ObjectCacheAppCache : IAppPolicyCache, IDisposable +{ + private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion); + private bool _disposedValue; + + /// + /// Initializes a new instance of the . + /// + public ObjectCacheAppCache() => + + // the MemoryCache is created with name "in-memory". That name is + // used to retrieve configuration options. It does not identify the memory cache, i.e. + // each instance of this class has its own, independent, memory cache. + MemoryCache = new MemoryCache("in-memory"); + + /// + /// Gets the internal memory cache, for tests only! + /// + public ObjectCache MemoryCache { get; private set; } + + /// + public object? Get(string key) + { + Lazy? result; + try { - // the MemoryCache is created with name "in-memory". That name is - // used to retrieve configuration options. It does not identify the memory cache, i.e. - // each instance of this class has its own, independent, memory cache. + _locker.EnterReadLock(); + result = MemoryCache.Get(key) as Lazy; // null if key not found + } + finally + { + if (_locker.IsReadLockHeld) + { + _locker.ExitReadLock(); + } + } + + return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null + } + + /// + public object? Get(string key, Func factory) => Get(key, factory, null); + + /// + public IEnumerable SearchByKey(string keyStartsWith) + { + KeyValuePair[] entries; + try + { + _locker.EnterReadLock(); + entries = MemoryCache + .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) + .ToArray(); // evaluate while locked + } + finally + { + if (_locker.IsReadLockHeld) + { + _locker.ExitReadLock(); + } + } + + return entries + .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null) // backward compat, don't store null values in the cache + .ToList()!; + } + + /// + public IEnumerable SearchByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + + KeyValuePair[] entries; + try + { + _locker.EnterReadLock(); + entries = MemoryCache + .Where(x => compiled.IsMatch(x.Key)) + .ToArray(); // evaluate while locked + } + finally + { + if (_locker.IsReadLockHeld) + { + _locker.ExitReadLock(); + } + } + + return entries + .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null) // backward compat, don't store null values in the cache + .ToList()!; + } + + /// + public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) + { + // see notes in HttpRuntimeAppCache + Lazy? result; + + try + { + _locker.EnterUpgradeableReadLock(); + + result = MemoryCache.Get(key) as Lazy; + + // get non-created as NonCreatedValue & exceptions as null + if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) + { + result = SafeLazy.GetSafeLazy(factory); + CacheItemPolicy policy = GetPolicy(timeout, isSliding, dependentFiles); + + try + { + _locker.EnterWriteLock(); + + // NOTE: This does an add or update + MemoryCache.Set(key, result, policy); + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + } + finally + { + if (_locker.IsUpgradeableReadLockHeld) + { + _locker.ExitUpgradeableReadLock(); + } + } + + // 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 void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, 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. + Lazy result = SafeLazy.GetSafeLazy(factory); + var value = result.Value; // force evaluation now + if (value == null) + { + return; // do not store null values (backward compat) + } + + CacheItemPolicy policy = GetPolicy(timeout, isSliding, dependentFiles); + + // NOTE: This does an add or update + MemoryCache.Set(key, result, policy); + } + + /// + public virtual void Clear() + { + try + { + _locker.EnterWriteLock(); + MemoryCache.DisposeIfDisposable(); MemoryCache = new MemoryCache("in-memory"); } - - /// - /// Gets the internal memory cache, for tests only! - /// - public ObjectCache MemoryCache { get; private set; } - - /// - public object? Get(string key) + finally { - Lazy? result; - try + if (_locker.IsWriteLockHeld) { - _locker.EnterReadLock(); - result = MemoryCache.Get(key) as Lazy; // null if key not found + _locker.ExitWriteLock(); } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - return result == null ? null : SafeLazy.GetSafeLazyValue(result); // return exceptions as null - } - - /// - public object? Get(string key, Func factory) - { - return Get(key, factory, null); - } - - /// - public IEnumerable SearchByKey(string keyStartsWith) - { - KeyValuePair[] entries; - try - { - _locker.EnterReadLock(); - entries = MemoryCache - .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) - .ToArray(); // evaluate while locked - } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - return entries - .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null - .Where(x => x != null) // backward compat, don't store null values in the cache - .ToList()!; - } - - /// - public IEnumerable SearchByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - - KeyValuePair[] entries; - try - { - _locker.EnterReadLock(); - entries = MemoryCache - .Where(x => compiled.IsMatch(x.Key)) - .ToArray(); // evaluate while locked - } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - return entries - .Select(x => SafeLazy.GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null - .Where(x => x != null) // backward compat, don't store null values in the cache - .ToList()!; - } - - /// - public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) - { - // see notes in HttpRuntimeAppCache - - Lazy? result; - - try - { - _locker.EnterUpgradeableReadLock(); - - result = MemoryCache.Get(key) as Lazy; - if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null - { - result = SafeLazy.GetSafeLazy(factory); - var policy = GetPolicy(timeout, isSliding, dependentFiles); - - try - { - _locker.EnterWriteLock(); - //NOTE: This does an add or update - MemoryCache.Set(key, result, policy); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - } - finally - { - if (_locker.IsUpgradeableReadLockHeld) - _locker.ExitUpgradeableReadLock(); - } - - //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 void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, 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. - - var result = SafeLazy.GetSafeLazy(factory); - var value = result.Value; // force evaluation now - if (value == null) return; // do not store null values (backward compat) - - var policy = GetPolicy(timeout, isSliding, dependentFiles); - //NOTE: This does an add or update - MemoryCache.Set(key, result, policy); - } - - /// - public virtual void Clear() - { - try - { - _locker.EnterWriteLock(); - MemoryCache.DisposeIfDisposable(); - MemoryCache = new MemoryCache("in-memory"); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void Clear(string key) - { - try - { - _locker.EnterWriteLock(); - if (MemoryCache[key] == null) return; - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType(Type type) - { - if (type == null) return; - var isInterface = type.IsInterface; - try - { - _locker.EnterWriteLock(); - foreach (var key in MemoryCache - .Where(x => - { - // x.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); - }) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType() - { - try - { - _locker.EnterWriteLock(); - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - foreach (var key in MemoryCache - .Where(x => - { - // x.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); - - }) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearOfType(Func predicate) - { - try - { - _locker.EnterWriteLock(); - var typeOfT = typeof(T); - var isInterface = typeOfT.IsInterface; - foreach (var key in MemoryCache - .Where(x => - { - // x.Value is Lazy and not null, its value may be null - // remove null values as well, does not hurt - // get non-created as NonCreatedValue & exceptions as null - var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); - if (value == null) return true; - - // if T is an interface remove anything that implements that interface - // otherwise remove exact types (not inherited types) - return (isInterface ? (value is T) : (value.GetType() == typeOfT)) - && predicate(x.Key, (T)value); - }) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearByKey(string keyStartsWith) - { - try - { - _locker.EnterWriteLock(); - foreach (var key in MemoryCache - .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - /// - public virtual void ClearByRegex(string regex) - { - var compiled = new Regex(regex, RegexOptions.Compiled); - - try - { - _locker.EnterWriteLock(); - foreach (var key in MemoryCache - .Where(x => compiled.IsMatch(x.Key)) - .Select(x => x.Key) - .ToArray()) // ToArray required to remove - MemoryCache.Remove(key); - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - private static CacheItemPolicy GetPolicy(TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) - { - var absolute = isSliding ? ObjectCache.InfiniteAbsoluteExpiration : (timeout == null ? ObjectCache.InfiniteAbsoluteExpiration : DateTime.Now.Add(timeout.Value)); - var sliding = isSliding == false ? ObjectCache.NoSlidingExpiration : (timeout ?? ObjectCache.NoSlidingExpiration); - - var policy = new CacheItemPolicy - { - AbsoluteExpiration = absolute, - SlidingExpiration = sliding - }; - - if (dependentFiles != null && dependentFiles.Any()) - { - policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); - } - - return policy; - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _locker.Dispose(); - } - _disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); } } + + /// + public virtual void Clear(string key) + { + try + { + _locker.EnterWriteLock(); + if (MemoryCache[key] == null) + { + return; + } + + MemoryCache.Remove(key); + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearOfType(Type type) + { + if (type == null) + { + return; + } + + var isInterface = type.IsInterface; + try + { + _locker.EnterWriteLock(); + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || + (isInterface ? type.IsInstanceOfType(value) : value.GetType() == type); + }) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearOfType() + { + try + { + _locker.EnterWriteLock(); + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? value is T : value.GetType() == typeOfT); + }) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearOfType(Func predicate) + { + try + { + _locker.EnterWriteLock(); + Type typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = SafeLazy.GetSafeLazyValue((Lazy)x.Value, true); + if (value == null) + { + return true; + } + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? value is T : value.GetType() == typeOfT) + && predicate(x.Key, (T)value); + }) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearByKey(string keyStartsWith) + { + try + { + _locker.EnterWriteLock(); + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + /// + public virtual void ClearByRegex(string regex) + { + var compiled = new Regex(regex, RegexOptions.Compiled); + + try + { + _locker.EnterWriteLock(); + + // ToArray required to remove + foreach (var key in MemoryCache + .Where(x => compiled.IsMatch(x.Key)) + .Select(x => x.Key) + .ToArray()) + { + MemoryCache.Remove(key); + } + } + finally + { + if (_locker.IsWriteLockHeld) + { + _locker.ExitWriteLock(); + } + } + } + + public void Dispose() => + + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _locker.Dispose(); + } + + _disposedValue = true; + } + } + + private static CacheItemPolicy GetPolicy(TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) + { + DateTimeOffset absolute = isSliding ? ObjectCache.InfiniteAbsoluteExpiration : + timeout == null ? ObjectCache.InfiniteAbsoluteExpiration : DateTime.Now.Add(timeout.Value); + TimeSpan sliding = isSliding == false + ? ObjectCache.NoSlidingExpiration + : timeout ?? ObjectCache.NoSlidingExpiration; + + var policy = new CacheItemPolicy { AbsoluteExpiration = absolute, SlidingExpiration = sliding }; + + if (dependentFiles != null && dependentFiles.Any()) + { + policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); + } + + return policy; + } } diff --git a/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs b/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs index 2dc3ddcf1b..f371e80979 100644 --- a/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/PayloadCacheRefresherBase.cs @@ -3,49 +3,48 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for "payload" class refreshers. +/// +/// The payload type. +/// The notification type +/// The actual cache refresher type is used for strongly typed events. +public abstract class + PayloadCacheRefresherBase : JsonCacheRefresherBase, + IPayloadCacheRefresher + where TNotification : CacheRefresherNotification { /// - /// A base class for "payload" class refreshers. + /// Initializes a new instance of the . /// - /// The actual cache refresher type. - /// The payload type. - /// The actual cache refresher type is used for strongly typed events. - public abstract class PayloadCacheRefresherBase : JsonCacheRefresherBase, IPayloadCacheRefresher - where TNotification : CacheRefresherNotification + /// A cache helper. + /// + /// + /// + protected PayloadCacheRefresherBase(AppCaches appCaches, IJsonSerializer serializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { - - /// - /// Initializes a new instance of the . - /// - /// A cache helper. - /// - protected PayloadCacheRefresherBase(AppCaches appCaches, IJsonSerializer serializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, serializer, eventAggregator, factory) - { - } - - - #region Refresher - - public override void Refresh(string json) - { - var payload = Deserialize(json); - if (payload is not null) - { - Refresh(payload); - } - } - - /// - /// Refreshes as specified by a payload. - /// - /// The payload. - public virtual void Refresh(TPayload[] payloads) - { - OnCacheUpdated(NotificationFactory.Create(payloads, MessageType.RefreshByPayload)); - } - - #endregion } + + #region Refresher + + public override void Refresh(string json) + { + TPayload[]? payload = Deserialize(json); + if (payload is not null) + { + Refresh(payload); + } + } + + /// + /// Refreshes as specified by a payload. + /// + /// The payload. + public virtual void Refresh(TPayload[] payloads) => + OnCacheUpdated(NotificationFactory.Create(payloads, MessageType.RefreshByPayload)); + + #endregion } diff --git a/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs b/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs index 5c9eb20b4c..9124d7350e 100644 --- a/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/PublicAccessCacheRefresher.cs @@ -1,52 +1,51 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class PublicAccessCacheRefresher : CacheRefresherBase { - public sealed class PublicAccessCacheRefresher : CacheRefresherBase + #region Define + + public static readonly Guid UniqueId = Guid.Parse("1DB08769-B104-4F8B-850E-169CAC1DF2EC"); + + public PublicAccessCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public PublicAccessCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("1DB08769-B104-4F8B-850E-169CAC1DF2EC"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "Public Access Cache Refresher"; - - #endregion - - #region Refresher - - public override void Refresh(Guid id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } - - public override void Refresh(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Refresh(id); - } - - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); - } - - public override void Remove(int id) - { - ClearAllIsolatedCacheByEntityType(); - base.Remove(id); - } - - #endregion } + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "Public Access Cache Refresher"; + + #endregion + + #region Refresher + + public override void Refresh(Guid id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(id); + } + + public override void Refresh(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Refresh(id); + } + + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } + + public override void Remove(int id) + { + ClearAllIsolatedCacheByEntityType(); + base.Remove(id); + } + + #endregion } diff --git a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs index 9f1c45374e..8da3cd5be0 100644 --- a/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/RelationTypeCacheRefresher.cs @@ -1,55 +1,51 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class RelationTypeCacheRefresher : CacheRefresherBase { - public sealed class RelationTypeCacheRefresher : CacheRefresherBase + public RelationTypeCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public RelationTypeCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } + } - #region Define + public static readonly Guid UniqueId = Guid.Parse("D8375ABA-4FB3-4F86-B505-92FBA1B6F7C9"); - public static readonly Guid UniqueId = Guid.Parse("D8375ABA-4FB3-4F86-B505-92FBA1B6F7C9"); + public override Guid RefresherUniqueId => UniqueId; - public override Guid RefresherUniqueId => UniqueId; + public override string Name => "Relation Type Cache Refresher"; - public override string Name => "Relation Type Cache Refresher"; + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } - #endregion - - #region Refresher - - public override void RefreshAll() + public override void Refresh(int id) + { + Attempt cache = AppCaches.IsolatedCaches.Get(); + if (cache.Success) { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); + cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); } - public override void Refresh(int id) + base.Refresh(id); + } + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + // base.Refresh(id); + public override void Remove(int id) + { + Attempt cache = AppCaches.IsolatedCaches.Get(); + if (cache.Success) { - var cache = AppCaches.IsolatedCaches.Get(); - if (cache.Success) cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - base.Refresh(id); + cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); } - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - //base.Refresh(id); - } - - public override void Remove(int id) - { - var cache = AppCaches.IsolatedCaches.Get(); - if (cache.Success) cache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - base.Remove(id); - } - - #endregion + base.Remove(id); } } diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index c719ce72e5..ba7b251aa0 100644 --- a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs @@ -1,51 +1,50 @@ -using System; +namespace Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Cache +/// +/// Specifies how a repository cache policy should cache entities. +/// +public class RepositoryCachePolicyOptions { /// - /// Specifies how a repository cache policy should cache entities. + /// Ctor - sets GetAllCacheValidateCount = true /// - public class RepositoryCachePolicyOptions + public RepositoryCachePolicyOptions(Func performCount) { - /// - /// Ctor - sets GetAllCacheValidateCount = true - /// - public RepositoryCachePolicyOptions(Func performCount) - { - PerformCount = performCount; - GetAllCacheValidateCount = true; - GetAllCacheAllowZeroCount = false; - } - - /// - /// Ctor - sets GetAllCacheValidateCount = false - /// - public RepositoryCachePolicyOptions() - { - PerformCount = null; - GetAllCacheValidateCount = false; - GetAllCacheAllowZeroCount = false; - } - - /// - /// Callback required to get count for GetAllCacheValidateCount - /// - public Func? PerformCount { get; set; } - - /// - /// True/false as to validate the total item count when all items are returned from cache, the default is true but this - /// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the normal - /// GetAll method. - /// - /// - /// setting this to return false will improve performance of GetAll cache with no params but should only be used - /// for specific circumstances - /// - public bool GetAllCacheValidateCount { get; set; } - - /// - /// True if the GetAll method will cache that there are zero results so that the db is not hit when there are no results found - /// - public bool GetAllCacheAllowZeroCount { get; set; } + PerformCount = performCount; + GetAllCacheValidateCount = true; + GetAllCacheAllowZeroCount = false; } + + /// + /// Ctor - sets GetAllCacheValidateCount = false + /// + public RepositoryCachePolicyOptions() + { + PerformCount = null; + GetAllCacheValidateCount = false; + GetAllCacheAllowZeroCount = false; + } + + /// + /// Callback required to get count for GetAllCacheValidateCount + /// + public Func? PerformCount { get; set; } + + /// + /// True/false as to validate the total item count when all items are returned from cache, the default is true but this + /// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the + /// normal + /// GetAll method. + /// + /// + /// setting this to return false will improve performance of GetAll cache with no params but should only be used + /// for specific circumstances + /// + public bool GetAllCacheValidateCount { get; set; } + + /// + /// True if the GetAll method will cache that there are zero results so that the db is not hit when there are no + /// results found + /// + public bool GetAllCacheAllowZeroCount { get; set; } } diff --git a/src/Umbraco.Core/Cache/SafeLazy.cs b/src/Umbraco.Core/Cache/SafeLazy.cs index 387e5c0271..40512ece67 100644 --- a/src/Umbraco.Core/Cache/SafeLazy.cs +++ b/src/Umbraco.Core/Cache/SafeLazy.cs @@ -1,63 +1,64 @@ -using System; using System.Runtime.ExceptionServices; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public static class SafeLazy { - public static class SafeLazy - { - // an object that represent a value that has not been created yet - internal static readonly object ValueNotCreated = new object(); + // an object that represent a value that has not been created yet + internal static readonly object ValueNotCreated = new(); - public static Lazy GetSafeLazy(Func getCacheItem) + public static Lazy GetSafeLazy(Func getCacheItem) => + + // try to generate the value and if it fails, + // wrap in an ExceptionHolder - would be much simpler + // to just use lazy.IsValueFaulted alas that field is + // internal + new Lazy(() => { - // try to generate the value and if it fails, - // wrap in an ExceptionHolder - would be much simpler - // to just use lazy.IsValueFaulted alas that field is - // internal - return new Lazy(() => - { - try - { - return getCacheItem(); - } - catch (Exception e) - { - return new ExceptionHolder(ExceptionDispatchInfo.Capture(e)); - } - }); - } - - public static object? GetSafeLazyValue(Lazy? lazy, bool onlyIfValueIsCreated = false) - { - // if onlyIfValueIsCreated, do not trigger value creation - // must return something, though, to differentiate from null values - if (onlyIfValueIsCreated && lazy?.IsValueCreated == false) return ValueNotCreated; - - // if execution has thrown then lazy.IsValueCreated is false - // and lazy.IsValueFaulted is true (but internal) so we use our - // own exception holder (see Lazy source code) to return null - if (lazy?.Value is ExceptionHolder) return null; - - // we have a value and execution has not thrown so returning - // here does not throw - unless we're re-entering, take care of it try { - return lazy?.Value; + return getCacheItem(); } - catch (InvalidOperationException e) + catch (Exception e) { - throw new InvalidOperationException("The method that computes a value for the cache has tried to read that value from the cache.", e); + return new ExceptionHolder(ExceptionDispatchInfo.Capture(e)); } + }); + + public static object? GetSafeLazyValue(Lazy? lazy, bool onlyIfValueIsCreated = false) + { + // if onlyIfValueIsCreated, do not trigger value creation + // must return something, though, to differentiate from null values + if (onlyIfValueIsCreated && lazy?.IsValueCreated == false) + { + return ValueNotCreated; } - public class ExceptionHolder + // if execution has thrown then lazy.IsValueCreated is false + // and lazy.IsValueFaulted is true (but internal) so we use our + // own exception holder (see Lazy source code) to return null + if (lazy?.Value is ExceptionHolder) { - public ExceptionHolder(ExceptionDispatchInfo e) - { - Exception = e; - } + return null; + } - public ExceptionDispatchInfo Exception { get; } + // we have a value and execution has not thrown so returning + // here does not throw - unless we're re-entering, take care of it + try + { + return lazy?.Value; + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException( + "The method that computes a value for the cache has tried to read that value from the cache.", e); } } + + public class ExceptionHolder + { + public ExceptionHolder(ExceptionDispatchInfo e) => Exception = e; + + public ExceptionDispatchInfo Exception { get; } + } } diff --git a/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs b/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs index 0bc2c6c5ef..221ad7c836 100644 --- a/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/TemplateCacheRefresher.cs @@ -1,66 +1,61 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class TemplateCacheRefresher : CacheRefresherBase { - public sealed class TemplateCacheRefresher : CacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("DD12B6A0-14B9-46e8-8800-C154F74047C8"); + + private readonly IContentTypeCommonRepository _contentTypeCommonRepository; + private readonly IIdKeyMap _idKeyMap; + + public TemplateCacheRefresher( + AppCaches appCaches, + IIdKeyMap idKeyMap, + IContentTypeCommonRepository contentTypeCommonRepository, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - private readonly IIdKeyMap _idKeyMap; - private readonly IContentTypeCommonRepository _contentTypeCommonRepository; + _idKeyMap = idKeyMap; + _contentTypeCommonRepository = contentTypeCommonRepository; + } - public TemplateCacheRefresher(AppCaches appCaches, IIdKeyMap idKeyMap, IContentTypeCommonRepository contentTypeCommonRepository, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { - _idKeyMap = idKeyMap; - _contentTypeCommonRepository = contentTypeCommonRepository; - } + public override Guid RefresherUniqueId => UniqueId; - #region Define + public override string Name => "Template Cache Refresher"; - public static readonly Guid UniqueId = Guid.Parse("DD12B6A0-14B9-46e8-8800-C154F74047C8"); + public override void Refresh(int id) + { + RemoveFromCache(id); + base.Refresh(id); + } - public override Guid RefresherUniqueId => UniqueId; + public override void Remove(int id) + { + RemoveFromCache(id); - public override string Name => "Template Cache Refresher"; + // During removal we need to clear the runtime cache for templates, content and content type instances!!! + // all three of these types are referenced by templates, and the cache needs to be cleared on every server, + // otherwise things like looking up content type's after a template is removed is still going to show that + // it has an associated template. + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + _contentTypeCommonRepository.ClearCache(); - #endregion + base.Remove(id); + } - #region Refresher + private void RemoveFromCache(int id) + { + _idKeyMap.ClearCache(id); + AppCaches.RuntimeCache.Clear($"{CacheKeys.TemplateFrontEndCacheKey}{id}"); - public override void Refresh(int id) - { - RemoveFromCache(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - RemoveFromCache(id); - - //During removal we need to clear the runtime cache for templates, content and content type instances!!! - // all three of these types are referenced by templates, and the cache needs to be cleared on every server, - // otherwise things like looking up content type's after a template is removed is still going to show that - // it has an associated template. - ClearAllIsolatedCacheByEntityType(); - ClearAllIsolatedCacheByEntityType(); - _contentTypeCommonRepository.ClearCache(); - - base.Remove(id); - } - - private void RemoveFromCache(int id) - { - _idKeyMap.ClearCache(id); - AppCaches.RuntimeCache.Clear($"{CacheKeys.TemplateFrontEndCacheKey}{id}"); - - //need to clear the runtime cache for templates - ClearAllIsolatedCacheByEntityType(); - } - - #endregion + // need to clear the runtime cache for templates + ClearAllIsolatedCacheByEntityType(); } } diff --git a/src/Umbraco.Core/Cache/UserCacheRefresher.cs b/src/Umbraco.Core/Cache/UserCacheRefresher.cs index 10c4865ba8..d1dc194f9b 100644 --- a/src/Umbraco.Core/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserCacheRefresher.cs @@ -1,56 +1,55 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class UserCacheRefresher : CacheRefresherBase { - public sealed class UserCacheRefresher : CacheRefresherBase + #region Define + + public static readonly Guid UniqueId = Guid.Parse("E057AF6D-2EE6-41F4-8045-3694010F0AA6"); + + public UserCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public UserCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("E057AF6D-2EE6-41F4-8045-3694010F0AA6"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "User Cache Refresher"; - - #endregion - - #region Refresher - - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - var userCache = AppCaches.IsolatedCaches.Get(); - if (userCache.Success) - { - userCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - userCache.Result?.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id); - } - - - base.Remove(id); - } - #endregion } + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "User Cache Refresher"; + + #endregion + + #region Refresher + + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } + + public override void Refresh(int id) + { + Remove(id); + base.Refresh(id); + } + + public override void Remove(int id) + { + Attempt userCache = AppCaches.IsolatedCaches.Get(); + if (userCache.Success) + { + userCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); + userCache.Result?.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); + userCache.Result?.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); + userCache.Result?.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); + userCache.Result?.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id); + } + + base.Remove(id); + } + + #endregion } diff --git a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs index a889146794..ccf004a8d7 100644 --- a/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/UserGroupCacheRefresher.cs @@ -1,71 +1,70 @@ -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Handles User group cache invalidation/refreshing +/// +/// +/// This also needs to clear the user cache since IReadOnlyUserGroup's are attached to IUser objects +/// +public sealed class UserGroupCacheRefresher : CacheRefresherBase { - /// - /// Handles User group cache invalidation/refreshing - /// - /// - /// This also needs to clear the user cache since IReadOnlyUserGroup's are attached to IUser objects - /// - public sealed class UserGroupCacheRefresher : CacheRefresherBase + #region Define + + public static readonly Guid UniqueId = Guid.Parse("45178038-B232-4FE8-AA1A-F2B949C44762"); + + public UserGroupCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, eventAggregator, factory) { - public UserGroupCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) - { } - - #region Define - - public static readonly Guid UniqueId = Guid.Parse("45178038-B232-4FE8-AA1A-F2B949C44762"); - - public override Guid RefresherUniqueId => UniqueId; - - public override string Name => "User Group Cache Refresher"; - - #endregion - - #region Refresher - - public override void RefreshAll() - { - ClearAllIsolatedCacheByEntityType(); - var userGroupCache = AppCaches.IsolatedCaches.Get(); - if (userGroupCache.Success) - { - userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); - } - - //We'll need to clear all user cache too - ClearAllIsolatedCacheByEntityType(); - - base.RefreshAll(); - } - - public override void Refresh(int id) - { - Remove(id); - base.Refresh(id); - } - - public override void Remove(int id) - { - var userGroupCache = AppCaches.IsolatedCaches.Get(); - if (userGroupCache.Success) - { - userGroupCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); - } - - //we don't know what user's belong to this group without doing a look up so we'll need to just clear them all - ClearAllIsolatedCacheByEntityType(); - - base.Remove(id); - } - - #endregion } + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "User Group Cache Refresher"; + + #endregion + + #region Refresher + + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + Attempt userGroupCache = AppCaches.IsolatedCaches.Get(); + if (userGroupCache.Success) + { + userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); + } + + // We'll need to clear all user cache too + ClearAllIsolatedCacheByEntityType(); + + base.RefreshAll(); + } + + public override void Refresh(int id) + { + Remove(id); + base.Refresh(id); + } + + public override void Remove(int id) + { + Attempt userGroupCache = AppCaches.IsolatedCaches.Get(); + if (userGroupCache.Success) + { + userGroupCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); + userGroupCache.Result?.ClearByKey(CacheKeys.UserGroupGetByAliasCacheKeyPrefix); + } + + // we don't know what user's belong to this group without doing a look up so we'll need to just clear them all + ClearAllIsolatedCacheByEntityType(); + + base.Remove(id); + } + + #endregion } diff --git a/src/Umbraco.Core/Cache/ValueEditorCache.cs b/src/Umbraco.Core/Cache/ValueEditorCache.cs index 7d5f20efb4..358134ab14 100644 --- a/src/Umbraco.Core/Cache/ValueEditorCache.cs +++ b/src/Umbraco.Core/Cache/ValueEditorCache.cs @@ -1,60 +1,57 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public class ValueEditorCache : IValueEditorCache { - public class ValueEditorCache : IValueEditorCache + private readonly object _dictionaryLocker; + private readonly Dictionary> _valueEditorCache; + + public ValueEditorCache() { - private readonly Dictionary> _valueEditorCache; - private readonly object _dictionaryLocker; + _valueEditorCache = new Dictionary>(); + _dictionaryLocker = new object(); + } - public ValueEditorCache() + public IDataValueEditor GetValueEditor(IDataEditor editor, IDataType dataType) + { + // Lock just in case multiple threads uses the cache at the same time. + lock (_dictionaryLocker) { - _valueEditorCache = new Dictionary>(); - _dictionaryLocker = new object(); - } - - public IDataValueEditor GetValueEditor(IDataEditor editor, IDataType dataType) - { - // Lock just in case multiple threads uses the cache at the same time. - lock (_dictionaryLocker) + // We try and get the dictionary based on the IDataEditor alias, + // this is here just in case a data type can have more than one value data editor. + // If this is not the case this could be simplified quite a bit, by just using the inner dictionary only. + IDataValueEditor? valueEditor; + if (_valueEditorCache.TryGetValue(editor.Alias, out Dictionary? dataEditorCache)) { - // We try and get the dictionary based on the IDataEditor alias, - // this is here just in case a data type can have more than one value data editor. - // If this is not the case this could be simplified quite a bit, by just using the inner dictionary only. - IDataValueEditor? valueEditor; - if (_valueEditorCache.TryGetValue(editor.Alias, out Dictionary? dataEditorCache)) + if (dataEditorCache.TryGetValue(dataType.Id, out valueEditor)) { - if (dataEditorCache.TryGetValue(dataType.Id, out valueEditor)) - { - return valueEditor; - } - - valueEditor = editor.GetValueEditor(dataType.Configuration); - dataEditorCache[dataType.Id] = valueEditor; return valueEditor; } valueEditor = editor.GetValueEditor(dataType.Configuration); - _valueEditorCache[editor.Alias] = new Dictionary { [dataType.Id] = valueEditor }; + dataEditorCache[dataType.Id] = valueEditor; return valueEditor; } - } - public void ClearCache(IEnumerable dataTypeIds) + valueEditor = editor.GetValueEditor(dataType.Configuration); + _valueEditorCache[editor.Alias] = new Dictionary { [dataType.Id] = valueEditor }; + return valueEditor; + } + } + + public void ClearCache(IEnumerable dataTypeIds) + { + lock (_dictionaryLocker) { - lock (_dictionaryLocker) + // If a datatype is saved or deleted we have to clear any value editors based on their ID from the cache, + // since it could mean that their configuration has changed. + foreach (var id in dataTypeIds) { - // If a datatype is saved or deleted we have to clear any value editors based on their ID from the cache, - // since it could mean that their configuration has changed. - foreach (var id in dataTypeIds) + foreach (Dictionary editors in _valueEditorCache.Values) { - foreach (Dictionary editors in _valueEditorCache.Values) - { - editors.Remove(id); - } + editors.Remove(id); } } } diff --git a/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs b/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs index c815ca7a71..68ccdea20d 100644 --- a/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/ValueEditorCacheRefresher.cs @@ -1,57 +1,41 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +public sealed class ValueEditorCacheRefresher : PayloadCacheRefresherBase { - public sealed class ValueEditorCacheRefresher : PayloadCacheRefresherBase + public static readonly Guid UniqueId = Guid.Parse("D28A1DBB-2308-4918-9A92-2F8689B6CBFE"); + private readonly IValueEditorCache _valueEditorCache; + + public ValueEditorCacheRefresher( + AppCaches appCaches, + IJsonSerializer serializer, + IEventAggregator eventAggregator, + ICacheRefresherNotificationFactory factory, + IValueEditorCache valueEditorCache) + : base(appCaches, serializer, eventAggregator, factory) => + _valueEditorCache = valueEditorCache; + + public override Guid RefresherUniqueId => UniqueId; + + public override string Name => "ValueEditorCacheRefresher"; + + public override void Refresh(DataTypeCacheRefresher.JsonPayload[] payloads) { - private readonly IValueEditorCache _valueEditorCache; - - public ValueEditorCacheRefresher( - AppCaches appCaches, - IJsonSerializer serializer, - IEventAggregator eventAggregator, - ICacheRefresherNotificationFactory factory, - IValueEditorCache valueEditorCache) : base(appCaches, serializer, eventAggregator, factory) - { - _valueEditorCache = valueEditorCache; - } - - public static readonly Guid UniqueId = Guid.Parse("D28A1DBB-2308-4918-9A92-2F8689B6CBFE"); - public override Guid RefresherUniqueId => UniqueId; - public override string Name => "ValueEditorCacheRefresher"; - - public override void Refresh(DataTypeCacheRefresher.JsonPayload[] payloads) - { - IEnumerable ids = payloads.Select(x => x.Id); - _valueEditorCache.ClearCache(ids); - } - - // these events should never trigger - // everything should be PAYLOAD/JSON - - public override void RefreshAll() - { - throw new NotSupportedException(); - } - - public override void Refresh(int id) - { - throw new NotSupportedException(); - } - - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } - - public override void Remove(int id) - { - throw new NotSupportedException(); - } + IEnumerable ids = payloads.Select(x => x.Id); + _valueEditorCache.ClearCache(ids); } + + // these events should never trigger + // everything should be PAYLOAD/JSON + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); } diff --git a/src/Umbraco.Core/CodeAnnotations/FriendlyNameAttribute.cs b/src/Umbraco.Core/CodeAnnotations/FriendlyNameAttribute.cs index f6ee121742..12e95f1e04 100644 --- a/src/Umbraco.Core/CodeAnnotations/FriendlyNameAttribute.cs +++ b/src/Umbraco.Core/CodeAnnotations/FriendlyNameAttribute.cs @@ -1,35 +1,26 @@ -using System; +namespace Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.CodeAnnotations +/// +/// Attribute to add a Friendly Name string with an UmbracoObjectType enum value +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +public class FriendlyNameAttribute : Attribute { /// - /// Attribute to add a Friendly Name string with an UmbracoObjectType enum value + /// friendly name value /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = false)] - public class FriendlyNameAttribute : Attribute - { - /// - /// friendly name value - /// - private readonly string _friendlyName; + private readonly string _friendlyName; - /// - /// Initializes a new instance of the FriendlyNameAttribute class - /// Sets the friendly name value - /// - /// attribute value - public FriendlyNameAttribute(string friendlyName) - { - this._friendlyName = friendlyName; - } + /// + /// Initializes a new instance of the FriendlyNameAttribute class + /// Sets the friendly name value + /// + /// attribute value + public FriendlyNameAttribute(string friendlyName) => _friendlyName = friendlyName; - /// - /// Gets the friendly name - /// - /// string of friendly name - public override string ToString() - { - return this._friendlyName; - } - } + /// + /// Gets the friendly name + /// + /// string of friendly name + public override string ToString() => _friendlyName; } diff --git a/src/Umbraco.Core/CodeAnnotations/UmbracoObjectTypeAttribute.cs b/src/Umbraco.Core/CodeAnnotations/UmbracoObjectTypeAttribute.cs index 6c4e2b9d04..13ec38f892 100644 --- a/src/Umbraco.Core/CodeAnnotations/UmbracoObjectTypeAttribute.cs +++ b/src/Umbraco.Core/CodeAnnotations/UmbracoObjectTypeAttribute.cs @@ -1,26 +1,20 @@ -using System; +namespace Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.CodeAnnotations +/// +/// Attribute to associate a GUID string and Type with an UmbracoObjectType Enum value +/// +[AttributeUsage(AttributeTargets.Field)] +public class UmbracoObjectTypeAttribute : Attribute { - /// - /// Attribute to associate a GUID string and Type with an UmbracoObjectType Enum value - /// - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] - public class UmbracoObjectTypeAttribute : Attribute + public UmbracoObjectTypeAttribute(string objectId) => ObjectId = new Guid(objectId); + + public UmbracoObjectTypeAttribute(string objectId, Type modelType) { - public UmbracoObjectTypeAttribute(string objectId) - { - ObjectId = new Guid(objectId); - } - - public UmbracoObjectTypeAttribute(string objectId, Type modelType) - { - ObjectId = new Guid(objectId); - ModelType = modelType; - } - - public Guid ObjectId { get; private set; } - - public Type? ModelType { get; private set; } + ObjectId = new Guid(objectId); + ModelType = modelType; } + + public Guid ObjectId { get; } + + public Type? ModelType { get; } } diff --git a/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs index 5f889daa5c..90df3185c6 100644 --- a/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs +++ b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs @@ -1,15 +1,9 @@ -using System; +namespace Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.CodeAnnotations +[AttributeUsage(AttributeTargets.Field)] +public class UmbracoUdiTypeAttribute : Attribute { - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] - public class UmbracoUdiTypeAttribute : Attribute - { - public string UdiType { get; private set; } + public UmbracoUdiTypeAttribute(string udiType) => UdiType = udiType; - public UmbracoUdiTypeAttribute(string udiType) - { - UdiType = udiType; - } - } + public string UdiType { get; } } diff --git a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs index a9bd71c6cc..abbde4f3f0 100644 --- a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs +++ b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs @@ -1,43 +1,44 @@ -using System; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a composite key of (int, string) for fast dictionaries. +/// +/// +/// The integer part of the key must be greater than, or equal to, zero. +/// The string part of the key is case-insensitive. +/// Null is a valid value for both parts. +/// +public struct CompositeIntStringKey : IEquatable { - /// - /// Represents a composite key of (int, string) for fast dictionaries. - /// - /// - /// The integer part of the key must be greater than, or equal to, zero. - /// The string part of the key is case-insensitive. - /// Null is a valid value for both parts. - /// - public struct CompositeIntStringKey : IEquatable - { - private readonly int _key1; - private readonly string _key2; + private readonly int _key1; + private readonly string _key2; - /// - /// Initializes a new instance of the struct. - /// - public CompositeIntStringKey(int? key1, string key2) + /// + /// Initializes a new instance of the struct. + /// + public CompositeIntStringKey(int? key1, string? key2) + { + if (key1 < 0) { - if (key1 < 0) throw new ArgumentOutOfRangeException(nameof(key1)); - _key1 = key1 ?? -1; - _key2 = key2?.ToLowerInvariant() ?? "NULL"; + throw new ArgumentOutOfRangeException(nameof(key1)); } - public bool Equals(CompositeIntStringKey other) - => _key2 == other._key2 && _key1 == other._key1; - - public override bool Equals(object? obj) - => obj is CompositeIntStringKey other && _key2 == other._key2 && _key1 == other._key1; - - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1; - - public static bool operator ==(CompositeIntStringKey key1, CompositeIntStringKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; - - public static bool operator !=(CompositeIntStringKey key1, CompositeIntStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._key1; + _key1 = key1 ?? -1; + _key2 = key2?.ToLowerInvariant() ?? "NULL"; } + + public static bool operator ==(CompositeIntStringKey key1, CompositeIntStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; + + public static bool operator !=(CompositeIntStringKey key1, CompositeIntStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; + + public bool Equals(CompositeIntStringKey other) + => _key2 == other._key2 && _key1 == other._key1; + + public override bool Equals(object? obj) + => obj is CompositeIntStringKey other && _key2 == other._key2 && _key1 == other._key1; + + public override int GetHashCode() + => (_key2.GetHashCode() * 31) + _key1; } diff --git a/src/Umbraco.Core/Collections/CompositeNStringNStringKey.cs b/src/Umbraco.Core/Collections/CompositeNStringNStringKey.cs index 2886de92f1..0b3ec1aa92 100644 --- a/src/Umbraco.Core/Collections/CompositeNStringNStringKey.cs +++ b/src/Umbraco.Core/Collections/CompositeNStringNStringKey.cs @@ -1,41 +1,38 @@ -using System; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a composite key of (string, string) for fast dictionaries. +/// +/// +/// The string parts of the key are case-insensitive. +/// Null is a valid value for both parts. +/// +public struct CompositeNStringNStringKey : IEquatable { + private readonly string _key1; + private readonly string _key2; + /// - /// Represents a composite key of (string, string) for fast dictionaries. + /// Initializes a new instance of the struct. /// - /// - /// The string parts of the key are case-insensitive. - /// Null is a valid value for both parts. - /// - public struct CompositeNStringNStringKey : IEquatable + public CompositeNStringNStringKey(string? key1, string? key2) { - private readonly string _key1; - private readonly string _key2; - - /// - /// Initializes a new instance of the struct. - /// - public CompositeNStringNStringKey(string? key1, string? key2) - { - _key1 = key1?.ToLowerInvariant() ?? "NULL"; - _key2 = key2?.ToLowerInvariant() ?? "NULL"; - } - - public bool Equals(CompositeNStringNStringKey other) - => _key2 == other._key2 && _key1 == other._key1; - - public override bool Equals(object? obj) - => obj is CompositeNStringNStringKey other && _key2 == other._key2 && _key1 == other._key1; - - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1.GetHashCode(); - - public static bool operator ==(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; - - public static bool operator !=(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._key1; + _key1 = key1?.ToLowerInvariant() ?? "NULL"; + _key2 = key2?.ToLowerInvariant() ?? "NULL"; } + + public static bool operator ==(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; + + public static bool operator !=(CompositeNStringNStringKey key1, CompositeNStringNStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; + + public bool Equals(CompositeNStringNStringKey other) + => _key2 == other._key2 && _key1 == other._key1; + + public override bool Equals(object? obj) + => obj is CompositeNStringNStringKey other && _key2 == other._key2 && _key1 == other._key1; + + public override int GetHashCode() + => (_key2.GetHashCode() * 31) + _key1.GetHashCode(); } diff --git a/src/Umbraco.Core/Collections/CompositeStringStringKey.cs b/src/Umbraco.Core/Collections/CompositeStringStringKey.cs index 01f94bf149..6fd25f6c12 100644 --- a/src/Umbraco.Core/Collections/CompositeStringStringKey.cs +++ b/src/Umbraco.Core/Collections/CompositeStringStringKey.cs @@ -1,41 +1,38 @@ -using System; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a composite key of (string, string) for fast dictionaries. +/// +/// +/// The string parts of the key are case-insensitive. +/// Null is NOT a valid value for neither parts. +/// +public struct CompositeStringStringKey : IEquatable { + private readonly string _key1; + private readonly string _key2; + /// - /// Represents a composite key of (string, string) for fast dictionaries. + /// Initializes a new instance of the struct. /// - /// - /// The string parts of the key are case-insensitive. - /// Null is NOT a valid value for neither parts. - /// - public struct CompositeStringStringKey : IEquatable + public CompositeStringStringKey(string? key1, string? key2) { - private readonly string _key1; - private readonly string _key2; - - /// - /// Initializes a new instance of the struct. - /// - public CompositeStringStringKey(string? key1, string? key2) - { - _key1 = key1?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(key1)); - _key2 = key2?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(key2)); - } - - public bool Equals(CompositeStringStringKey other) - => _key2 == other._key2 && _key1 == other._key1; - - public override bool Equals(object? obj) - => obj is CompositeStringStringKey other && _key2 == other._key2 && _key1 == other._key1; - - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1.GetHashCode(); - - public static bool operator ==(CompositeStringStringKey key1, CompositeStringStringKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; - - public static bool operator !=(CompositeStringStringKey key1, CompositeStringStringKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._key1; + _key1 = key1?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(key1)); + _key2 = key2?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(key2)); } + + public static bool operator ==(CompositeStringStringKey key1, CompositeStringStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; + + public static bool operator !=(CompositeStringStringKey key1, CompositeStringStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; + + public bool Equals(CompositeStringStringKey other) + => _key2 == other._key2 && _key1 == other._key1; + + public override bool Equals(object? obj) + => obj is CompositeStringStringKey other && _key2 == other._key2 && _key1 == other._key1; + + public override int GetHashCode() + => (_key2.GetHashCode() * 31) + _key1.GetHashCode(); } diff --git a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs index ea737e0522..ea9a5a496f 100644 --- a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs +++ b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs @@ -1,62 +1,52 @@ -using System; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a composite key of (Type, Type) for fast dictionaries. +/// +public struct CompositeTypeTypeKey : IEquatable { /// - /// Represents a composite key of (Type, Type) for fast dictionaries. + /// Initializes a new instance of the struct. /// - public struct CompositeTypeTypeKey : IEquatable + public CompositeTypeTypeKey(Type type1, Type type2) + : this() { - /// - /// Initializes a new instance of the struct. - /// - public CompositeTypeTypeKey(Type type1, Type type2) - : this() + Type1 = type1; + Type2 = type2; + } + + /// + /// Gets the first type. + /// + public Type Type1 { get; } + + /// + /// Gets the second type. + /// + public Type Type2 { get; } + + public static bool operator ==(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) => + key1.Type1 == key2.Type1 && key1.Type2 == key2.Type2; + + public static bool operator !=(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) => + key1.Type1 != key2.Type1 || key1.Type2 != key2.Type2; + + /// + public bool Equals(CompositeTypeTypeKey other) => Type1 == other.Type1 && Type2 == other.Type2; + + /// + public override bool Equals(object? obj) + { + CompositeTypeTypeKey other = obj is CompositeTypeTypeKey key ? key : default; + return Type1 == other.Type1 && Type2 == other.Type2; + } + + /// + public override int GetHashCode() + { + unchecked { - Type1 = type1; - Type2 = type2; - } - - /// - /// Gets the first type. - /// - public Type Type1 { get; } - - /// - /// Gets the second type. - /// - public Type Type2 { get; } - - /// - public bool Equals(CompositeTypeTypeKey other) - { - return Type1 == other.Type1 && Type2 == other.Type2; - } - - /// - public override bool Equals(object? obj) - { - var other = obj is CompositeTypeTypeKey key ? key : default; - return Type1 == other.Type1 && Type2 == other.Type2; - } - - public static bool operator ==(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) - { - return key1.Type1 == key2.Type1 && key1.Type2 == key2.Type2; - } - - public static bool operator !=(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2) - { - return key1.Type1 != key2.Type1 || key1.Type2 != key2.Type2; - } - - /// - public override int GetHashCode() - { - unchecked - { - return (Type1.GetHashCode() * 397) ^ Type2.GetHashCode(); - } + return (Type1.GetHashCode() * 397) ^ Type2.GetHashCode(); } } } diff --git a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs index f9c10e5607..a79c61a173 100644 --- a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs +++ b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs @@ -1,216 +1,277 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// A thread-safe representation of a . +/// Enumerating this collection is thread-safe and will only operate on a clone that is generated before returning the +/// enumerator. +/// +/// +[Serializable] +public class ConcurrentHashSet : ICollection { + private readonly HashSet _innerSet = new(); + private readonly ReaderWriterLockSlim _instanceLocker = new(LockRecursionPolicy.NoRecursion); + /// - /// A thread-safe representation of a . - /// Enumerating this collection is thread-safe and will only operate on a clone that is generated before returning the enumerator. + /// Gets the number of elements contained in the . /// - /// - [Serializable] - public class ConcurrentHashSet : ICollection + /// + /// The number of elements contained in the . + /// + /// 2 + public int Count { - private readonly HashSet _innerSet = new HashSet(); - private readonly ReaderWriterLockSlim _instanceLocker = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// A that can be used to iterate through the collection. - /// - /// 1 - public IEnumerator GetEnumerator() - { - return GetThreadSafeClone().GetEnumerator(); - } - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - /// 2 - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// Removes the first occurrence of a specific object from the . - /// - /// - /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . - /// - /// The object to remove from the .The is read-only. - public bool Remove(T item) - { - try - { - _instanceLocker.EnterWriteLock(); - return _innerSet.Remove(item); - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); - } - } - - - /// - /// Gets the number of elements contained in the . - /// - /// - /// The number of elements contained in the . - /// - /// 2 - public int Count - { - get - { - try - { - _instanceLocker.EnterReadLock(); - return _innerSet.Count; - } - finally - { - if (_instanceLocker.IsReadLockHeld) - _instanceLocker.ExitReadLock(); - } - - } - } - - /// - /// Gets a value indicating whether the is read-only. - /// - /// - /// true if the is read-only; otherwise, false. - /// - public bool IsReadOnly => false; - - /// - /// Adds an item to the . - /// - /// The object to add to the .The is read-only. - public void Add(T item) - { - try - { - _instanceLocker.EnterWriteLock(); - _innerSet.Add(item); - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); - } - } - - /// - /// Attempts to add an item to the collection - /// - /// - /// - public bool TryAdd(T item) - { - if (Contains(item)) return false; - try - { - _instanceLocker.EnterWriteLock(); - - //double check - if (_innerSet.Contains(item)) return false; - _innerSet.Add(item); - return true; - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); - } - } - - /// - /// Removes all items from the . - /// - /// The is read-only. - public void Clear() - { - try - { - _instanceLocker.EnterWriteLock(); - _innerSet.Clear(); - } - finally - { - if (_instanceLocker.IsWriteLockHeld) - _instanceLocker.ExitWriteLock(); - } - } - - /// - /// Determines whether the contains a specific value. - /// - /// - /// true if is found in the ; otherwise, false. - /// - /// The object to locate in the . - public bool Contains(T item) + get { try { _instanceLocker.EnterReadLock(); - return _innerSet.Contains(item); + return _innerSet.Count; } finally { if (_instanceLocker.IsReadLockHeld) + { _instanceLocker.ExitReadLock(); + } } } - - /// - /// Copies the elements of the to an , starting at a specified index. - /// - /// The one-dimensional that is the destination of the elements copied from the . The array must have zero-based indexing.The zero-based index in at which copying begins. is a null reference (Nothing in Visual Basic). is less than zero. is equal to or greater than the length of the -or- The number of elements in the source is greater than the available space from to the end of the destination . - public void CopyTo(T[] array, int index) - { - var clone = GetThreadSafeClone(); - clone.CopyTo(array, index); - } - - private HashSet GetThreadSafeClone() - { - HashSet? clone = null; - try - { - _instanceLocker.EnterReadLock(); - clone = new HashSet(_innerSet, _innerSet.Comparer); - } - finally - { - if (_instanceLocker.IsReadLockHeld) - _instanceLocker.ExitReadLock(); - } - return clone; - } - - /// - /// Copies the elements of the to an , starting at a particular index. - /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. The zero-based index in at which copying begins. is null. is less than zero. is multidimensional.-or- The number of elements in the source is greater than the available space from to the end of the destination . The type of the source cannot be cast automatically to the type of the destination . 2 - public void CopyTo(Array array, int index) - { - var clone = GetThreadSafeClone(); - Array.Copy(clone.ToArray(), 0, array, index, clone.Count); - } + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + /// 1 + public IEnumerator GetEnumerator() => GetThreadSafeClone().GetEnumerator(); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + /// 2 + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Removes the first occurrence of a specific object from the + /// . + /// + /// + /// true if was successfully removed from the + /// ; otherwise, false. This method also returns false if + /// is not found in the original . + /// + /// The object to remove from the . + /// + /// The is + /// read-only. + /// + public bool Remove(T item) + { + try + { + _instanceLocker.EnterWriteLock(); + return _innerSet.Remove(item); + } + finally + { + if (_instanceLocker.IsWriteLockHeld) + { + _instanceLocker.ExitWriteLock(); + } + } + } + + /// + /// Gets a value indicating whether the is read-only. + /// + /// + /// true if the is read-only; otherwise, false. + /// + public bool IsReadOnly => false; + + /// + /// Adds an item to the . + /// + /// The object to add to the . + /// + /// The is + /// read-only. + /// + public void Add(T item) + { + try + { + _instanceLocker.EnterWriteLock(); + _innerSet.Add(item); + } + finally + { + if (_instanceLocker.IsWriteLockHeld) + { + _instanceLocker.ExitWriteLock(); + } + } + } + + /// + /// Removes all items from the . + /// + /// + /// The is + /// read-only. + /// + public void Clear() + { + try + { + _instanceLocker.EnterWriteLock(); + _innerSet.Clear(); + } + finally + { + if (_instanceLocker.IsWriteLockHeld) + { + _instanceLocker.ExitWriteLock(); + } + } + } + + /// + /// Determines whether the contains a specific value. + /// + /// + /// true if is found in the ; + /// otherwise, false. + /// + /// The object to locate in the . + public bool Contains(T item) + { + try + { + _instanceLocker.EnterReadLock(); + return _innerSet.Contains(item); + } + finally + { + if (_instanceLocker.IsReadLockHeld) + { + _instanceLocker.ExitReadLock(); + } + } + } + + /// + /// Copies the elements of the to an + /// , starting at a specified index. + /// + /// + /// The one-dimensional that is the destination of the elements copied + /// from the . The array must have + /// zero-based indexing. + /// + /// The zero-based index in at which copying begins. + /// + /// is a null reference (Nothing in Visual + /// Basic). + /// + /// is less than zero. + /// + /// is equal to or greater than the length of the + /// -or- The number of elements in the source + /// is greater than the available space from + /// to the end of the destination . + /// + public void CopyTo(T[] array, int index) + { + HashSet clone = GetThreadSafeClone(); + clone.CopyTo(array, index); + } + + /// + /// Attempts to add an item to the collection + /// + /// + /// + public bool TryAdd(T item) + { + if (Contains(item)) + { + return false; + } + + try + { + _instanceLocker.EnterWriteLock(); + + // double check + if (_innerSet.Contains(item)) + { + return false; + } + + _innerSet.Add(item); + return true; + } + finally + { + if (_instanceLocker.IsWriteLockHeld) + { + _instanceLocker.ExitWriteLock(); + } + } + } + + /// + /// Copies the elements of the to an , + /// starting at a particular index. + /// + /// + /// The one-dimensional that is the destination of the elements copied + /// from . The must have zero-based + /// indexing. + /// + /// The zero-based index in at which copying begins. + /// is null. + /// is less than zero. + /// + /// is multidimensional.-or- The number of elements + /// in the source is greater than the available space from + /// to the end of the destination . + /// + /// + /// The type of the source + /// cannot be cast automatically to the type of the destination . + /// + /// 2 + public void CopyTo(Array array, int index) + { + HashSet clone = GetThreadSafeClone(); + Array.Copy(clone.ToArray(), 0, array, index, clone.Count); + } + + private HashSet GetThreadSafeClone() + { + HashSet? clone = null; + try + { + _instanceLocker.EnterReadLock(); + clone = new HashSet(_innerSet, _innerSet.Comparer); + } + finally + { + if (_instanceLocker.IsReadLockHeld) + { + _instanceLocker.ExitReadLock(); + } + } + + return clone; } } diff --git a/src/Umbraco.Core/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs index db7677153c..301795281c 100644 --- a/src/Umbraco.Core/Collections/DeepCloneableList.cs +++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs @@ -1,159 +1,137 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// A List that can be deep cloned with deep cloned elements and can reset the collection's items dirty flags +/// +/// +public class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty { + private readonly ListCloneBehavior _listCloneBehavior; + + public DeepCloneableList(ListCloneBehavior listCloneBehavior) => _listCloneBehavior = listCloneBehavior; + + public DeepCloneableList(IEnumerable collection, ListCloneBehavior listCloneBehavior) + : base(collection) => + _listCloneBehavior = listCloneBehavior; + /// - /// A List that can be deep cloned with deep cloned elements and can reset the collection's items dirty flags + /// Default behavior is CloneOnce /// - /// - public class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty + /// + public DeepCloneableList(IEnumerable collection) + : this(collection, ListCloneBehavior.CloneOnce) { - private readonly ListCloneBehavior _listCloneBehavior; - - public DeepCloneableList(ListCloneBehavior listCloneBehavior) - { - _listCloneBehavior = listCloneBehavior; - } - - public DeepCloneableList(IEnumerable collection, ListCloneBehavior listCloneBehavior) : base(collection) - { - _listCloneBehavior = listCloneBehavior; - } - - /// - /// Default behavior is CloneOnce - /// - /// - public DeepCloneableList(IEnumerable collection) - : this(collection, ListCloneBehavior.CloneOnce) - { - } - - /// - /// Creates a new list and adds each element as a deep cloned element if it is of type IDeepCloneable - /// - /// - public object DeepClone() - { - switch (_listCloneBehavior) - { - case ListCloneBehavior.CloneOnce: - //we are cloning once, so create a new list in none mode - // and deep clone all items into it - var newList = new DeepCloneableList(ListCloneBehavior.None); - foreach (var item in this) - { - if (item is IDeepCloneable dc) - { - newList.Add((T)dc.DeepClone()); - } - else - { - newList.Add(item); - } - } - return newList; - case ListCloneBehavior.None: - //we are in none mode, so just return a new list with the same items - return new DeepCloneableList(this, ListCloneBehavior.None); - case ListCloneBehavior.Always: - //always clone to new list - var newList2 = new DeepCloneableList(ListCloneBehavior.Always); - foreach (var item in this) - { - if (item is IDeepCloneable dc) - { - newList2.Add((T)dc.DeepClone()); - } - else - { - newList2.Add(item); - } - } - return newList2; - default: - throw new ArgumentOutOfRangeException(); - } - } - - #region IRememberBeingDirty - public bool IsDirty() - { - return this.OfType().Any(x => x.IsDirty()); - } - - public bool WasDirty() - { - return this.OfType().Any(x => x.WasDirty()); - } - - /// - /// Always return false, the list has no properties that can be dirty. - public bool IsPropertyDirty(string propName) - { - return false; - } - - /// - /// Always return false, the list has no properties that can be dirty. - public bool WasPropertyDirty(string propertyName) - { - return false; - } - - /// - /// Always return an empty enumerable, the list has no properties that can be dirty. - public IEnumerable GetDirtyProperties() - { - return Enumerable.Empty(); - } - - public void ResetDirtyProperties() - { - foreach (var dc in this.OfType()) - { - dc.ResetDirtyProperties(); - } - } - - public void DisableChangeTracking() - { - // noop - } - - public void EnableChangeTracking() - { - // noop - } - - public void ResetWereDirtyProperties() - { - foreach (var dc in this.OfType()) - { - dc.ResetWereDirtyProperties(); - } - } - - public void ResetDirtyProperties(bool rememberDirty) - { - foreach (var dc in this.OfType()) - { - dc.ResetDirtyProperties(rememberDirty); - } - } - - /// Always return an empty enumerable, the list has no properties that can be dirty. - public IEnumerable GetWereDirtyProperties() - { - return Enumerable.Empty(); - } - - public event PropertyChangedEventHandler? PropertyChanged; // noop - #endregion } + + public event PropertyChangedEventHandler? PropertyChanged; // noop + + /// + /// Creates a new list and adds each element as a deep cloned element if it is of type IDeepCloneable + /// + /// + public object DeepClone() + { + switch (_listCloneBehavior) + { + case ListCloneBehavior.CloneOnce: + // we are cloning once, so create a new list in none mode + // and deep clone all items into it + var newList = new DeepCloneableList(ListCloneBehavior.None); + foreach (T item in this) + { + if (item is IDeepCloneable dc) + { + newList.Add((T)dc.DeepClone()); + } + else + { + newList.Add(item); + } + } + + return newList; + case ListCloneBehavior.None: + // we are in none mode, so just return a new list with the same items + return new DeepCloneableList(this, ListCloneBehavior.None); + case ListCloneBehavior.Always: + // always clone to new list + var newList2 = new DeepCloneableList(ListCloneBehavior.Always); + foreach (T item in this) + { + if (item is IDeepCloneable dc) + { + newList2.Add((T)dc.DeepClone()); + } + else + { + newList2.Add(item); + } + } + + return newList2; + default: + throw new ArgumentOutOfRangeException(); + } + } + + #region IRememberBeingDirty + + public bool IsDirty() => this.OfType().Any(x => x.IsDirty()); + + public bool WasDirty() => this.OfType().Any(x => x.WasDirty()); + + /// + /// Always return false, the list has no properties that can be dirty. + public bool IsPropertyDirty(string propName) => false; + + /// + /// Always return false, the list has no properties that can be dirty. + public bool WasPropertyDirty(string propertyName) => false; + + /// + /// Always return an empty enumerable, the list has no properties that can be dirty. + public IEnumerable GetDirtyProperties() => Enumerable.Empty(); + + public void ResetDirtyProperties() + { + foreach (IRememberBeingDirty dc in this.OfType()) + { + dc.ResetDirtyProperties(); + } + } + + public void DisableChangeTracking() + { + // noop + } + + public void EnableChangeTracking() + { + // noop + } + + public void ResetWereDirtyProperties() + { + foreach (IRememberBeingDirty dc in this.OfType()) + { + dc.ResetWereDirtyProperties(); + } + } + + public void ResetDirtyProperties(bool rememberDirty) + { + foreach (IRememberBeingDirty dc in this.OfType()) + { + dc.ResetDirtyProperties(rememberDirty); + } + } + + /// Always return an empty enumerable, the list has no properties that can be dirty. + public IEnumerable GetWereDirtyProperties() => Enumerable.Empty(); + + #endregion } diff --git a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs index f4702f0124..579716456b 100644 --- a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs +++ b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs @@ -1,41 +1,42 @@ -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// Allows clearing all event handlers +/// +/// +public class EventClearingObservableCollection : ObservableCollection, INotifyCollectionChanged { - /// - /// Allows clearing all event handlers - /// - /// - public class EventClearingObservableCollection : ObservableCollection, INotifyCollectionChanged + // need to explicitly implement with event accessor syntax in order to override in order to to clear + // c# events are weird, they do not behave the same way as other c# things that are 'virtual', + // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252 + // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event + private NotifyCollectionChangedEventHandler? _changed; + + public EventClearingObservableCollection() { - public EventClearingObservableCollection() - { - } - - public EventClearingObservableCollection(List list) : base(list) - { - } - - public EventClearingObservableCollection(IEnumerable collection) : base(collection) - { - } - - // need to explicitly implement with event accessor syntax in order to override in order to to clear - // c# events are weird, they do not behave the same way as other c# things that are 'virtual', - // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252 - // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event - private NotifyCollectionChangedEventHandler? _changed; - event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged - { - add { _changed += value; } - remove { _changed -= value; } - } - - /// - /// Clears all event handlers for the event - /// - public void ClearCollectionChangedEvents() => _changed = null; } + + public EventClearingObservableCollection(List list) + : base(list) + { + } + + public EventClearingObservableCollection(IEnumerable collection) + : base(collection) + { + } + + event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged + { + add => _changed += value; + remove => _changed -= value; + } + + /// + /// Clears all event handlers for the event + /// + public void ClearCollectionChangedEvents() => _changed = null; } diff --git a/src/Umbraco.Core/Collections/ListCloneBehavior.cs b/src/Umbraco.Core/Collections/ListCloneBehavior.cs index 148141f783..4fc9edf3ae 100644 --- a/src/Umbraco.Core/Collections/ListCloneBehavior.cs +++ b/src/Umbraco.Core/Collections/ListCloneBehavior.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +public enum ListCloneBehavior { - public enum ListCloneBehavior - { - /// - /// When set, DeepClone will clone the items one time and the result list behavior will be None - /// - CloneOnce, + /// + /// When set, DeepClone will clone the items one time and the result list behavior will be None + /// + CloneOnce, - /// - /// When set, DeepClone will not clone any items - /// - None, + /// + /// When set, DeepClone will not clone any items + /// + None, - /// - /// When set, DeepClone will always clone all items - /// - Always - } + /// + /// When set, DeepClone will always clone all items + /// + Always, } diff --git a/src/Umbraco.Core/Collections/ObservableDictionary.cs b/src/Umbraco.Core/Collections/ObservableDictionary.cs index 1ea6a827c4..9e52b4dae7 100644 --- a/src/Umbraco.Core/Collections/ObservableDictionary.cs +++ b/src/Umbraco.Core/Collections/ObservableDictionary.cs @@ -1,257 +1,250 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.Linq; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// An ObservableDictionary +/// +/// +/// Assumes that the key will not change and is unique for each element in the collection. +/// Collection is not thread-safe, so calls should be made single-threaded. +/// +/// The type of elements contained in the BindableCollection +/// The type of the indexing key +public class ObservableDictionary : ObservableCollection, IReadOnlyDictionary, + IDictionary, INotifyCollectionChanged + where TKey : notnull { + // need to explicitly implement with event accessor syntax in order to override in order to to clear + // c# events are weird, they do not behave the same way as other c# things that are 'virtual', + // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252 + // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event + private NotifyCollectionChangedEventHandler? _changed; /// - /// An ObservableDictionary + /// Create new ObservableDictionary /// - /// - /// Assumes that the key will not change and is unique for each element in the collection. - /// Collection is not thread-safe, so calls should be made single-threaded. - /// - /// The type of elements contained in the BindableCollection - /// The type of the indexing key - public class ObservableDictionary : ObservableCollection, IReadOnlyDictionary, IDictionary, INotifyCollectionChanged - where TKey : notnull + /// Selector function to create key from value + /// The equality comparer to use when comparing keys, or null to use the default comparer. + public ObservableDictionary(Func keySelector, IEqualityComparer? equalityComparer = null) { - protected Dictionary Indecies { get; } - protected Func KeySelector { get; } + KeySelector = keySelector ?? throw new ArgumentException(nameof(keySelector)); + Indecies = new Dictionary(equalityComparer); + } - /// - /// Create new ObservableDictionary - /// - /// Selector function to create key from value - /// The equality comparer to use when comparing keys, or null to use the default comparer. - public ObservableDictionary(Func keySelector, IEqualityComparer? equalityComparer = null) + protected Dictionary Indecies { get; } + + protected Func KeySelector { get; } + + public bool Remove(TKey key) + { + if (!Indecies.ContainsKey(key)) { - KeySelector = keySelector ?? throw new ArgumentException(nameof(keySelector)); - Indecies = new Dictionary(equalityComparer); - } - - #region Protected Methods - - protected override void InsertItem(int index, TValue item) - { - var key = KeySelector(item); - if (Indecies.ContainsKey(key)) - throw new ArgumentException($"An element with the same key '{key}' already exists in the dictionary.", nameof(item)); - - if (index != Count) - { - foreach (var k in Indecies.Keys.Where(k => Indecies[k] >= index).ToList()) - { - Indecies[k]++; - } - } - - base.InsertItem(index, item); - Indecies[key] = index; - } - - protected override void ClearItems() - { - base.ClearItems(); - Indecies.Clear(); - } - - protected override void RemoveItem(int index) - { - var item = this[index]; - var key = KeySelector(item); - - base.RemoveItem(index); - - Indecies.Remove(key); - - foreach (var k in Indecies.Keys.Where(k => Indecies[k] > index).ToList()) - { - Indecies[k]--; - } - } - - #endregion - - // need to explicitly implement with event accessor syntax in order to override in order to to clear - // c# events are weird, they do not behave the same way as other c# things that are 'virtual', - // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252 - // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event - private NotifyCollectionChangedEventHandler? _changed; - event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged - { - add { _changed += value; } - remove { _changed -= value; } - } - - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => _changed = null; - - public bool ContainsKey(TKey key) - { - return Indecies.ContainsKey(key); - } - - /// - /// Gets or sets the element with the specified key. If setting a new value, new value must have same key. - /// - /// Key of element to replace - /// - public TValue this[TKey key] - { - - get => this[Indecies[key]]; - set - { - //confirm key matches - if (!KeySelector(value)!.Equals(key)) - throw new InvalidOperationException("Key of new value does not match."); - - if (!Indecies.ContainsKey(key)) - { - Add(value); - } - else - { - this[Indecies[key]] = value; - } - } - } - - /// - /// Replaces element at given key with new value. New value must have same key. - /// - /// Key of element to replace - /// New value - /// - /// - /// False if key not found - public bool Replace(TKey key, TValue value) - { - if (!Indecies.ContainsKey(key)) return false; - - //confirm key matches - if (!KeySelector(value)!.Equals(key)) - throw new InvalidOperationException("Key of new value does not match."); - - this[Indecies[key]] = value; - return true; - - } - - public void ReplaceAll(IEnumerable values) - { - if (values == null) throw new ArgumentNullException(nameof(values)); - - Clear(); - - foreach (var value in values) - { - Add(value); - } - } - - public bool Remove(TKey key) - { - if (!Indecies.ContainsKey(key)) return false; - - RemoveAt(Indecies[key]); - return true; - - } - - /// - /// Allows us to change the key of an item - /// - /// - /// - public void ChangeKey(TKey currentKey, TKey newKey) - { - if (!Indecies.ContainsKey(currentKey)) - { - throw new InvalidOperationException($"No item with the key '{currentKey}' was found in the dictionary."); - } - - if (ContainsKey(newKey)) - { - throw new ArgumentException($"An element with the same key '{newKey}' already exists in the dictionary.", nameof(newKey)); - } - - var currentIndex = Indecies[currentKey]; - - Indecies.Remove(currentKey); - Indecies.Add(newKey, currentIndex); - } - - #region IDictionary and IReadOnlyDictionary implementation - - public bool TryGetValue(TKey key, out TValue val) - { - if (Indecies.TryGetValue(key, out var index)) - { - val = this[index]; - return true; - } - val = default!; return false; } - /// - /// Returns all keys - /// - public IEnumerable Keys => Indecies.Keys; + RemoveAt(Indecies[key]); + return true; + } - /// - /// Returns all values - /// - public IEnumerable Values => base.Items; + event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged + { + add => _changed += value; + remove => _changed -= value; + } - ICollection IDictionary.Keys => Indecies.Keys; + public bool ContainsKey(TKey key) => Indecies.ContainsKey(key); - //this will never be used - ICollection IDictionary.Values => Values.ToList(); - - bool ICollection>.IsReadOnly => false; - - IEnumerator> IEnumerable>.GetEnumerator() + /// + /// Gets or sets the element with the specified key. If setting a new value, new value must have same key. + /// + /// Key of element to replace + /// + public TValue this[TKey key] + { + get => this[Indecies[key]]; + set { - foreach (var i in Values) + // confirm key matches + if (!KeySelector(value)!.Equals(key)) { - var key = KeySelector(i); - yield return new KeyValuePair(key, i); + throw new InvalidOperationException("Key of new value does not match."); + } + + if (!Indecies.ContainsKey(key)) + { + Add(value); + } + else + { + this[Indecies[key]] = value; } } + } - void IDictionary.Add(TKey key, TValue value) + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => _changed = null; + + /// + /// Replaces element at given key with new value. New value must have same key. + /// + /// Key of element to replace + /// New value + /// + /// False if key not found + public bool Replace(TKey key, TValue value) + { + if (!Indecies.ContainsKey(key)) + { + return false; + } + + // confirm key matches + if (!KeySelector(value)!.Equals(key)) + { + throw new InvalidOperationException("Key of new value does not match."); + } + + this[Indecies[key]] = value; + return true; + } + + public void ReplaceAll(IEnumerable values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + Clear(); + + foreach (TValue value in values) { Add(value); } - - void ICollection>.Add(KeyValuePair item) - { - Add(item.Value); - } - - bool ICollection>.Contains(KeyValuePair item) - { - return ContainsKey(item.Key); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - bool ICollection>.Remove(KeyValuePair item) - { - return Remove(item.Key); - } - - #endregion } + + /// + /// Allows us to change the key of an item + /// + /// + /// + public void ChangeKey(TKey currentKey, TKey newKey) + { + if (!Indecies.ContainsKey(currentKey)) + { + throw new InvalidOperationException($"No item with the key '{currentKey}' was found in the dictionary."); + } + + if (ContainsKey(newKey)) + { + throw new ArgumentException($"An element with the same key '{newKey}' already exists in the dictionary.", nameof(newKey)); + } + + var currentIndex = Indecies[currentKey]; + + Indecies.Remove(currentKey); + Indecies.Add(newKey, currentIndex); + } + + #region Protected Methods + + protected override void InsertItem(int index, TValue item) + { + TKey key = KeySelector(item); + if (Indecies.ContainsKey(key)) + { + throw new ArgumentException($"An element with the same key '{key}' already exists in the dictionary.", nameof(item)); + } + + if (index != Count) + { + foreach (TKey k in Indecies.Keys.Where(k => Indecies[k] >= index).ToList()) + { + Indecies[k]++; + } + } + + base.InsertItem(index, item); + Indecies[key] = index; + } + + protected override void ClearItems() + { + base.ClearItems(); + Indecies.Clear(); + } + + protected override void RemoveItem(int index) + { + TValue item = this[index]; + TKey key = KeySelector(item); + + base.RemoveItem(index); + + Indecies.Remove(key); + + foreach (TKey k in Indecies.Keys.Where(k => Indecies[k] > index).ToList()) + { + Indecies[k]--; + } + } + + #endregion + + #region IDictionary and IReadOnlyDictionary implementation + + public bool TryGetValue(TKey key, out TValue val) + { + if (Indecies.TryGetValue(key, out var index)) + { + val = this[index]; + return true; + } + + val = default!; + return false; + } + + /// + /// Returns all keys + /// + public IEnumerable Keys => Indecies.Keys; + + /// + /// Returns all values + /// + public IEnumerable Values => Items; + + ICollection IDictionary.Keys => Indecies.Keys; + + // this will never be used + ICollection IDictionary.Values => Values.ToList(); + + bool ICollection>.IsReadOnly => false; + + IEnumerator> IEnumerable>.GetEnumerator() + { + foreach (TValue i in Values) + { + TKey key = KeySelector(i); + yield return new KeyValuePair(key, i); + } + } + + void IDictionary.Add(TKey key, TValue value) => Add(value); + + void ICollection>.Add(KeyValuePair item) => Add(item.Value); + + bool ICollection>.Contains(KeyValuePair item) => ContainsKey(item.Key); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => + throw new NotImplementedException(); + + bool ICollection>.Remove(KeyValuePair item) => Remove(item.Key); + + #endregion } diff --git a/src/Umbraco.Core/Collections/OrderedHashSet.cs b/src/Umbraco.Core/Collections/OrderedHashSet.cs index e5a34083be..d23c81a7b2 100644 --- a/src/Umbraco.Core/Collections/OrderedHashSet.cs +++ b/src/Umbraco.Core/Collections/OrderedHashSet.cs @@ -1,50 +1,46 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// A custom collection similar to HashSet{T} which only contains unique items, however this collection keeps items in +/// order +/// and is customizable to keep the newest or oldest equatable item +/// +/// +public class OrderedHashSet : KeyedCollection + where T : notnull { - /// - /// A custom collection similar to HashSet{T} which only contains unique items, however this collection keeps items in order - /// and is customizable to keep the newest or oldest equatable item - /// - /// - public class OrderedHashSet : KeyedCollection where T : notnull + private readonly bool _keepOldest; + + public OrderedHashSet(bool keepOldest = true) => _keepOldest = keepOldest; + + protected override void InsertItem(int index, T item) { - private readonly bool _keepOldest; - - public OrderedHashSet(bool keepOldest = true) + if (Dictionary == null) { - _keepOldest = keepOldest; + base.InsertItem(index, item); } - - protected override void InsertItem(int index, T item) + else { - if (Dictionary == null) + var exists = Dictionary.ContainsKey(item); + + // if we want to keep the newest, then we need to remove the old item and add the new one + if (exists == false) { base.InsertItem(index, item); } - else + else if (_keepOldest == false) { - var exists = Dictionary.ContainsKey(item); + if (Remove(item)) + { + index--; + } - //if we want to keep the newest, then we need to remove the old item and add the new one - if (exists == false) - { - base.InsertItem(index, item); - } - else if(_keepOldest == false) - { - if (Remove(item)) - { - index--; - } - base.InsertItem(index, item); - } + base.InsertItem(index, item); } } - - protected override T GetKeyForItem(T item) - { - return item; - } } + + protected override T GetKeyForItem(T item) => item; } diff --git a/src/Umbraco.Core/Collections/StackQueue.cs b/src/Umbraco.Core/Collections/StackQueue.cs index 242766771d..2324eec892 100644 --- a/src/Umbraco.Core/Collections/StackQueue.cs +++ b/src/Umbraco.Core/Collections/StackQueue.cs @@ -1,45 +1,46 @@ -namespace Umbraco.Cms.Core.Collections +namespace Umbraco.Cms.Core.Collections; + +/// +/// Collection that can be both a queue and a stack. +/// +/// +public class StackQueue { - /// - /// Collection that can be both a queue and a stack. - /// - /// - public class StackQueue + private readonly LinkedList _linkedList = new(); + + public int Count => _linkedList.Count; + + public void Clear() => _linkedList.Clear(); + + public void Push(T? obj) => _linkedList.AddFirst(obj); + + public void Enqueue(T? obj) => _linkedList.AddFirst(obj); + + public T Pop() { - private readonly LinkedList _linkedList = new (); - - public int Count => _linkedList.Count; - - public void Clear() => _linkedList.Clear(); - - public void Push(T? obj) => _linkedList.AddFirst(obj); - - public void Enqueue(T? obj) => _linkedList.AddFirst(obj); - - public T Pop() + var obj = default(T); + if (_linkedList.First is not null) { - T? obj = default(T); - if (_linkedList.First is not null) - { - obj = _linkedList.First.Value; - } - _linkedList.RemoveFirst(); - return obj!; + obj = _linkedList.First.Value; } - public T Dequeue() - { - T? obj = default(T); - if (_linkedList.Last is not null) - { - obj = _linkedList.Last.Value; - } - _linkedList.RemoveLast(); - return obj!; - } - - public T? PeekStack() => _linkedList.First is not null ? _linkedList.First.Value : default; - - public T? PeekQueue() => _linkedList.Last is not null ? _linkedList.Last.Value : default; + _linkedList.RemoveFirst(); + return obj!; } + + public T Dequeue() + { + var obj = default(T); + if (_linkedList.Last is not null) + { + obj = _linkedList.Last.Value; + } + + _linkedList.RemoveLast(); + return obj!; + } + + public T? PeekStack() => _linkedList.First is not null ? _linkedList.First.Value : default; + + public T? PeekQueue() => _linkedList.Last is not null ? _linkedList.Last.Value : default; } diff --git a/src/Umbraco.Core/Collections/TopoGraph.cs b/src/Umbraco.Core/Collections/TopoGraph.cs index 11fd155684..fd2161c6d3 100644 --- a/src/Umbraco.Core/Collections/TopoGraph.cs +++ b/src/Umbraco.Core/Collections/TopoGraph.cs @@ -1,133 +1,143 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +public class TopoGraph { - public class TopoGraph + internal const string CycleDependencyError = "Cyclic dependency."; + internal const string MissingDependencyError = "Missing dependency."; + + public static Node CreateNode(TKey key, TItem item, IEnumerable dependencies) => + new(key, item, dependencies); + + public class Node { - internal const string CycleDependencyError = "Cyclic dependency."; - internal const string MissingDependencyError = "Missing dependency."; - - public class Node + public Node(TKey key, TItem item, IEnumerable dependencies) { - public Node(TKey key, TItem item, IEnumerable dependencies) - { - Key = key; - Item = item; - Dependencies = dependencies; - } - - public TKey Key { get; } - public TItem Item { get; } - public IEnumerable Dependencies { get; } + Key = key; + Item = item; + Dependencies = dependencies; } - public static Node CreateNode(TKey key, TItem item, IEnumerable dependencies) - => new Node(key, item, dependencies); + public TKey Key { get; } + + public TItem Item { get; } + + public IEnumerable Dependencies { get; } + } +} + +/// +/// Represents a generic DAG that can be topologically sorted. +/// +/// The type of the keys. +/// The type of the items. +public class TopoGraph : TopoGraph + where TKey : notnull +{ + private readonly Func?> _getDependencies; + private readonly Func _getKey; + private readonly Dictionary _items = new(); + + /// + /// Initializes a new instance of the class. + /// + /// A method that returns the key of an item. + /// A method that returns the dependency keys of an item. + public TopoGraph(Func getKey, Func?> getDependencies) + { + _getKey = getKey; + _getDependencies = getDependencies; } /// - /// Represents a generic DAG that can be topologically sorted. + /// Adds an item to the graph. /// - /// The type of the keys. - /// The type of the items. - public class TopoGraph : TopoGraph - where TKey : notnull + /// The item. + public void AddItem(TItem item) { - private readonly Func _getKey; - private readonly Func?> _getDependencies; - private readonly Dictionary _items = new Dictionary(); + TKey key = _getKey(item); + _items[key] = item; + } - /// - /// Initializes a new instance of the class. - /// - /// A method that returns the key of an item. - /// A method that returns the dependency keys of an item. - public TopoGraph(Func getKey, Func?> getDependencies) + /// + /// Adds items to the graph. + /// + /// The items. + public void AddItems(IEnumerable items) + { + foreach (TItem item in items) { - _getKey = getKey; - _getDependencies = getDependencies; + AddItem(item); + } + } + + /// + /// Gets the sorted items. + /// + /// A value indicating whether to throw on cycles, or just ignore the branch. + /// A value indicating whether to throw on missing dependency, or just ignore the dependency. + /// A value indicating whether to reverse the order. + /// The (topologically) sorted items. + public IEnumerable GetSortedItems(bool throwOnCycle = true, bool throwOnMissing = true, bool reverse = false) + { + var sorted = new TItem[_items.Count]; + var visited = new HashSet(); + var index = reverse ? _items.Count - 1 : 0; + var incr = reverse ? -1 : +1; + + foreach (TItem item in _items.Values) + { + Visit(item, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); } - /// - /// Adds an item to the graph. - /// - /// The item. - public void AddItem(TItem item) + return sorted; + } + + private static bool Contains(TItem[] items, TItem item, int start, int count) => + Array.IndexOf(items, item, start, count) >= 0; + + private void Visit(TItem item, ISet visited, TItem[] sorted, ref int index, int incr, bool throwOnCycle, bool throwOnMissing) + { + if (visited.Contains(item)) { - var key = _getKey(item); - _items[key] = item; - } - - /// - /// Adds items to the graph. - /// - /// The items. - public void AddItems(IEnumerable items) - { - foreach (var item in items) - AddItem(item); - } - - /// - /// Gets the sorted items. - /// - /// A value indicating whether to throw on cycles, or just ignore the branch. - /// A value indicating whether to throw on missing dependency, or just ignore the dependency. - /// A value indicating whether to reverse the order. - /// The (topologically) sorted items. - public IEnumerable GetSortedItems(bool throwOnCycle = true, bool throwOnMissing = true, bool reverse = false) - { - var sorted = new TItem[_items.Count]; - var visited = new HashSet(); - var index = reverse ? _items.Count - 1 : 0; - var incr = reverse ? -1 : +1; - - foreach (var item in _items.Values) - Visit(item, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); - - return sorted; - } - - private static bool Contains(TItem[] items, TItem item, int start, int count) - { - return Array.IndexOf(items, item, start, count) >= 0; - } - - private void Visit(TItem item, ISet visited, TItem[] sorted, ref int index, int incr, bool throwOnCycle, bool throwOnMissing) - { - if (visited.Contains(item)) + // visited but not sorted yet = cycle + var start = incr > 0 ? 0 : index; + var count = incr > 0 ? index : sorted.Length - index; + if (throwOnCycle && Contains(sorted, item, start, count) == false) { - // visited but not sorted yet = cycle - var start = incr > 0 ? 0 : index; - var count = incr > 0 ? index : sorted.Length - index; - if (throwOnCycle && Contains(sorted, item, start, count) == false) - throw new Exception(CycleDependencyError +": " + item); - return; + throw new Exception(CycleDependencyError + ": " + item); } - visited.Add(item); - - var keys = _getDependencies(item); - var dependencies = keys == null ? null : FindDependencies(keys, throwOnMissing); - - if (dependencies != null) - foreach (var dep in dependencies) - Visit(dep, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); - - sorted[index] = item; - index += incr; + return; } - private IEnumerable FindDependencies(IEnumerable keys, bool throwOnMissing) + visited.Add(item); + + IEnumerable? keys = _getDependencies(item); + IEnumerable? dependencies = keys == null ? null : FindDependencies(keys, throwOnMissing); + + if (dependencies != null) { - foreach (var key in keys) + foreach (TItem dep in dependencies) { - TItem? value; - if (_items.TryGetValue(key, out value)) - yield return value; - else if (throwOnMissing) - throw new Exception($"{MissingDependencyError} Error in type {typeof(TItem).Name}, with key {key}"); + Visit(dep, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing); + } + } + + sorted[index] = item; + index += incr; + } + + private IEnumerable FindDependencies(IEnumerable keys, bool throwOnMissing) + { + foreach (TKey key in keys) + { + if (_items.TryGetValue(key, out TItem? value)) + { + yield return value; + } + else if (throwOnMissing) + { + throw new Exception($"{MissingDependencyError} Error in type {typeof(TItem).Name}, with key {key}"); } } } diff --git a/src/Umbraco.Core/Collections/TypeList.cs b/src/Umbraco.Core/Collections/TypeList.cs index 96565a843c..ab51dd56b2 100644 --- a/src/Umbraco.Core/Collections/TypeList.cs +++ b/src/Umbraco.Core/Collections/TypeList.cs @@ -1,33 +1,24 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Collections +/// +/// Represents a list of types. +/// +/// Types in the list are, or derive from, or implement, the base type. +/// The base type. +public class TypeList { + private readonly List _list = new(); + /// - /// Represents a list of types. + /// Adds a type to the list. /// - /// Types in the list are, or derive from, or implement, the base type. - /// The base type. - public class TypeList - { - private readonly List _list = new List(); + /// The type to add. + public void Add() + where T : TBase => + _list.Add(typeof(T)); - /// - /// Adds a type to the list. - /// - /// The type to add. - public void Add() - where T : TBase - { - _list.Add(typeof(T)); - } - - /// - /// Determines whether a type is in the list. - /// - public bool Contains(Type type) - { - return _list.Contains(type); - } - } + /// + /// Determines whether a type is in the list. + /// + public bool Contains(Type type) => _list.Contains(type); } diff --git a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs index 1af9511fb7..ffacd89cff 100644 --- a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs +++ b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs @@ -1,34 +1,32 @@ -using System; using System.Collections; -using System.Collections.Generic; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a base class for builder collections. +/// +/// The type of the items. +public abstract class BuilderCollectionBase : IBuilderCollection { + private readonly LazyReadOnlyCollection _items; + + /// Initializes a new instance of the + /// + /// with items. + /// + /// The items. + public BuilderCollectionBase(Func> items) => _items = new LazyReadOnlyCollection(items); + + /// + public int Count => _items.Count; /// - /// Provides a base class for builder collections. + /// Gets an enumerator. /// - /// The type of the items. - public abstract class BuilderCollectionBase : IBuilderCollection - { - private readonly LazyReadOnlyCollection _items; + public IEnumerator GetEnumerator() => _items.GetEnumerator(); - /// Initializes a new instance of the with items. - /// - /// The items. - public BuilderCollectionBase(Func> items) => _items = new LazyReadOnlyCollection(items); - - /// - public int Count => _items.Count; - - /// - /// Gets an enumerator. - /// - public IEnumerator GetEnumerator() => ((IEnumerable)_items).GetEnumerator(); - - /// - /// Gets an enumerator. - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } + /// + /// Gets an enumerator. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs index 8b5913ab1d..8b1c33a610 100644 --- a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs @@ -1,160 +1,174 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a base class for collection builders. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +public abstract class CollectionBuilderBase : ICollectionBuilder + where TBuilder : CollectionBuilderBase + where TCollection : class, IBuilderCollection { + private readonly object _locker = new(); + private readonly List _types = new(); + private Type[]? _registeredTypes; + /// - /// Provides a base class for collection builders. + /// Gets the collection lifetime. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - public abstract class CollectionBuilderBase : ICollectionBuilder - where TBuilder : CollectionBuilderBase - where TCollection : class, IBuilderCollection + protected virtual ServiceLifetime CollectionLifetime => ServiceLifetime.Singleton; + + /// + public virtual void RegisterWith(IServiceCollection services) { - private readonly List _types = new List(); - private readonly object _locker = new object(); - private Type[]? _registeredTypes; + if (_registeredTypes != null) + { + throw new InvalidOperationException("This builder has already been registered."); + } - /// - /// Gets the internal list of types as an IEnumerable (immutable). - /// - public IEnumerable GetTypes() => _types; + // register the collection + services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, CollectionLifetime)); - /// - public virtual void RegisterWith(IServiceCollection services) + // register the types + RegisterTypes(services); + } + + /// + /// Creates a collection. + /// + /// A collection. + /// Creates a new collection each time it is invoked. + public virtual TCollection CreateCollection(IServiceProvider factory) + => factory.CreateInstance(CreateItemsFactory(factory)); + + /// + /// Gets the internal list of types as an IEnumerable (immutable). + /// + public IEnumerable GetTypes() => _types; + + /// + /// Gets a value indicating whether the collection contains a type. + /// + /// The type to look for. + /// A value indicating whether the collection contains the type. + /// + /// Some builder implementations may use this to expose a public Has{T}() method, + /// when it makes sense. Probably does not make sense for lazy builders, for example. + /// + public virtual bool Has() + where T : TItem => + _types.Contains(typeof(T)); + + /// + /// Gets a value indicating whether the collection contains a type. + /// + /// The type to look for. + /// A value indicating whether the collection contains the type. + /// + /// Some builder implementations may use this to expose a public Has{T}() method, + /// when it makes sense. Probably does not make sense for lazy builders, for example. + /// + public virtual bool Has(Type type) + { + EnsureType(type, "find"); + return _types.Contains(type); + } + + /// + /// Configures the internal list of types. + /// + /// The action to execute. + /// Throws if the types have already been registered. + protected void Configure(Action> action) + { + lock (_locker) { if (_registeredTypes != null) - throw new InvalidOperationException("This builder has already been registered."); - - // register the collection - services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, CollectionLifetime)); - - // register the types - RegisterTypes(services); - } - - /// - /// Gets the collection lifetime. - /// - protected virtual ServiceLifetime CollectionLifetime => ServiceLifetime.Singleton; - - /// - /// Configures the internal list of types. - /// - /// The action to execute. - /// Throws if the types have already been registered. - protected void Configure(Action> action) - { - lock (_locker) { - if (_registeredTypes != null) - throw new InvalidOperationException("Cannot configure a collection builder after it has been registered."); - action(_types); + throw new InvalidOperationException( + "Cannot configure a collection builder after it has been registered."); } - } - /// - /// Gets the types. - /// - /// The internal list of types. - /// The list of types to register. - /// Used by implementations to add types to the internal list, sort the list, etc. - protected virtual IEnumerable GetRegisteringTypes(IEnumerable types) - { - return types; - } - - private void RegisterTypes(IServiceCollection services) - { - lock (_locker) - { - if (_registeredTypes != null) - return; - - var types = GetRegisteringTypes(_types).ToArray(); - - // ensure they are safe - foreach (var type in types) - EnsureType(type, "register"); - - // register them - ensuring that each item is registered with the same lifetime as the collection. - // NOTE: Previously each one was not registered with the same lifetime which would mean that if there - // was a dependency on an individual item, it would resolve a brand new transient instance which isn't what - // we would expect to happen. The same item should be resolved from the container as the collection. - foreach (var type in types) - services.Add(new ServiceDescriptor(type, type, CollectionLifetime)); - - _registeredTypes = types; - } - } - - /// - /// Creates the collection items. - /// - /// The collection items. - protected virtual IEnumerable CreateItems(IServiceProvider factory) - { - if (_registeredTypes == null) - throw new InvalidOperationException("Cannot create items before the collection builder has been registered."); - - return _registeredTypes // respect order - .Select(x => CreateItem(factory, x)) - .ToArray(); // safe - } - - /// - /// Creates a collection item. - /// - protected virtual TItem CreateItem(IServiceProvider factory, Type itemType) - => (TItem)factory.GetRequiredService(itemType); - - /// - /// Creates a collection. - /// - /// A collection. - /// Creates a new collection each time it is invoked. - public virtual TCollection CreateCollection(IServiceProvider factory) - => factory.CreateInstance(CreateItemsFactory(factory)); - - // used to resolve a Func> parameter - private Func> CreateItemsFactory(IServiceProvider factory) => () => CreateItems(factory); - - protected Type EnsureType(Type type, string action) - { - if (typeof(TItem).IsAssignableFrom(type) == false) - throw new InvalidOperationException($"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TItem).FullName}."); - return type; - } - - /// - /// Gets a value indicating whether the collection contains a type. - /// - /// The type to look for. - /// A value indicating whether the collection contains the type. - /// Some builder implementations may use this to expose a public Has{T}() method, - /// when it makes sense. Probably does not make sense for lazy builders, for example. - public virtual bool Has() - where T : TItem - { - return _types.Contains(typeof(T)); - } - - /// - /// Gets a value indicating whether the collection contains a type. - /// - /// The type to look for. - /// A value indicating whether the collection contains the type. - /// Some builder implementations may use this to expose a public Has{T}() method, - /// when it makes sense. Probably does not make sense for lazy builders, for example. - public virtual bool Has(Type type) - { - EnsureType(type, "find"); - return _types.Contains(type); + action(_types); } } + + /// + /// Gets the types. + /// + /// The internal list of types. + /// The list of types to register. + /// Used by implementations to add types to the internal list, sort the list, etc. + protected virtual IEnumerable GetRegisteringTypes(IEnumerable types) => types; + + /// + /// Creates the collection items. + /// + /// The collection items. + protected virtual IEnumerable CreateItems(IServiceProvider factory) + { + if (_registeredTypes == null) + { + throw new InvalidOperationException( + "Cannot create items before the collection builder has been registered."); + } + + return _registeredTypes // respect order + .Select(x => CreateItem(factory, x)) + .ToArray(); // safe + } + + /// + /// Creates a collection item. + /// + protected virtual TItem CreateItem(IServiceProvider factory, Type itemType) + => (TItem)factory.GetRequiredService(itemType); + + protected Type EnsureType(Type type, string action) + { + if (typeof(TItem).IsAssignableFrom(type) == false) + { + throw new InvalidOperationException( + $"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TItem).FullName}."); + } + + return type; + } + + private void RegisterTypes(IServiceCollection services) + { + lock (_locker) + { + if (_registeredTypes != null) + { + return; + } + + Type[] types = GetRegisteringTypes(_types).ToArray(); + + // ensure they are safe + foreach (Type type in types) + { + EnsureType(type, "register"); + } + + // register them - ensuring that each item is registered with the same lifetime as the collection. + // NOTE: Previously each one was not registered with the same lifetime which would mean that if there + // was a dependency on an individual item, it would resolve a brand new transient instance which isn't what + // we would expect to happen. The same item should be resolved from the container as the collection. + foreach (Type type in types) + { + services.Add(new ServiceDescriptor(type, type, CollectionLifetime)); + } + + _registeredTypes = types; + } + } + + // used to resolve a Func> parameter + private Func> CreateItemsFactory(IServiceProvider factory) => () => CreateItems(factory); } diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs index c39dd503e0..d64de626d0 100644 --- a/src/Umbraco.Core/Composing/ComponentCollection.cs +++ b/src/Umbraco.Core/Composing/ComponentCollection.cs @@ -1,62 +1,67 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents the collection of implementations. +/// +public class ComponentCollection : BuilderCollectionBase { - /// - /// Represents the collection of implementations. - /// - public class ComponentCollection : BuilderCollectionBase + private const int LogThresholdMilliseconds = 100; + private readonly ILogger _logger; + + private readonly IProfilingLogger _profilingLogger; + + public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) + : base(items) { - private const int LogThresholdMilliseconds = 100; + _profilingLogger = profilingLogger; + _logger = logger; + } - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - - public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) - : base(items) + public void Initialize() + { + using (_profilingLogger.DebugDuration( + $"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) { - _profilingLogger = profilingLogger; - _logger = logger; - } - - public void Initialize() - { - using (_profilingLogger.DebugDuration($"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) + foreach (IComponent component in this) { - foreach (var component in this) + Type componentType = component.GetType(); + using (_profilingLogger.DebugDuration( + $"Initializing {componentType.FullName}.", + $"Initialized {componentType.FullName}.", + thresholdMilliseconds: LogThresholdMilliseconds)) { - var componentType = component.GetType(); - using (_profilingLogger.DebugDuration($"Initializing {componentType.FullName}.", $"Initialized {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) - { - component.Initialize(); - } + component.Initialize(); } } } + } - public void Terminate() + public void Terminate() + { + using (_profilingLogger.DebugDuration( + $"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) { - using (_profilingLogger.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) + // terminate components in reverse order + foreach (IComponent component in this.Reverse()) { - foreach (var component in this.Reverse()) // terminate components in reverse order + Type componentType = component.GetType(); + using (_profilingLogger.DebugDuration( + $"Terminating {componentType.FullName}.", + $"Terminated {componentType.FullName}.", + thresholdMilliseconds: LogThresholdMilliseconds)) { - var componentType = component.GetType(); - using (_profilingLogger.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) + try { - try - { - component.Terminate(); - component.DisposeIfDisposable(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while terminating component {ComponentType}.", componentType.FullName); - } + component.Terminate(); + component.DisposeIfDisposable(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while terminating component {ComponentType}.", componentType.FullName); } } } diff --git a/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs b/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs index 1e36c4e8e9..b77dfde819 100644 --- a/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs +++ b/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs @@ -1,40 +1,40 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Builds a . +/// +public class + ComponentCollectionBuilder : OrderedCollectionBuilderBase { - /// - /// Builds a . - /// - public class ComponentCollectionBuilder : OrderedCollectionBuilderBase + private const int LogThresholdMilliseconds = 100; + + protected override ComponentCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) { - private const int LogThresholdMilliseconds = 100; + IProfilingLogger logger = factory.GetRequiredService(); - public ComponentCollectionBuilder() - { } - - protected override ComponentCollectionBuilder This => this; - - protected override IEnumerable CreateItems(IServiceProvider factory) + using (logger.DebugDuration( + $"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) { - var logger = factory.GetRequiredService(); - - using (logger.DebugDuration($"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) - { - return base.CreateItems(factory); - } + return base.CreateItems(factory); } + } - protected override IComponent CreateItem(IServiceProvider factory, Type itemType) + protected override IComponent CreateItem(IServiceProvider factory, Type itemType) + { + IProfilingLogger logger = factory.GetRequiredService(); + + using (logger.DebugDuration( + $"Creating {itemType.FullName}.", + $"Created {itemType.FullName}.", + thresholdMilliseconds: LogThresholdMilliseconds)) { - var logger = factory.GetRequiredService(); - - using (logger.DebugDuration($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) - { - return base.CreateItem(factory, itemType); - } + return base.CreateItem(factory, itemType); } } } diff --git a/src/Umbraco.Core/Composing/ComponentComposer.cs b/src/Umbraco.Core/Composing/ComponentComposer.cs index c1d921df03..2a9641e64b 100644 --- a/src/Umbraco.Core/Composing/ComponentComposer.cs +++ b/src/Umbraco.Core/Composing/ComponentComposer.cs @@ -1,22 +1,18 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a base class for composers which compose a component. +/// +/// The type of the component +public abstract class ComponentComposer : IComposer + where TComponent : IComponent { - /// - /// Provides a base class for composers which compose a component. - /// - /// The type of the component - public abstract class ComponentComposer : IComposer - where TComponent : IComponent - { - /// - public virtual void Compose(IUmbracoBuilder builder) - { - builder.Components().Append(); - } + /// + public virtual void Compose(IUmbracoBuilder builder) => builder.Components().Append(); - // note: thanks to this class, a component that does not compose anything can be - // registered with one line: - // public class MyComponentComposer : ComponentComposer { } - } + // note: thanks to this class, a component that does not compose anything can be + // registered with one line: + // public class MyComponentComposer : ComponentComposer { } } diff --git a/src/Umbraco.Core/Composing/ComposeAfterAttribute.cs b/src/Umbraco.Core/Composing/ComposeAfterAttribute.cs index c12ddbcd3e..bd3567595d 100644 --- a/src/Umbraco.Core/Composing/ComposeAfterAttribute.cs +++ b/src/Umbraco.Core/Composing/ComposeAfterAttribute.cs @@ -1,59 +1,66 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a composer requires another composer. +/// +/// +/// +/// This attribute is *not* inherited. This means that a composer class inheriting from +/// another composer class does *not* inherit its requirements. However, the runtime checks +/// the *interfaces* of every composer for their requirements, so requirements declared on +/// interfaces are inherited by every composer class implementing the interface. +/// +/// +/// When targeting a class, indicates a dependency on the composer which must be enabled, +/// unless the requirement has explicitly been declared as weak (and then, only if the composer +/// is enabled). +/// +/// +/// When targeting an interface, indicates a dependency on enabled composers implementing +/// the interface. It could be no composer at all, unless the requirement has explicitly been +/// declared as strong (and at least one composer must be enabled). +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] +public sealed class ComposeAfterAttribute : Attribute { /// - /// Indicates that a composer requires another composer. + /// Initializes a new instance of the class. + /// + /// The type of the required composer. + public ComposeAfterAttribute(Type requiredType) + { + if (typeof(IComposer).IsAssignableFrom(requiredType) == false) + { + throw new ArgumentException( + $"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); + } + + RequiredType = requiredType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The type of the required composer. + /// A value indicating whether the requirement is weak. + public ComposeAfterAttribute(Type requiredType, bool weak) + : this(requiredType) => + Weak = weak; + + /// + /// Gets the required type. + /// + public Type RequiredType { get; } + + /// + /// Gets a value indicating whether the requirement is weak. /// /// - /// This attribute is *not* inherited. This means that a composer class inheriting from - /// another composer class does *not* inherit its requirements. However, the runtime checks - /// the *interfaces* of every composer for their requirements, so requirements declared on - /// interfaces are inherited by every composer class implementing the interface. - /// When targeting a class, indicates a dependency on the composer which must be enabled, - /// unless the requirement has explicitly been declared as weak (and then, only if the composer - /// is enabled). - /// When targeting an interface, indicates a dependency on enabled composers implementing - /// the interface. It could be no composer at all, unless the requirement has explicitly been - /// declared as strong (and at least one composer must be enabled). + /// Returns true if the requirement is weak (requires the other composer if it + /// is enabled), false if the requirement is strong (requires the other composer to be + /// enabled), and null if unspecified, in which case it is strong for classes and weak for + /// interfaces. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] - public sealed class ComposeAfterAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The type of the required composer. - public ComposeAfterAttribute(Type requiredType) - { - if (typeof(IComposer).IsAssignableFrom(requiredType) == false) - throw new ArgumentException($"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); - RequiredType = requiredType; - } - - /// - /// Initializes a new instance of the class. - /// - /// The type of the required composer. - /// A value indicating whether the requirement is weak. - public ComposeAfterAttribute(Type requiredType, bool weak) - : this(requiredType) - { - Weak = weak; - } - - /// - /// Gets the required type. - /// - public Type RequiredType { get; } - - /// - /// Gets a value indicating whether the requirement is weak. - /// - /// Returns true if the requirement is weak (requires the other composer if it - /// is enabled), false if the requirement is strong (requires the other composer to be - /// enabled), and null if unspecified, in which case it is strong for classes and weak for - /// interfaces. - public bool? Weak { get; } - } + public bool? Weak { get; } } diff --git a/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs b/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs index 382772de8d..c41f1e5074 100644 --- a/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs +++ b/src/Umbraco.Core/Composing/ComposeBeforeAttribute.cs @@ -1,40 +1,46 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a component is required by another composer. +/// +/// +/// +/// This attribute is *not* inherited. This means that a composer class inheriting from +/// another composer class does *not* inherit its requirements. However, the runtime checks +/// the *interfaces* of every composer for their requirements, so requirements declared on +/// interfaces are inherited by every composer class implementing the interface. +/// +/// +/// When targeting a class, indicates a dependency on the composer which must be enabled, +/// unless the requirement has explicitly been declared as weak (and then, only if the composer +/// is enabled). +/// +/// +/// When targeting an interface, indicates a dependency on enabled composers implementing +/// the interface. It could be no composer at all, unless the requirement has explicitly been +/// declared as strong (and at least one composer must be enabled). +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] +public sealed class ComposeBeforeAttribute : Attribute { /// - /// Indicates that a component is required by another composer. + /// Initializes a new instance of the class. /// - /// - /// This attribute is *not* inherited. This means that a composer class inheriting from - /// another composer class does *not* inherit its requirements. However, the runtime checks - /// the *interfaces* of every composer for their requirements, so requirements declared on - /// interfaces are inherited by every composer class implementing the interface. - /// When targeting a class, indicates a dependency on the composer which must be enabled, - /// unless the requirement has explicitly been declared as weak (and then, only if the composer - /// is enabled). - /// When targeting an interface, indicates a dependency on enabled composers implementing - /// the interface. It could be no composer at all, unless the requirement has explicitly been - /// declared as strong (and at least one composer must be enabled). - /// - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] - public sealed class ComposeBeforeAttribute : Attribute + /// The type of the required composer. + public ComposeBeforeAttribute(Type requiringType) { - /// - /// Initializes a new instance of the class. - /// - /// The type of the required composer. - public ComposeBeforeAttribute(Type requiringType) + if (typeof(IComposer).IsAssignableFrom(requiringType) == false) { - if (typeof(IComposer).IsAssignableFrom(requiringType) == false) - throw new ArgumentException($"Type {requiringType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); - RequiringType = requiringType; + throw new ArgumentException( + $"Type {requiringType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}."); } - /// - /// Gets the required type. - /// - public Type RequiringType { get; } + RequiringType = requiringType; } + + /// + /// Gets the required type. + /// + public Type RequiringType { get; } } diff --git a/src/Umbraco.Core/Composing/ComposerGraph.cs b/src/Umbraco.Core/Composing/ComposerGraph.cs index 510d59b374..3c602b0ad9 100644 --- a/src/Umbraco.Core/Composing/ComposerGraph.cs +++ b/src/Umbraco.Core/Composing/ComposerGraph.cs @@ -1,351 +1,421 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Text; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +// note: this class is NOT thread-safe in any way + +/// +/// Handles the composers. +/// +internal class ComposerGraph { - // note: this class is NOT thread-safe in any way + private readonly IUmbracoBuilder _builder; + private readonly IEnumerable _composerTypes; + private readonly IEnumerable _enableDisableAttributes; + private readonly ILogger _logger; /// - /// Handles the composers. + /// Initializes a new instance of the class. /// - internal class ComposerGraph + /// The composition. + /// The types. + /// + /// The and/or + /// attributes. + /// + /// The logger. + /// + /// composition + /// or + /// composerTypes + /// or + /// enableDisableAttributes + /// or + /// logger + /// + public ComposerGraph(IUmbracoBuilder builder, IEnumerable composerTypes, IEnumerable enableDisableAttributes, ILogger logger) { - private readonly IUmbracoBuilder _builder; - private readonly ILogger _logger; - private readonly IEnumerable _composerTypes; - private readonly IEnumerable _enableDisableAttributes; + _builder = builder ?? throw new ArgumentNullException(nameof(builder)); + _composerTypes = composerTypes ?? throw new ArgumentNullException(nameof(composerTypes)); + _enableDisableAttributes = + enableDisableAttributes ?? throw new ArgumentNullException(nameof(enableDisableAttributes)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } - /// - /// Initializes a new instance of the class. - /// - /// The composition. - /// The types. - /// The and/or attributes. - /// The logger. - /// composition - /// or - /// composerTypes - /// or - /// enableDisableAttributes - /// or - /// logger - public ComposerGraph(IUmbracoBuilder builder, IEnumerable composerTypes, IEnumerable enableDisableAttributes, ILogger logger) + /// + /// Instantiates and composes the composers. + /// + public void Compose() + { + // make sure it is there + _builder.WithCollectionBuilder(); + + IEnumerable orderedComposerTypes = PrepareComposerTypes(); + + foreach (IComposer composer in InstantiateComposers(orderedComposerTypes)) { - _builder = builder ?? throw new ArgumentNullException(nameof(builder)); - _composerTypes = composerTypes ?? throw new ArgumentNullException(nameof(composerTypes)); - _enableDisableAttributes = enableDisableAttributes ?? throw new ArgumentNullException(nameof(enableDisableAttributes)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + composer.Compose(_builder); + } + } + + internal static string GetComposersReport(Dictionary?> requirements) + { + var text = new StringBuilder(); + text.AppendLine("Composers & Dependencies:"); + text.AppendLine(" < compose before"); + text.AppendLine(" > compose after"); + text.AppendLine(" : implements"); + text.AppendLine(" = depends"); + text.AppendLine(); + + bool HasReq(IEnumerable types, Type type) + { + return types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); } - private class EnableInfo + foreach (KeyValuePair?> kvp in requirements) { - public bool Enabled { get; set; } - public int Weight { get; set; } = -1; - } + Type type = kvp.Key; - /// - /// Instantiates and composes the composers. - /// - public void Compose() - { - // make sure it is there - _builder.WithCollectionBuilder(); - - IEnumerable orderedComposerTypes = PrepareComposerTypes(); - - foreach (IComposer composer in InstantiateComposers(orderedComposerTypes)) + text.AppendLine(type.FullName); + foreach (ComposeAfterAttribute attribute in type.GetCustomAttributes()) { - composer.Compose(_builder); + var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); + text.AppendLine(" > " + attribute.RequiredType + + (weak ? " (weak" : " (strong") + + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); } - } - internal IEnumerable PrepareComposerTypes() - { - var requirements = GetRequirements(); - - // only for debugging, this is verbose - //_logger.Debug(GetComposersReport(requirements)); - - var sortedComposerTypes = SortComposers(requirements); - - // bit verbose but should help for troubleshooting - //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; - _logger.LogDebug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); - - return sortedComposerTypes; - } - - internal Dictionary?> GetRequirements(bool throwOnMissing = true) - { - // create a list, remove those that cannot be enabled due to runtime level - var composerTypeList = _composerTypes.ToList(); - - // enable or disable composers - EnableDisableComposers(_enableDisableAttributes, composerTypeList); - - void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) - where TAttribute : Attribute + foreach (ComposeBeforeAttribute attribute in type.GetCustomAttributes()) { - foreach (var attribute in type.GetCustomAttributes()) - { - var typeInAttribute = getTypeInAttribute(attribute); - if (typeInAttribute != null && // if the attribute references a type ... - typeInAttribute.IsInterface && // ... which is an interface ... - typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... - !iset.Contains(typeInAttribute)) // ... which is not already in the list - { - // add it to the new list - iset.Add(typeInAttribute); - set2.Add(typeInAttribute); + text.AppendLine(" < " + attribute.RequiringType); + } - // add all its interfaces implementing IComposer - foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x))) - { - iset.Add(i); - set2.Add(i); - } - } + foreach (Type i in type.GetInterfaces()) + { + text.AppendLine(" : " + i.FullName); + } + + if (kvp.Value != null) + { + foreach (Type t in kvp.Value) + { + text.AppendLine(" = " + t); } } - // gather interfaces too - var interfaces = new HashSet(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); - composerTypeList.AddRange(interfaces); - var list1 = composerTypeList; - while (list1.Count > 0) - { - var list2 = new List(); - foreach (var t in list1) - { - GatherInterfaces(t, a => a.RequiredType, interfaces, list2); - GatherInterfaces(t, a => a.RequiringType, interfaces, list2); - } - composerTypeList.AddRange(list2); - list1 = list2; - } - - // sort the composers according to their dependencies - var requirements = new Dictionary?>(); - foreach (var type in composerTypeList) - requirements[type] = null; - foreach (var type in composerTypeList) - { - GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); - GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); - } - - return requirements; - } - - internal IEnumerable SortComposers(Dictionary?> requirements) - { - // sort composers - var graph = new TopoGraph?>>(kvp => kvp.Key, kvp => kvp.Value); - graph.AddItems(requirements); - List sortedComposerTypes; - try - { - sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); - } - catch (Exception e) - { - // in case of an error, force-dump everything to log - _logger.LogInformation("Composer Report:\r\n{ComposerReport}", GetComposersReport(requirements)); - _logger.LogError(e, "Failed to sort composers."); - throw; - } - - return sortedComposerTypes; - } - - internal static string GetComposersReport(Dictionary?> requirements) - { - var text = new StringBuilder(); - text.AppendLine("Composers & Dependencies:"); - text.AppendLine(" < compose before"); - text.AppendLine(" > compose after"); - text.AppendLine(" : implements"); - text.AppendLine(" = depends"); text.AppendLine(); - - bool HasReq(IEnumerable types, Type type) - => types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); - - foreach (var kvp in requirements) - { - var type = kvp.Key; - - text.AppendLine(type.FullName); - foreach (var attribute in type.GetCustomAttributes()) - { - var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); - text.AppendLine(" > " + attribute.RequiredType + - (weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); - } - foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" < " + attribute.RequiringType); - foreach (var i in type.GetInterfaces()) - text.AppendLine(" : " + i.FullName); - if (kvp.Value != null) - foreach (var t in kvp.Value) - text.AppendLine(" = " + t); - text.AppendLine(); - } - text.AppendLine("/"); - text.AppendLine(); - return text.ToString(); } - private static void EnableDisableComposers(IEnumerable enableDisableAttributes, ICollection types) + text.AppendLine("/"); + text.AppendLine(); + return text.ToString(); + } + + internal IEnumerable PrepareComposerTypes() + { + Dictionary?> requirements = GetRequirements(); + + // only for debugging, this is verbose + // _logger.Debug(GetComposersReport(requirements)); + IEnumerable sortedComposerTypes = SortComposers(requirements); + + // bit verbose but should help for troubleshooting + // var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; + _logger.LogDebug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); + + return sortedComposerTypes; + } + + internal Dictionary?> GetRequirements(bool throwOnMissing = true) + { + // create a list, remove those that cannot be enabled due to runtime level + var composerTypeList = _composerTypes.ToList(); + + // enable or disable composers + EnableDisableComposers(_enableDisableAttributes, composerTypeList); + + static void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) + where TAttribute : Attribute { - var enabled = new Dictionary(); - - // process the enable/disable attributes - // these two attributes are *not* inherited and apply to *classes* only (not interfaces). - // remote declarations (when a composer enables/disables *another* composer) - // have priority over local declarations (when a composer disables itself) so that - // ppl can enable composers that, by default, are disabled. - // what happens in case of conflicting remote declarations is unspecified. more - // precisely, the last declaration to be processed wins, but the order of the - // declarations depends on the type finder and is unspecified. - - void UpdateEnableInfo(Type composerType, int weight2, Dictionary enabled2, bool value) + foreach (TAttribute attribute in type.GetCustomAttributes()) { - if (enabled.TryGetValue(composerType, out var enableInfo) == false) enableInfo = enabled2[composerType] = new EnableInfo(); - if (enableInfo.Weight > weight2) return; - - enableInfo.Enabled = value; - enableInfo.Weight = weight2; - } - - foreach (var attr in enableDisableAttributes.OfType()) - { - var type = attr.EnabledType; - UpdateEnableInfo(type, 2, enabled, true); - } - - foreach (var attr in enableDisableAttributes.OfType()) - { - var type = attr.DisabledType; - UpdateEnableInfo(type, 2, enabled, false); - } - - foreach (var composerType in types) - { - foreach (var attr in composerType.GetCustomAttributes()) + Type typeInAttribute = getTypeInAttribute(attribute); + if (typeInAttribute != null && // if the attribute references a type ... + typeInAttribute.IsInterface && // ... which is an interface ... + typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... + !iset.Contains(typeInAttribute)) // ... which is not already in the list { - var type = attr.EnabledType ?? composerType; - var weight = type == composerType ? 1 : 3; - UpdateEnableInfo(type, weight, enabled, true); - } + // add it to the new list + iset.Add(typeInAttribute); + set2.Add(typeInAttribute); - foreach (var attr in composerType.GetCustomAttributes()) - { - var type = attr.DisabledType ?? composerType; - var weight = type == composerType ? 1 : 3; - UpdateEnableInfo(type, weight, enabled, false); - } - } - - // remove composers that end up being disabled - foreach (var kvp in enabled.Where(x => x.Value.Enabled == false)) - types.Remove(kvp.Key); - } - - private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary?> requirements, bool throwOnMissing = true) - { - // get 'require' attributes - // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var afterAttributes = type - .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces - .Concat(type.GetCustomAttributes()); // those marking the composer - - // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. - foreach (var attr in afterAttributes) - { - if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) - - // requiring an interface = require any enabled composer implementing that interface - // unless strong, and then require at least one enabled composer implementing that interface - if (attr.RequiredType.IsInterface) - { - var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList(); - if (implems.Count > 0) + // add all its interfaces implementing IComposer + foreach (Type i in typeInAttribute.GetInterfaces() + .Where(x => typeof(IComposer).IsAssignableFrom(x))) { - if (requirements[type] == null) requirements[type] = new List(); - requirements[type]!.AddRange(implems); - } - else if (attr.Weak == false && throwOnMissing) // if explicitly set to !weak, is strong, else is weak - throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); - } - // requiring a class = require that the composer is enabled - // unless weak, and then requires it if it is enabled - else - { - if (types.Contains(attr.RequiredType)) - { - if (requirements[type] == null) requirements[type] = new List(); - requirements[type]!.Add(attr.RequiredType); - } - else if (attr.Weak != true && throwOnMissing) // if not explicitly set to weak, is strong - throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); - } - } - } - - private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary?> requirements) - { - // get 'required' attributes - // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var beforeAttributes = type - .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces - .Concat(type.GetCustomAttributes()); // those marking the composer - - foreach (var attr in beforeAttributes) - { - if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) - - // required by an interface = by any enabled composer implementing this that interface - if (attr.RequiringType.IsInterface) - { - var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList(); - foreach (var implem in implems) - { - if (requirements[implem] == null) requirements[implem] = new List(); - requirements[implem]!.Add(type); - } - } - // required by a class - else - { - if (types.Contains(attr.RequiringType)) - { - if (requirements[attr.RequiringType] == null) requirements[attr.RequiringType] = new List(); - requirements[attr.RequiringType]!.Add(type); + iset.Add(i); + set2.Add(i); } } } } - private static IEnumerable InstantiateComposers(IEnumerable types) + // gather interfaces too + var interfaces = new HashSet(composerTypeList.SelectMany(x => + x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); + composerTypeList.AddRange(interfaces); + List list1 = composerTypeList; + while (list1.Count > 0) { - foreach (Type type in types) + var list2 = new List(); + foreach (Type t in list1) { - ConstructorInfo? ctor = type.GetConstructor(Array.Empty()); + GatherInterfaces(t, a => a.RequiredType, interfaces, list2); + GatherInterfaces(t, a => a.RequiringType, interfaces, list2); + } - if (ctor == null) + composerTypeList.AddRange(list2); + list1 = list2; + } + + // sort the composers according to their dependencies + var requirements = new Dictionary?>(); + foreach (Type type in composerTypeList) + { + requirements[type] = null; + } + + foreach (Type type in composerTypeList) + { + GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); + GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); + } + + return requirements; + } + + internal IEnumerable SortComposers(Dictionary?> requirements) + { + // sort composers + var graph = new TopoGraph?>>(kvp => kvp.Key, kvp => kvp.Value); + graph.AddItems(requirements); + List sortedComposerTypes; + try + { + sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); + } + catch (Exception e) + { + // in case of an error, force-dump everything to log + _logger.LogInformation("Composer Report:\r\n{ComposerReport}", GetComposersReport(requirements)); + _logger.LogError(e, "Failed to sort composers."); + throw; + } + + return sortedComposerTypes; + } + + private static void EnableDisableComposers(IEnumerable enableDisableAttributes, ICollection types) + { + var enabled = new Dictionary(); + + // process the enable/disable attributes + // these two attributes are *not* inherited and apply to *classes* only (not interfaces). + // remote declarations (when a composer enables/disables *another* composer) + // have priority over local declarations (when a composer disables itself) so that + // ppl can enable composers that, by default, are disabled. + // what happens in case of conflicting remote declarations is unspecified. more + // precisely, the last declaration to be processed wins, but the order of the + // declarations depends on the type finder and is unspecified. + void UpdateEnableInfo(Type composerType, int weight2, Dictionary enabled2, bool value) + { + if (enabled.TryGetValue(composerType, out EnableInfo? enableInfo) == false) + { + enableInfo = enabled2[composerType] = new EnableInfo(); + } + + if (enableInfo.Weight > weight2) + { + return; + } + + enableInfo.Enabled = value; + enableInfo.Weight = weight2; + } + + foreach (EnableComposerAttribute attr in enableDisableAttributes.OfType()) + { + Type type = attr.EnabledType; + UpdateEnableInfo(type, 2, enabled, true); + } + + foreach (DisableComposerAttribute attr in enableDisableAttributes.OfType()) + { + Type type = attr.DisabledType; + UpdateEnableInfo(type, 2, enabled, false); + } + + foreach (Type composerType in types) + { + foreach (EnableAttribute attr in composerType.GetCustomAttributes()) + { + Type type = attr.EnabledType ?? composerType; + var weight = type == composerType ? 1 : 3; + UpdateEnableInfo(type, weight, enabled, true); + } + + foreach (DisableAttribute attr in composerType.GetCustomAttributes()) + { + Type type = attr.DisabledType ?? composerType; + var weight = type == composerType ? 1 : 3; + UpdateEnableInfo(type, weight, enabled, false); + } + } + + // remove composers that end up being disabled + foreach (KeyValuePair kvp in enabled.Where(x => x.Value.Enabled == false)) + { + types.Remove(kvp.Key); + } + } + + private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary?> requirements, bool throwOnMissing = true) + { + // get 'require' attributes + // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only + IEnumerable afterAttributes = type + .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces + .Concat(type.GetCustomAttributes()); // those marking the composer + + // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. + foreach (ComposeAfterAttribute attr in afterAttributes) + { + if (attr.RequiredType == type) + { + continue; // ignore self-requirements (+ exclude in implems, below) + } + + // requiring an interface = require any enabled composer implementing that interface + // unless strong, and then require at least one enabled composer implementing that interface + if (attr.RequiredType.IsInterface) + { + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface) + .ToList(); + if (implems.Count > 0) { - throw new InvalidOperationException($"Composer {type.FullName} does not have a parameter-less constructor."); + if (requirements[type] == null) + { + requirements[type] = new List(); + } + + requirements[type]!.AddRange(implems); } - yield return (IComposer) ctor.Invoke(Array.Empty()); + // if explicitly set to !weak, is strong, else is weak + else if (attr.Weak == false && throwOnMissing) + { + throw new Exception( + $"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); + } + } + + // requiring a class = require that the composer is enabled + // unless weak, and then requires it if it is enabled + else + { + if (types.Contains(attr.RequiredType)) + { + if (requirements[type] == null) + { + requirements[type] = new List(); + } + + requirements[type]!.Add(attr.RequiredType); + } + + // if not explicitly set to weak, is strong + else if (attr.Weak != true && throwOnMissing) + { + throw new Exception( + $"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); + } } } } + + private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary?> requirements) + { + // get 'required' attributes + // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only + IEnumerable beforeAttributes = type + .GetInterfaces() + .SelectMany(x => x.GetCustomAttributes()) // those marking interfaces + .Concat(type.GetCustomAttributes()); // those marking the composer + + foreach (ComposeBeforeAttribute attr in beforeAttributes) + { + if (attr.RequiringType == type) + { + continue; // ignore self-requirements (+ exclude in implems, below) + } + + // required by an interface = by any enabled composer implementing this that interface + if (attr.RequiringType.IsInterface) + { + var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface) + .ToList(); + foreach (Type implem in implems) + { + if (requirements[implem] == null) + { + requirements[implem] = new List(); + } + + requirements[implem]!.Add(type); + } + } + + // required by a class + else + { + if (types.Contains(attr.RequiringType)) + { + if (requirements[attr.RequiringType] == null) + { + requirements[attr.RequiringType] = new List(); + } + + requirements[attr.RequiringType]!.Add(type); + } + } + } + } + + private static IEnumerable InstantiateComposers(IEnumerable types) + { + foreach (Type type in types) + { + ConstructorInfo? ctor = type.GetConstructor(Array.Empty()); + + if (ctor == null) + { + throw new InvalidOperationException( + $"Composer {type.FullName} does not have a parameter-less constructor."); + } + + yield return (IComposer)ctor.Invoke(Array.Empty()); + } + } + + private class EnableInfo + { + public bool Enabled { get; set; } + + public int Weight { get; set; } = -1; + } } diff --git a/src/Umbraco.Core/Composing/CompositionExtensions.cs b/src/Umbraco.Core/Composing/CompositionExtensions.cs index d087af77d8..2906070e4f 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions.cs @@ -1,43 +1,45 @@ -using System; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.PublishedCache; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class CompositionExtensions { - public static class CompositionExtensions + /// + /// Sets the published snapshot service. + /// + /// The builder. + /// A function creating a published snapshot service. + public static IUmbracoBuilder SetPublishedSnapshotService( + this IUmbracoBuilder builder, + Func factory) { - /// - /// Sets the published snapshot service. - /// - /// The builder. - /// A function creating a published snapshot service. - public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the published snapshot service. - /// - /// The type of the published snapshot service. - /// The builder. - public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder) - where T : class, IPublishedSnapshotService - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the published snapshot service. + /// + /// The type of the published snapshot service. + /// The builder. + public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder) + where T : class, IPublishedSnapshotService + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the published snapshot service. - /// - /// The builder. - /// A published snapshot service. - public static IUmbracoBuilder SetPublishedSnapshotService(this IUmbracoBuilder builder, IPublishedSnapshotService service) - { - builder.Services.AddUnique(service); - return builder; - } + /// + /// Sets the published snapshot service. + /// + /// The builder. + /// A published snapshot service. + public static IUmbracoBuilder SetPublishedSnapshotService( + this IUmbracoBuilder builder, + IPublishedSnapshotService service) + { + builder.Services.AddUnique(service); + return builder; } } diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index 5cc38f31a7..0f1d0cc571 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -1,61 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Returns a list of scannable assemblies based on an entry point assembly and it's references +/// +/// +/// This will recursively search through the entry point's assemblies and Umbraco's core assemblies and their +/// references +/// to create a list of scannable assemblies based on whether they themselves or their transitive dependencies +/// reference Umbraco core assemblies. +/// +public class DefaultUmbracoAssemblyProvider : IAssemblyProvider { - /// - /// Returns a list of scannable assemblies based on an entry point assembly and it's references - /// - /// - /// This will recursively search through the entry point's assemblies and Umbraco's core assemblies and their references - /// to create a list of scannable assemblies based on whether they themselves or their transitive dependencies reference Umbraco core assemblies. - /// - public class DefaultUmbracoAssemblyProvider : IAssemblyProvider + private readonly IEnumerable? _additionalTargetAssemblies; + private readonly Assembly _entryPointAssembly; + private readonly ILoggerFactory _loggerFactory; + private List? _discovered; + + public DefaultUmbracoAssemblyProvider( + Assembly? entryPointAssembly, + ILoggerFactory loggerFactory, + IEnumerable? additionalTargetAssemblies = null) { - private readonly Assembly _entryPointAssembly; - private readonly ILoggerFactory _loggerFactory; - private readonly IEnumerable? _additionalTargetAssemblies; - private List? _discovered; + _entryPointAssembly = entryPointAssembly ?? throw new ArgumentNullException(nameof(entryPointAssembly)); + _loggerFactory = loggerFactory; + _additionalTargetAssemblies = additionalTargetAssemblies; + } - public DefaultUmbracoAssemblyProvider( - Assembly? entryPointAssembly, - ILoggerFactory loggerFactory, - IEnumerable? additionalTargetAssemblies = null) + // TODO: It would be worth investigating a netcore3 version of this which would use + // var allAssemblies = System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies); + // that will still only resolve Assemblies that are already loaded but it would also make it possible to + // query dynamically generated assemblies once they are added. It would also provide the ability to probe + // assembly locations that are not in the same place as the entry point assemblies. + public IEnumerable Assemblies + { + get { - _entryPointAssembly = entryPointAssembly ?? throw new ArgumentNullException(nameof(entryPointAssembly)); - _loggerFactory = loggerFactory; - _additionalTargetAssemblies = additionalTargetAssemblies; - } - - // TODO: It would be worth investigating a netcore3 version of this which would use - // var allAssemblies = System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies); - // that will still only resolve Assemblies that are already loaded but it would also make it possible to - // query dynamically generated assemblies once they are added. It would also provide the ability to probe - // assembly locations that are not in the same place as the entry point assemblies. - - public IEnumerable Assemblies - { - get + if (_discovered != null) { - if (_discovered != null) - { - return _discovered; - } - - IEnumerable additionalTargetAssemblies = Constants.Composing.UmbracoCoreAssemblyNames; - if (_additionalTargetAssemblies != null) - { - additionalTargetAssemblies = additionalTargetAssemblies.Concat(_additionalTargetAssemblies); - } - - var finder = new FindAssembliesWithReferencesTo(new[] { _entryPointAssembly }, additionalTargetAssemblies.ToArray(), true, _loggerFactory); - _discovered = finder.Find().ToList(); - return _discovered; } + + IEnumerable additionalTargetAssemblies = Constants.Composing.UmbracoCoreAssemblyNames; + if (_additionalTargetAssemblies != null) + { + additionalTargetAssemblies = additionalTargetAssemblies.Concat(_additionalTargetAssemblies); + } + + var finder = new FindAssembliesWithReferencesTo( + new[] { _entryPointAssembly }, + additionalTargetAssemblies.ToArray(), + true, + _loggerFactory); + _discovered = finder.Find().ToList(); + + return _discovered; } } } diff --git a/src/Umbraco.Core/Composing/DisableAttribute.cs b/src/Umbraco.Core/Composing/DisableAttribute.cs index 23d825ee1c..09d638188d 100644 --- a/src/Umbraco.Core/Composing/DisableAttribute.cs +++ b/src/Umbraco.Core/Composing/DisableAttribute.cs @@ -1,43 +1,44 @@ -using System; using System.Reflection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Indicates that a composer should be disabled. +/// +/// +/// +/// If a type is specified, disables the composer of that type, else disables the composer marked with the +/// attribute. +/// +/// This attribute is *not* inherited. +/// This attribute applies to classes only, it is not possible to enable/disable interfaces. +/// +/// Assembly-level has greater priority than +/// +/// attribute when it is marking the composer itself, but lower priority that when it is referencing another +/// composer. +/// +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public class DisableAttribute : Attribute { /// - /// Indicates that a composer should be disabled. + /// Initializes a new instance of the class. /// - /// - /// If a type is specified, disables the composer of that type, else disables the composer marked with the attribute. - /// This attribute is *not* inherited. - /// This attribute applies to classes only, it is not possible to enable/disable interfaces. - /// Assembly-level has greater priority than - /// attribute when it is marking the composer itself, but lower priority that when it is referencing another composer. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public class DisableAttribute : Attribute + public DisableAttribute() { - /// - /// Initializes a new instance of the class. - /// - public DisableAttribute() - { } - - public DisableAttribute(string fullTypeName, string assemblyName) - { - DisabledType = Assembly.Load(assemblyName)?.GetType(fullTypeName); - } - - /// - /// Initializes a new instance of the class. - /// - public DisableAttribute(Type disabledType) - { - DisabledType = disabledType; - } - - /// - /// Gets the disabled type, or null if it is the composer marked with the attribute. - /// - public Type? DisabledType { get; } } + + public DisableAttribute(string fullTypeName, string assemblyName) => + DisabledType = Assembly.Load(assemblyName)?.GetType(fullTypeName); + + /// + /// Initializes a new instance of the class. + /// + public DisableAttribute(Type disabledType) => DisabledType = disabledType; + + /// + /// Gets the disabled type, or null if it is the composer marked with the attribute. + /// + public Type? DisabledType { get; } } diff --git a/src/Umbraco.Core/Composing/DisableComposerAttribute.cs b/src/Umbraco.Core/Composing/DisableComposerAttribute.cs index 59b36178cf..2c85d45b46 100644 --- a/src/Umbraco.Core/Composing/DisableComposerAttribute.cs +++ b/src/Umbraco.Core/Composing/DisableComposerAttribute.cs @@ -1,28 +1,26 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a composer should be disabled. +/// +/// +/// +/// Assembly-level has greater priority than +/// +/// attribute when it is marking the composer itself, but lower priority that when it is referencing another +/// composer. +/// +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public class DisableComposerAttribute : Attribute { /// - /// Indicates that a composer should be disabled. + /// Initializes a new instance of the class. /// - /// - /// Assembly-level has greater priority than - /// attribute when it is marking the composer itself, but lower priority that when it is referencing another composer. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public class DisableComposerAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - public DisableComposerAttribute(Type disabledType) - { - DisabledType = disabledType; - } + public DisableComposerAttribute(Type disabledType) => DisabledType = disabledType; - /// - /// Gets the disabled type, or null if it is the composer marked with the attribute. - /// - public Type DisabledType { get; } - } + /// + /// Gets the disabled type, or null if it is the composer marked with the attribute. + /// + public Type DisabledType { get; } } diff --git a/src/Umbraco.Core/Composing/EnableAttribute.cs b/src/Umbraco.Core/Composing/EnableAttribute.cs index 90fb1a9cc6..7ca33d50b7 100644 --- a/src/Umbraco.Core/Composing/EnableAttribute.cs +++ b/src/Umbraco.Core/Composing/EnableAttribute.cs @@ -1,37 +1,39 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a composer should be enabled. +/// +/// +/// +/// If a type is specified, enables the composer of that type, else enables the composer marked with the +/// attribute. +/// +/// This attribute is *not* inherited. +/// This attribute applies to classes only, it is not possible to enable/disable interfaces. +/// +/// Assembly-level has greater priority than +/// +/// attribute when it is marking the composer itself, but lower priority that when it is referencing another +/// composer. +/// +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public class EnableAttribute : Attribute { /// - /// Indicates that a composer should be enabled. + /// Initializes a new instance of the class. /// - /// - /// If a type is specified, enables the composer of that type, else enables the composer marked with the attribute. - /// This attribute is *not* inherited. - /// This attribute applies to classes only, it is not possible to enable/disable interfaces. - /// Assembly-level has greater priority than - /// attribute when it is marking the composer itself, but lower priority that when it is referencing another composer. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public class EnableAttribute : Attribute + public EnableAttribute() { - /// - /// Initializes a new instance of the class. - /// - public EnableAttribute() - { } - - /// - /// Initializes a new instance of the class. - /// - public EnableAttribute(Type enabledType) - { - EnabledType = enabledType; - } - - /// - /// Gets the enabled type, or null if it is the composer marked with the attribute. - /// - public Type? EnabledType { get; } } + + /// + /// Initializes a new instance of the class. + /// + public EnableAttribute(Type enabledType) => EnabledType = enabledType; + + /// + /// Gets the enabled type, or null if it is the composer marked with the attribute. + /// + public Type? EnabledType { get; } } diff --git a/src/Umbraco.Core/Composing/EnableComposerAttribute.cs b/src/Umbraco.Core/Composing/EnableComposerAttribute.cs index 048a19a80f..b1a0f53bcd 100644 --- a/src/Umbraco.Core/Composing/EnableComposerAttribute.cs +++ b/src/Umbraco.Core/Composing/EnableComposerAttribute.cs @@ -1,31 +1,32 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Indicates that a composer should be enabled. +/// +/// +/// +/// If a type is specified, enables the composer of that type, else enables the composer marked with the +/// attribute. +/// +/// This attribute is *not* inherited. +/// This attribute applies to classes only, it is not possible to enable/disable interfaces. +/// +/// Assembly-level has greater priority than +/// +/// attribute when it is marking the composer itself, but lower priority that when it is referencing another +/// composer. +/// +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +public class EnableComposerAttribute : Attribute { /// - /// Indicates that a composer should be enabled. + /// Initializes a new instance of the class. /// - /// - /// If a type is specified, enables the composer of that type, else enables the composer marked with the attribute. - /// This attribute is *not* inherited. - /// This attribute applies to classes only, it is not possible to enable/disable interfaces. - /// Assembly-level has greater priority than - /// attribute when it is marking the composer itself, but lower priority that when it is referencing another composer. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public class EnableComposerAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - public EnableComposerAttribute(Type enabledType) - { - EnabledType = enabledType; - } + public EnableComposerAttribute(Type enabledType) => EnabledType = enabledType; - /// - /// Gets the enabled type, or null if it is the composer marked with the attribute. - /// - public Type EnabledType { get; } - } + /// + /// Gets the enabled type, or null if it is the composer marked with the attribute. + /// + public Type EnabledType { get; } } diff --git a/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs b/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs index 78cdb80f58..f9e4ed6dbe 100644 --- a/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs +++ b/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs @@ -1,69 +1,69 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Composing -{ - /// - /// Finds Assemblies from the entry point assemblies, it's dependencies and it's transitive dependencies that reference that targetAssemblyNames - /// - /// - /// borrowed and modified from here https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/FindAssembliesWithReferencesTo.cs - /// - internal class FindAssembliesWithReferencesTo - { - private readonly Assembly[] _referenceAssemblies; - private readonly string[] _targetAssemblies; - private readonly bool _includeTargets; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; +namespace Umbraco.Cms.Core.Composing; - /// - /// Constructor - /// - /// Entry point assemblies - /// Used to check if the entry point or it's transitive assemblies reference these assembly names - /// If true will also use the target assembly names as entry point assemblies - /// Logger factory for when scanning goes wrong - public FindAssembliesWithReferencesTo(Assembly[] referenceAssemblies, string[] targetAssemblyNames, bool includeTargets, ILoggerFactory loggerFactory) +/// +/// Finds Assemblies from the entry point assemblies, it's dependencies and it's transitive dependencies that reference +/// that targetAssemblyNames +/// +/// +/// borrowed and modified from here +/// https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/FindAssembliesWithReferencesTo.cs +/// +internal class FindAssembliesWithReferencesTo +{ + private readonly bool _includeTargets; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly Assembly[] _referenceAssemblies; + private readonly string[] _targetAssemblies; + + /// + /// Constructor + /// + /// Entry point assemblies + /// + /// Used to check if the entry point or it's transitive assemblies reference these + /// assembly names + /// + /// If true will also use the target assembly names as entry point assemblies + /// Logger factory for when scanning goes wrong + public FindAssembliesWithReferencesTo(Assembly[] referenceAssemblies, string[] targetAssemblyNames, bool includeTargets, ILoggerFactory loggerFactory) + { + _referenceAssemblies = referenceAssemblies; + _targetAssemblies = targetAssemblyNames; + _includeTargets = includeTargets; + _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger(); + } + + public IEnumerable Find() + { + var referenceItems = new List(); + foreach (Assembly assembly in _referenceAssemblies) { - _referenceAssemblies = referenceAssemblies; - _targetAssemblies = targetAssemblyNames; - _includeTargets = includeTargets; - _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(); + referenceItems.Add(assembly); } - public IEnumerable Find() + if (_includeTargets) { - var referenceItems = new List(); - foreach (var assembly in _referenceAssemblies) + foreach (var target in _targetAssemblies) { - referenceItems.Add(assembly); - } - - if (_includeTargets) - { - foreach(var target in _targetAssemblies) + try { - try - { - referenceItems.Add(Assembly.Load(target)); - } - catch (FileNotFoundException ex) - { - // occurs if we cannot load this ... for example in a test project where we aren't currently referencing Umbraco.Web, etc... - _logger.LogDebug(ex, "Could not load assembly " + target); - } + referenceItems.Add(Assembly.Load(target)); + } + catch (FileNotFoundException ex) + { + // occurs if we cannot load this ... for example in a test project where we aren't currently referencing Umbraco.Web, etc... + _logger.LogDebug(ex, "Could not load assembly " + target); } } - - var provider = new ReferenceResolver(_targetAssemblies, referenceItems, _loggerFactory.CreateLogger()); - var assemblyNames = provider.ResolveAssemblies(); - return assemblyNames.ToList(); } + var provider = new ReferenceResolver(_targetAssemblies, referenceItems, _loggerFactory.CreateLogger()); + IEnumerable assemblyNames = provider.ResolveAssemblies(); + return assemblyNames.ToList(); } } diff --git a/src/Umbraco.Core/Composing/HideFromTypeFinderAttribute.cs b/src/Umbraco.Core/Composing/HideFromTypeFinderAttribute.cs index b985a79494..4478deb5ac 100644 --- a/src/Umbraco.Core/Composing/HideFromTypeFinderAttribute.cs +++ b/src/Umbraco.Core/Composing/HideFromTypeFinderAttribute.cs @@ -1,11 +1,9 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Notifies the TypeFinder that it should ignore the class marked with this attribute. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class HideFromTypeFinderAttribute : Attribute { - /// - /// Notifies the TypeFinder that it should ignore the class marked with this attribute. - /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class HideFromTypeFinderAttribute : Attribute - { } } diff --git a/src/Umbraco.Core/Composing/IAssemblyProvider.cs b/src/Umbraco.Core/Composing/IAssemblyProvider.cs index fdc942ae24..4148c9ee47 100644 --- a/src/Umbraco.Core/Composing/IAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/IAssemblyProvider.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; using System.Reflection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a list of assemblies that can be scanned +/// +public interface IAssemblyProvider { - /// - /// Provides a list of assemblies that can be scanned - /// - public interface IAssemblyProvider - { - IEnumerable Assemblies { get; } - } + IEnumerable Assemblies { get; } } diff --git a/src/Umbraco.Core/Composing/IBuilderCollection.cs b/src/Umbraco.Core/Composing/IBuilderCollection.cs index 5e78cf0c2f..56036997bc 100644 --- a/src/Umbraco.Core/Composing/IBuilderCollection.cs +++ b/src/Umbraco.Core/Composing/IBuilderCollection.cs @@ -1,16 +1,13 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Represents a builder collection, ie an immutable enumeration of items. +/// +/// The type of the items. +public interface IBuilderCollection : IEnumerable { /// - /// Represents a builder collection, ie an immutable enumeration of items. + /// Gets the number of items in the collection. /// - /// The type of the items. - public interface IBuilderCollection : IEnumerable - { - /// - /// Gets the number of items in the collection. - /// - int Count { get; } - } + int Count { get; } } diff --git a/src/Umbraco.Core/Composing/ICollectionBuilder.cs b/src/Umbraco.Core/Composing/ICollectionBuilder.cs index ea09558cad..da25a548e7 100644 --- a/src/Umbraco.Core/Composing/ICollectionBuilder.cs +++ b/src/Umbraco.Core/Composing/ICollectionBuilder.cs @@ -1,33 +1,31 @@ -using System; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a collection builder. +/// +public interface ICollectionBuilder { /// - /// Represents a collection builder. + /// Registers the builder so it can build the collection, by + /// registering the collection and the types. /// - public interface ICollectionBuilder - { - /// - /// Registers the builder so it can build the collection, by - /// registering the collection and the types. - /// - void RegisterWith(IServiceCollection services); - } - - /// - /// Represents a collection builder. - /// - /// The type of the collection. - /// The type of the items. - public interface ICollectionBuilder : ICollectionBuilder - where TCollection : IBuilderCollection - { - /// - /// Creates a collection. - /// - /// A collection. - /// Creates a new collection each time it is invoked. - TCollection CreateCollection(IServiceProvider factory); - } + void RegisterWith(IServiceCollection services); +} + +/// +/// Represents a collection builder. +/// +/// The type of the collection. +/// The type of the items. +public interface ICollectionBuilder : ICollectionBuilder + where TCollection : IBuilderCollection +{ + /// + /// Creates a collection. + /// + /// A collection. + /// Creates a new collection each time it is invoked. + TCollection CreateCollection(IServiceProvider factory); } diff --git a/src/Umbraco.Core/Composing/IComponent.cs b/src/Umbraco.Core/Composing/IComponent.cs index 8e9cf815e8..d5655f8a1f 100644 --- a/src/Umbraco.Core/Composing/IComponent.cs +++ b/src/Umbraco.Core/Composing/IComponent.cs @@ -1,25 +1,28 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a component. +/// +/// +/// Components are created by DI and therefore must have a public constructor. +/// +/// All components are terminated in reverse order when Umbraco terminates, and +/// disposable components are disposed. +/// +/// +/// The Dispose method may be invoked more than once, and components +/// should ensure they support this. +/// +/// +public interface IComponent { /// - /// Represents a component. + /// Initializes the component. /// - /// - /// Components are created by DI and therefore must have a public constructor. - /// All components are terminated in reverse order when Umbraco terminates, and - /// disposable components are disposed. - /// The Dispose method may be invoked more than once, and components - /// should ensure they support this. - /// - public interface IComponent - { - /// - /// Initializes the component. - /// - void Initialize(); + void Initialize(); - /// - /// Terminates the component. - /// - void Terminate(); - } + /// + /// Terminates the component. + /// + void Terminate(); } diff --git a/src/Umbraco.Core/Composing/IComposer.cs b/src/Umbraco.Core/Composing/IComposer.cs index 6f1978ee3e..7d0a859314 100644 --- a/src/Umbraco.Core/Composing/IComposer.cs +++ b/src/Umbraco.Core/Composing/IComposer.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a composer. +/// +public interface IComposer : IDiscoverable { /// - /// Represents a composer. + /// Compose. /// - public interface IComposer : IDiscoverable - { - /// - /// Compose. - /// - void Compose(IUmbracoBuilder builder); - } + void Compose(IUmbracoBuilder builder); } diff --git a/src/Umbraco.Core/Composing/IDiscoverable.cs b/src/Umbraco.Core/Composing/IDiscoverable.cs index 153fde36b6..848c70ddab 100644 --- a/src/Umbraco.Core/Composing/IDiscoverable.cs +++ b/src/Umbraco.Core/Composing/IDiscoverable.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +public interface IDiscoverable { - public interface IDiscoverable - { } } diff --git a/src/Umbraco.Core/Composing/IRuntimeHash.cs b/src/Umbraco.Core/Composing/IRuntimeHash.cs index b19b22a7e9..d641c90538 100644 --- a/src/Umbraco.Core/Composing/IRuntimeHash.cs +++ b/src/Umbraco.Core/Composing/IRuntimeHash.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Used to create a hash value of the current runtime +/// +/// +/// This is used to detect if the runtime itself has changed, like a DLL has changed or another dynamically compiled +/// part of the application has changed. This is used to detect if we need to re-type scan. +/// +public interface IRuntimeHash { - /// - /// Used to create a hash value of the current runtime - /// - /// - /// This is used to detect if the runtime itself has changed, like a DLL has changed or another dynamically compiled - /// part of the application has changed. This is used to detect if we need to re-type scan. - /// - public interface IRuntimeHash - { - string GetHashValue(); - } + string GetHashValue(); } diff --git a/src/Umbraco.Core/Composing/ITypeFinder.cs b/src/Umbraco.Core/Composing/ITypeFinder.cs index 7d59b68869..4bebfae334 100644 --- a/src/Umbraco.Core/Composing/ITypeFinder.cs +++ b/src/Umbraco.Core/Composing/ITypeFinder.cs @@ -1,55 +1,52 @@ -using System; -using System.Collections.Generic; using System.Reflection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Used to find objects by implemented types, names and/or attributes +/// +public interface ITypeFinder { /// - /// Used to find objects by implemented types, names and/or attributes + /// Return a list of found local Assemblies that Umbraco should scan for type finding /// - public interface ITypeFinder - { - Type? GetTypeByName(string name); + /// + IEnumerable AssembliesToScan { get; } - /// - /// Return a list of found local Assemblies that Umbraco should scan for type finding - /// - /// - IEnumerable AssembliesToScan { get; } + Type? GetTypeByName(string name); - /// - /// Finds any classes derived from the assignTypeFrom Type that contain the attribute TAttribute - /// - /// - /// - /// - /// - /// - IEnumerable FindClassesOfTypeWithAttribute( - Type assignTypeFrom, - Type attributeType, - IEnumerable? assemblies = null, - bool onlyConcreteClasses = true); + /// + /// Finds any classes derived from the assignTypeFrom Type that contain the attribute TAttribute + /// + /// + /// + /// + /// + /// + IEnumerable FindClassesOfTypeWithAttribute( + Type assignTypeFrom, + Type attributeType, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true); - /// - /// Returns all types found of in the assemblies specified of type T - /// - /// - /// - /// - /// - IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true); + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + /// + IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true); - /// - /// Finds any classes with the attribute. - /// - /// The attribute type - /// The assemblies. - /// if set to true only concrete classes. - /// - IEnumerable FindClassesWithAttribute( - Type attributeType, - IEnumerable? assemblies, - bool onlyConcreteClasses); - } + /// + /// Finds any classes with the attribute. + /// + /// The attribute type + /// The assemblies. + /// if set to true only concrete classes. + /// + IEnumerable FindClassesWithAttribute( + Type attributeType, + IEnumerable? assemblies, + bool onlyConcreteClasses); } diff --git a/src/Umbraco.Core/Composing/IUserComposer.cs b/src/Umbraco.Core/Composing/IUserComposer.cs index fe5af3a985..a3e45054f8 100644 --- a/src/Umbraco.Core/Composing/IUserComposer.cs +++ b/src/Umbraco.Core/Composing/IUserComposer.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a user . +/// +[Obsolete("This interface is obsolete. Use IComposer instead.")] +public interface IUserComposer : IComposer { - /// - /// Represents a user . - /// - [System.Obsolete("This interface is obsolete. Use IComposer instead.")] - public interface IUserComposer : IComposer - { } } diff --git a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs index baae385af4..49ada40dfa 100644 --- a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs @@ -1,129 +1,131 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Implements a lazy collection builder. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +/// +/// This type of collection builder is typically used when type scanning is required (i.e. plugins). +/// +public abstract class + LazyCollectionBuilderBase : CollectionBuilderBase + where TBuilder : LazyCollectionBuilderBase + where TCollection : class, IBuilderCollection { + private readonly List _excluded = new(); + private readonly List>> _producers = new(); + + protected abstract TBuilder This { get; } + /// - /// Implements a lazy collection builder. + /// Clears all types in the collection. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - /// - /// This type of collection builder is typically used when type scanning is required (i.e. plugins). - /// - public abstract class LazyCollectionBuilderBase : CollectionBuilderBase - where TBuilder : LazyCollectionBuilderBase - where TCollection : class, IBuilderCollection + /// The builder. + public TBuilder Clear() { - private readonly List>> _producers = new List>>(); - private readonly List _excluded = new List(); - - protected abstract TBuilder This { get; } - - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() + Configure(types => { - Configure(types => - { - types.Clear(); - _producers.Clear(); - _excluded.Clear(); - }); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add() - where T : TItem - { - Configure(types => - { - var type = typeof(T); - if (types.Contains(type) == false) - types.Add(type); - }); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add(Type type) - { - Configure(types => - { - EnsureType(type, "register"); - if (types.Contains(type) == false) - types.Add(type); - }); - return This; - } - - /// - /// Adds a types producer to the collection. - /// - /// The types producer. - /// The builder. - public TBuilder Add(Func> producer) - { - Configure(types => - { - _producers.Add(producer); - }); - return This; - } - - /// - /// Excludes a type from the collection. - /// - /// The type to exclude. - /// The builder. - public TBuilder Exclude() - where T : TItem - { - Configure(types => - { - var type = typeof(T); - if (_excluded.Contains(type) == false) - _excluded.Add(type); - }); - return This; - } - - /// - /// Excludes a type from the collection. - /// - /// The type to exclude. - /// The builder. - public TBuilder Exclude(Type type) - { - Configure(types => - { - EnsureType(type, "exclude"); - if (_excluded.Contains(type) == false) - _excluded.Add(type); - }); - return This; - } - - protected override IEnumerable GetRegisteringTypes(IEnumerable types) - { - return types - .Union(_producers.SelectMany(x => x())) - .Distinct() - .Select(x => EnsureType(x, "register")) - .Except(_excluded); - } + types.Clear(); + _producers.Clear(); + _excluded.Clear(); + }); + return This; } + + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add() + where T : TItem + { + Configure(types => + { + Type type = typeof(T); + if (types.Contains(type) == false) + { + types.Add(type); + } + }); + return This; + } + + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add(Type type) + { + Configure(types => + { + EnsureType(type, "register"); + if (types.Contains(type) == false) + { + types.Add(type); + } + }); + return This; + } + + /// + /// Adds a types producer to the collection. + /// + /// The types producer. + /// The builder. + public TBuilder Add(Func> producer) + { + Configure(types => + { + _producers.Add(producer); + }); + return This; + } + + /// + /// Excludes a type from the collection. + /// + /// The type to exclude. + /// The builder. + public TBuilder Exclude() + where T : TItem + { + Configure(types => + { + Type type = typeof(T); + if (_excluded.Contains(type) == false) + { + _excluded.Add(type); + } + }); + return This; + } + + /// + /// Excludes a type from the collection. + /// + /// The type to exclude. + /// The builder. + public TBuilder Exclude(Type type) + { + Configure(types => + { + EnsureType(type, "exclude"); + if (_excluded.Contains(type) == false) + { + _excluded.Add(type); + } + }); + return This; + } + + protected override IEnumerable GetRegisteringTypes(IEnumerable types) => + types + .Union(_producers.SelectMany(x => x())) + .Distinct() + .Select(x => EnsureType(x, "register")) + .Except(_excluded); } diff --git a/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs b/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs index 67116524ac..eb6f2ed055 100644 --- a/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs +++ b/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs @@ -1,48 +1,46 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +public sealed class LazyReadOnlyCollection : IReadOnlyCollection { - public sealed class LazyReadOnlyCollection : IReadOnlyCollection + private readonly Lazy> _lazyCollection; + private int? _count; + + public LazyReadOnlyCollection(Lazy> lazyCollection) => _lazyCollection = lazyCollection; + + public LazyReadOnlyCollection(Func> lazyCollection) => + _lazyCollection = new Lazy>(lazyCollection); + + public IEnumerable Value => EnsureCollection(); + + public int Count { - private readonly Lazy> _lazyCollection; - private int? _count; - - public LazyReadOnlyCollection(Lazy> lazyCollection) => _lazyCollection = lazyCollection; - - public LazyReadOnlyCollection(Func> lazyCollection) => _lazyCollection = new Lazy>(lazyCollection); - - public IEnumerable Value => EnsureCollection(); - - private IEnumerable EnsureCollection() + get { - if (_lazyCollection == null) - { - _count = 0; - return Enumerable.Empty(); - } + EnsureCollection(); + return _count.GetValueOrDefault(); + } + } - IEnumerable val = _lazyCollection.Value; - if (_count == null) - { - _count = val.Count(); - } - return val; + public IEnumerator GetEnumerator() => Value.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IEnumerable EnsureCollection() + { + if (_lazyCollection == null) + { + _count = 0; + return Enumerable.Empty(); } - public int Count + IEnumerable val = _lazyCollection.Value; + if (_count == null) { - get - { - EnsureCollection(); - return _count.GetValueOrDefault(); - } + _count = val.Count(); } - public IEnumerator GetEnumerator() => Value.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return val; } } diff --git a/src/Umbraco.Core/Composing/LazyResolve.cs b/src/Umbraco.Core/Composing/LazyResolve.cs index afa22f74b6..723d9afe2e 100644 --- a/src/Umbraco.Core/Composing/LazyResolve.cs +++ b/src/Umbraco.Core/Composing/LazyResolve.cs @@ -1,13 +1,12 @@ -using System; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +public class LazyResolve : Lazy + where T : class { - public class LazyResolve : Lazy - where T : class + public LazyResolve(IServiceProvider serviceProvider) + : base(serviceProvider.GetRequiredService) { - public LazyResolve(IServiceProvider serviceProvider) - : base(serviceProvider.GetRequiredService) - { } } } diff --git a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs index 939561f557..d9c733da7d 100644 --- a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs @@ -1,331 +1,418 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Implements an ordered collection builder. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +public abstract class OrderedCollectionBuilderBase : CollectionBuilderBase + where TBuilder : OrderedCollectionBuilderBase + where TCollection : class, IBuilderCollection { + protected abstract TBuilder This { get; } + /// - /// Implements an ordered collection builder. + /// Clears all types in the collection. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - public abstract class OrderedCollectionBuilderBase : CollectionBuilderBase - where TBuilder : OrderedCollectionBuilderBase - where TCollection : class, IBuilderCollection + /// The builder. + public TBuilder Clear() { - protected abstract TBuilder This { get; } + Configure(types => types.Clear()); + return This; + } - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() + /// + /// Appends a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Append() + where T : TItem + { + Configure(types => { - Configure(types => types.Clear()); - return This; - } - - /// - /// Appends a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Append() - where T : TItem - { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var type = typeof (T); - if (types.Contains(type)) types.Remove(type); - types.Add(type); - }); - return This; - } + types.Remove(type); + } - /// - /// Appends a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Append(Type type) + types.Add(type); + }); + return This; + } + + /// + /// Appends a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Append(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "register"); + if (types.Contains(type)) { + types.Remove(type); + } + + types.Add(type); + }); + return This; + } + + /// + /// Appends types to the collections. + /// + /// The types to append. + /// The builder. + public TBuilder Append(IEnumerable types) + { + Configure(list => + { + foreach (Type type in types) + { + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast EnsureType(type, "register"); - if (types.Contains(type)) types.Remove(type); - types.Add(type); - }); - return This; - } - - /// - /// Appends types to the collections. - /// - /// The types to append. - /// The builder. - public TBuilder Append(IEnumerable types) - { - Configure(list => - { - foreach (var type in types) + if (list.Contains(type)) { - // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast - EnsureType(type, "register"); - if (list.Contains(type)) list.Remove(type); - list.Add(type); + list.Remove(type); } - }); - return This; - } - /// - /// Inserts a type into the collection. - /// - /// The type to insert. - /// The optional index. - /// The builder. - /// Throws if the index is out of range. - public TBuilder Insert(int index = 0) - where T : TItem + list.Add(type); + } + }); + return This; + } + + /// + /// Inserts a type into the collection. + /// + /// The type to insert. + /// The optional index. + /// The builder. + /// Throws if the index is out of range. + public TBuilder Insert(int index = 0) + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) + { + types.Remove(type); + } + + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type into the collection. + /// + /// The type to insert. + /// The builder. + /// Throws if the index is out of range. + public TBuilder Insert(Type type) => Insert(0, type); + + /// + /// Inserts a type into the collection. + /// + /// The index. + /// The type to insert. + /// The builder. + /// Throws if the index is out of range. + public TBuilder Insert(int index, Type type) + { + Configure(types => + { + EnsureType(type, "register"); + if (types.Contains(type)) + { + types.Remove(type); + } + + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type before another type. + /// + /// The other type. + /// The type to insert. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertBefore() + where TBefore : TItem + where T : TItem + { + Configure(types => + { + Type typeBefore = typeof(TBefore); + Type type = typeof(T); + if (typeBefore == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeBefore); + if (index < 0) + { + throw new InvalidOperationException(); + } + + if (types.Contains(type)) + { + types.Remove(type); + } + + index = types.IndexOf(typeBefore); // in case removing type changed index + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type before another type. + /// + /// The other type. + /// The type to insert. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertBefore(Type typeBefore, Type type) + { + Configure(types => + { + EnsureType(typeBefore, "find"); + EnsureType(type, "register"); + + if (typeBefore == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeBefore); + if (index < 0) + { + throw new InvalidOperationException(); + } + + if (types.Contains(type)) + { + types.Remove(type); + } + + index = types.IndexOf(typeBefore); // in case removing type changed index + types.Insert(index, type); + }); + return This; + } + + /// + /// Inserts a type after another type. + /// + /// The other type. + /// The type to append. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertAfter() + where TAfter : TItem + where T : TItem + { + Configure(types => + { + Type typeAfter = typeof(TAfter); + Type type = typeof(T); + if (typeAfter == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeAfter); + if (index < 0) + { + throw new InvalidOperationException(); + } + + if (types.Contains(type)) + { + types.Remove(type); + } + + index = types.IndexOf(typeAfter); // in case removing type changed index + index += 1; // insert here + + if (index == types.Count) + { + types.Add(type); + } + else { - var type = typeof (T); - if (types.Contains(type)) types.Remove(type); types.Insert(index, type); - }); - return This; - } + } + }); + return This; + } - /// - /// Inserts a type into the collection. - /// - /// The type to insert. - /// The builder. - /// Throws if the index is out of range. - public TBuilder Insert(Type type) + /// + /// Inserts a type after another type. + /// + /// The other type. + /// The type to insert. + /// The builder. + /// Throws if both types are identical, or if the other type does not already belong to the collection. + public TBuilder InsertAfter(Type typeAfter, Type type) + { + Configure(types => { - return Insert(0, type); - } + EnsureType(typeAfter, "find"); + EnsureType(type, "register"); - /// - /// Inserts a type into the collection. - /// - /// The index. - /// The type to insert. - /// The builder. - /// Throws if the index is out of range. - public TBuilder Insert(int index, Type type) - { - Configure(types => + if (typeAfter == type) + { + throw new InvalidOperationException(); + } + + var index = types.IndexOf(typeAfter); + if (index < 0) + { + throw new InvalidOperationException(); + } + + if (types.Contains(type)) + { + types.Remove(type); + } + + index = types.IndexOf(typeAfter); // in case removing type changed index + index += 1; // insert here + + if (index == types.Count) + { + types.Add(type); + } + else { - EnsureType(type, "register"); - if (types.Contains(type)) types.Remove(type); types.Insert(index, type); - }); - return This; - } + } + }); + return This; + } - /// - /// Inserts a type before another type. - /// - /// The other type. - /// The type to insert. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder InsertBefore() - where TBefore : TItem - where T : TItem + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove() + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var typeBefore = typeof(TBefore); - var type = typeof(T); - if (typeBefore == type) throw new InvalidOperationException(); + types.Remove(type); + } + }); + return This; + } - var index = types.IndexOf(typeBefore); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeBefore); // in case removing type changed index - types.Insert(index, type); - }); - return This; - } - - /// - /// Inserts a type before another type. - /// - /// The other type. - /// The type to insert. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder InsertBefore(Type typeBefore, Type type) + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "remove"); + if (types.Contains(type)) { - EnsureType(typeBefore, "find"); - EnsureType(type, "register"); + types.Remove(type); + } + }); + return This; + } - if (typeBefore == type) throw new InvalidOperationException(); - - var index = types.IndexOf(typeBefore); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeBefore); // in case removing type changed index - types.Insert(index, type); - }); - return This; - } - - /// - /// Inserts a type after another type. - /// - /// The other type. - /// The type to append. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder InsertAfter() - where TAfter : TItem - where T : TItem + /// + /// Replaces a type in the collection. + /// + /// The type to replace. + /// The type to insert. + /// The builder. + /// Throws if the type to replace does not already belong to the collection. + public TBuilder Replace() + where TReplaced : TItem + where T : TItem + { + Configure(types => { - Configure(types => + Type typeReplaced = typeof(TReplaced); + Type type = typeof(T); + if (typeReplaced == type) { - var typeAfter = typeof(TAfter); - var type = typeof(T); - if (typeAfter == type) throw new InvalidOperationException(); + return; + } - var index = types.IndexOf(typeAfter); - if (index < 0) throw new InvalidOperationException(); + var index = types.IndexOf(typeReplaced); + if (index < 0) + { + throw new InvalidOperationException(); + } - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeAfter); // in case removing type changed index - index += 1; // insert here + if (types.Contains(type)) + { + types.Remove(type); + } - if (index == types.Count) - types.Add(type); - else - types.Insert(index, type); - }); - return This; - } + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; + } - /// - /// Inserts a type after another type. - /// - /// The other type. - /// The type to insert. - /// The builder. - /// Throws if both types are identical, or if the other type does not already belong to the collection. - public TBuilder InsertAfter(Type typeAfter, Type type) + /// + /// Replaces a type in the collection. + /// + /// The type to replace. + /// The type to insert. + /// The builder. + /// Throws if the type to replace does not already belong to the collection. + public TBuilder Replace(Type typeReplaced, Type type) + { + Configure(types => { - Configure(types => + EnsureType(typeReplaced, "find"); + EnsureType(type, "register"); + + if (typeReplaced == type) { - EnsureType(typeAfter, "find"); - EnsureType(type, "register"); + return; + } - if (typeAfter == type) throw new InvalidOperationException(); - - var index = types.IndexOf(typeAfter); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeAfter); // in case removing type changed index - index += 1; // insert here - - if (index == types.Count) - types.Add(type); - else - types.Insert(index, type); - }); - return This; - } - - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove() - where T : TItem - { - Configure(types => + var index = types.IndexOf(typeReplaced); + if (index < 0) { - var type = typeof (T); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + throw new InvalidOperationException(); + } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) - { - Configure(types => + if (types.Contains(type)) { - EnsureType(type, "remove"); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } - /// - /// Replaces a type in the collection. - /// - /// The type to replace. - /// The type to insert. - /// The builder. - /// Throws if the type to replace does not already belong to the collection. - public TBuilder Replace() - where TReplaced : TItem - where T : TItem - { - Configure(types => - { - var typeReplaced = typeof(TReplaced); - var type = typeof(T); - if (typeReplaced == type) return; - - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeReplaced); // in case removing type changed index - types.Insert(index, type); - types.Remove(typeReplaced); - }); - return This; - } - - /// - /// Replaces a type in the collection. - /// - /// The type to replace. - /// The type to insert. - /// The builder. - /// Throws if the type to replace does not already belong to the collection. - public TBuilder Replace(Type typeReplaced, Type type) - { - Configure(types => - { - EnsureType(typeReplaced, "find"); - EnsureType(type, "register"); - - if (typeReplaced == type) return; - - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); - - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeReplaced); // in case removing type changed index - types.Insert(index, type); - types.Remove(typeReplaced); - }); - return This; - } + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; } } diff --git a/src/Umbraco.Core/Composing/ReferenceResolver.cs b/src/Umbraco.Core/Composing/ReferenceResolver.cs index 5b7c5ffde9..1924fb4b75 100644 --- a/src/Umbraco.Core/Composing/ReferenceResolver.cs +++ b/src/Umbraco.Core/Composing/ReferenceResolver.cs @@ -1,195 +1,199 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Reflection; using System.Security; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Resolves assemblies that reference one of the specified "targetAssemblies" either directly or transitively. +/// +/// +/// Borrowed and modified from +/// https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/ReferenceResolver.cs +/// +internal class ReferenceResolver { - /// - /// Resolves assemblies that reference one of the specified "targetAssemblies" either directly or transitively. - /// - /// - /// Borrowed and modified from https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/ReferenceResolver.cs - /// - internal class ReferenceResolver + private readonly IReadOnlyList _assemblies; + private readonly Dictionary _classifications; + private readonly ILogger _logger; + private readonly List _lookup = new(); + private readonly HashSet _umbracoAssemblies; + + public ReferenceResolver(IReadOnlyList targetAssemblies, IReadOnlyList entryPointAssemblies, ILogger logger) { - private readonly HashSet _umbracoAssemblies; - private readonly IReadOnlyList _assemblies; - private readonly Dictionary _classifications; - private readonly List _lookup = new List(); - private readonly ILogger _logger; - public ReferenceResolver(IReadOnlyList targetAssemblies, IReadOnlyList entryPointAssemblies, ILogger logger) - { - _umbracoAssemblies = new HashSet(targetAssemblies, StringComparer.Ordinal); - _assemblies = entryPointAssemblies; - _logger = logger; - _classifications = new Dictionary(); + _umbracoAssemblies = new HashSet(targetAssemblies, StringComparer.Ordinal); + _assemblies = entryPointAssemblies; + _logger = logger; + _classifications = new Dictionary(); - foreach (var item in entryPointAssemblies) - { - _lookup.Add(item); - } + foreach (Assembly item in entryPointAssemblies) + { + _lookup.Add(item); } + } - /// - /// Returns a list of assemblies that directly reference or transitively reference the targetAssemblies - /// - /// - /// - /// This includes all assemblies in the same location as the entry point assemblies - /// - public IEnumerable ResolveAssemblies() + protected enum Classification + { + Unknown, + DoesNotReferenceUmbraco, + ReferencesUmbraco, + IsUmbraco, + } + + /// + /// Returns a list of assemblies that directly reference or transitively reference the targetAssemblies + /// + /// + /// + /// This includes all assemblies in the same location as the entry point assemblies + /// + public IEnumerable ResolveAssemblies() + { + var applicationParts = new List(); + + var assemblies = new HashSet(_assemblies); + + // Get the unique directories of the assemblies + var assemblyLocations = GetAssemblyFolders(assemblies).ToList(); + + // Load in each assembly in the directory of the entry assembly to be included in the search + // for Umbraco dependencies/transitive dependencies + foreach (var dir in assemblyLocations) { - var applicationParts = new List(); - - var assemblies = new HashSet(_assemblies); - - // Get the unique directories of the assemblies - var assemblyLocations = GetAssemblyFolders(assemblies).ToList(); - - // Load in each assembly in the directory of the entry assembly to be included in the search - // for Umbraco dependencies/transitive dependencies - foreach(var dir in assemblyLocations) + foreach (var dll in Directory.EnumerateFiles(dir ?? string.Empty, "*.dll")) { - foreach(var dll in Directory.EnumerateFiles(dir ?? string.Empty, "*.dll")) + AssemblyName? assemblyName = null; + try { - AssemblyName? assemblyName = null; - try - { - assemblyName = AssemblyName.GetAssemblyName(dll); - } - catch (BadImageFormatException e) - { - _logger.LogDebug(e, "Could not load {dll} for type scanning, skipping", dll); - } - catch (SecurityException e) - { - _logger.LogError(e, "Could not access {dll} for type scanning due to a security problem", dll); - } - catch (Exception e) - { - _logger.LogInformation(e, "Error: could not load {dll} for type scanning", dll); - } + assemblyName = AssemblyName.GetAssemblyName(dll); + } + catch (BadImageFormatException e) + { + _logger.LogDebug(e, "Could not load {dll} for type scanning, skipping", dll); + } + catch (SecurityException e) + { + _logger.LogError(e, "Could not access {dll} for type scanning due to a security problem", dll); + } + catch (Exception e) + { + _logger.LogInformation(e, "Error: could not load {dll} for type scanning", dll); + } - if (assemblyName != null) - { - // don't include if this is excluded - if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => + if (assemblyName != null) + { + // don't include if this is excluded + if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => assemblyName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) - continue; - - // don't include this item if it's Umbraco Core - if (Constants.Composing.UmbracoCoreAssemblyNames.Any(x=>assemblyName.FullName.StartsWith(x) || (assemblyName.Name?.EndsWith(".Views") ?? false))) - continue; - - var assembly = Assembly.Load(assemblyName); - assemblies.Add(assembly); - } - } - } - - foreach (var item in assemblies) - { - var classification = Resolve(item); - if (classification == Classification.ReferencesUmbraco || classification == Classification.IsUmbraco) - { - applicationParts.Add(item); - } - } - - return applicationParts; - } - - - private IEnumerable GetAssemblyFolders(IEnumerable assemblies) - { - return assemblies.Select(x => Path.GetDirectoryName(GetAssemblyLocation(x))).Distinct(); - } - - // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs - private string GetAssemblyLocation(Assembly assembly) - { - if (Uri.TryCreate(assembly.CodeBase, UriKind.Absolute, out var result) && - result.IsFile && string.IsNullOrWhiteSpace(result.Fragment)) - { - return result.LocalPath; - } - - return assembly.Location; - } - - private Classification Resolve(Assembly assembly) - { - if (_classifications.TryGetValue(assembly, out var classification)) - { - return classification; - } - - // Initialize the dictionary with a value to short-circuit recursive references. - classification = Classification.Unknown; - _classifications[assembly] = classification; - - if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => assembly.FullName?.StartsWith(f, StringComparison.InvariantCultureIgnoreCase) ?? false)) - { - // if its part of the filter it doesn't reference umbraco - classification = Classification.DoesNotReferenceUmbraco; - } - else if (_umbracoAssemblies.Contains(assembly.GetName().Name!)) - { - classification = Classification.IsUmbraco; - } - else - { - classification = Classification.DoesNotReferenceUmbraco; - foreach (var reference in GetReferences(assembly)) - { - // recurse - var referenceClassification = Resolve(reference); - - if (referenceClassification == Classification.IsUmbraco || referenceClassification == Classification.ReferencesUmbraco) { - classification = Classification.ReferencesUmbraco; - break; + continue; } + + // don't include this item if it's Umbraco Core + if (Constants.Composing.UmbracoCoreAssemblyNames.Any(x => + assemblyName.FullName.StartsWith(x) || (assemblyName.Name?.EndsWith(".Views") ?? false))) + { + continue; + } + + var assembly = Assembly.Load(assemblyName); + assemblies.Add(assembly); } } + } - Debug.Assert(classification != Classification.Unknown); - _classifications[assembly] = classification; + foreach (Assembly item in assemblies) + { + Classification classification = Resolve(item); + if (classification == Classification.ReferencesUmbraco || classification == Classification.IsUmbraco) + { + applicationParts.Add(item); + } + } + + return applicationParts; + } + + protected virtual IEnumerable GetReferences(Assembly assembly) + { + foreach (AssemblyName referenceName in assembly.GetReferencedAssemblies()) + { + // don't include if this is excluded + if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => + referenceName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) + { + continue; + } + + var reference = Assembly.Load(referenceName); + + if (!_lookup.Contains(reference)) + { + // A dependency references an item that isn't referenced by this project. + // We'll add this reference so that we can calculate the classification. + _lookup.Add(reference); + } + + yield return reference; + } + } + + private IEnumerable GetAssemblyFolders(IEnumerable assemblies) => + assemblies.Select(x => Path.GetDirectoryName(GetAssemblyLocation(x))).Distinct(); + + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs + private string GetAssemblyLocation(Assembly assembly) + { + if (Uri.TryCreate(assembly.Location, UriKind.Absolute, out Uri? result) && + result.IsFile && string.IsNullOrWhiteSpace(result.Fragment)) + { + return result.LocalPath; + } + + return assembly.Location; + } + + private Classification Resolve(Assembly assembly) + { + if (_classifications.TryGetValue(assembly, out Classification classification)) + { return classification; } - protected virtual IEnumerable GetReferences(Assembly assembly) + // Initialize the dictionary with a value to short-circuit recursive references. + classification = Classification.Unknown; + _classifications[assembly] = classification; + + if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => + assembly.FullName?.StartsWith(f, StringComparison.InvariantCultureIgnoreCase) ?? false)) { - foreach (var referenceName in assembly.GetReferencedAssemblies()) + // if its part of the filter it doesn't reference umbraco + classification = Classification.DoesNotReferenceUmbraco; + } + else if (_umbracoAssemblies.Contains(assembly.GetName().Name!)) + { + classification = Classification.IsUmbraco; + } + else + { + classification = Classification.DoesNotReferenceUmbraco; + foreach (Assembly reference in GetReferences(assembly)) { - // don't include if this is excluded - if (TypeFinder.KnownAssemblyExclusionFilter.Any(f => referenceName.FullName.StartsWith(f, StringComparison.InvariantCultureIgnoreCase))) - continue; + // recurse + Classification referenceClassification = Resolve(reference); - var reference = Assembly.Load(referenceName); - - if (!_lookup.Contains(reference)) + if (referenceClassification == Classification.IsUmbraco || + referenceClassification == Classification.ReferencesUmbraco) { - // A dependency references an item that isn't referenced by this project. - // We'll add this reference so that we can calculate the classification. - - _lookup.Add(reference); + classification = Classification.ReferencesUmbraco; + break; } - yield return reference; } } - protected enum Classification - { - Unknown, - DoesNotReferenceUmbraco, - ReferencesUmbraco, - IsUmbraco, - } + Debug.Assert(classification != Classification.Unknown); + _classifications[assembly] = classification; + return classification; } } diff --git a/src/Umbraco.Core/Composing/RuntimeHash.cs b/src/Umbraco.Core/Composing/RuntimeHash.cs index 5e0523f09d..e66bedf79f 100644 --- a/src/Umbraco.Core/Composing/RuntimeHash.cs +++ b/src/Umbraco.Core/Composing/RuntimeHash.cs @@ -1,93 +1,89 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; using Umbraco.Cms.Core.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Determines the runtime hash based on file system paths to scan +/// +public class RuntimeHash : IRuntimeHash { - /// - /// Determines the runtime hash based on file system paths to scan - /// - public class RuntimeHash : IRuntimeHash + private readonly IProfilingLogger _logger; + private readonly RuntimeHashPaths _paths; + private string? _calculated; + + public RuntimeHash(IProfilingLogger logger, RuntimeHashPaths paths) { - private readonly IProfilingLogger _logger; - private readonly RuntimeHashPaths _paths; - private string? _calculated; + _logger = logger; + _paths = paths; + } - public RuntimeHash(IProfilingLogger logger, RuntimeHashPaths paths) + public string GetHashValue() + { + if (_calculated != null) { - _logger = logger; - _paths = paths; - } - - - public string GetHashValue() - { - if (_calculated != null) - { - return _calculated; - } - - IEnumerable<(FileSystemInfo, bool)> allPaths = _paths.GetFolders() - .Select(x => ((FileSystemInfo)x, false)) - .Concat(_paths.GetFiles().Select(x => ((FileSystemInfo)x.Key, x.Value))); - - _calculated = GetFileHash(allPaths); - return _calculated; } - /// - /// Returns a unique hash for a combination of FileInfo objects. - /// - /// A collection of files. - /// The hash. - /// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the - /// file properties (false) or the file contents (true). - private string GetFileHash(IEnumerable<(FileSystemInfo fileOrFolder, bool scanFileContent)> filesAndFolders) + IEnumerable<(FileSystemInfo, bool)> allPaths = _paths.GetFolders() + .Select(x => ((FileSystemInfo)x, false)) + .Concat(_paths.GetFiles().Select(x => ((FileSystemInfo)x.Key, x.Value))); + + _calculated = GetFileHash(allPaths); + + return _calculated; + } + + /// + /// Returns a unique hash for a combination of FileInfo objects. + /// + /// A collection of files. + /// The hash. + /// + /// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the + /// file properties (false) or the file contents (true). + /// + private string GetFileHash(IEnumerable<(FileSystemInfo fileOrFolder, bool scanFileContent)> filesAndFolders) + { + using (_logger.DebugDuration("Determining hash of code files on disk", "Hash determined")) { - using (_logger.DebugDuration("Determining hash of code files on disk", "Hash determined")) + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + var uniqContent = new HashSet(); + + using var generator = new HashGenerator(); + + foreach ((FileSystemInfo fileOrFolder, var scanFileContent) in filesAndFolders) { - // get the distinct file infos to hash - var uniqInfos = new HashSet(); - var uniqContent = new HashSet(); - - using var generator = new HashGenerator(); - - foreach ((FileSystemInfo fileOrFolder, bool scanFileContent) in filesAndFolders) + if (scanFileContent) { - if (scanFileContent) + // add each unique file's contents to the hash + // normalize the content for cr/lf and case-sensitivity + if (uniqContent.Add(fileOrFolder.FullName)) { - // add each unique file's contents to the hash - // normalize the content for cr/lf and case-sensitivity - if (uniqContent.Add(fileOrFolder.FullName)) + if (File.Exists(fileOrFolder.FullName) == false) { - if (File.Exists(fileOrFolder.FullName) == false) - { - continue; - } - - using (FileStream fileStream = File.OpenRead(fileOrFolder.FullName)) - { - var hash = fileStream.GetStreamHash(); - generator.AddCaseInsensitiveString(hash); - } + continue; } - } - else - { - // add each unique folder/file to the hash - if (uniqInfos.Add(fileOrFolder.FullName)) + + using (FileStream fileStream = File.OpenRead(fileOrFolder.FullName)) { - generator.AddFileSystemItem(fileOrFolder); + var hash = fileStream.GetStreamHash(); + generator.AddCaseInsensitiveString(hash); } } } - return generator.GenerateHash(); + else + { + // add each unique folder/file to the hash + if (uniqInfos.Add(fileOrFolder.FullName)) + { + generator.AddFileSystemItem(fileOrFolder); + } + } } - } + return generator.GenerateHash(); + } } } diff --git a/src/Umbraco.Core/Composing/RuntimeHashPaths.cs b/src/Umbraco.Core/Composing/RuntimeHashPaths.cs index eac2f83bcd..5720fdebe2 100644 --- a/src/Umbraco.Core/Composing/RuntimeHashPaths.cs +++ b/src/Umbraco.Core/Composing/RuntimeHashPaths.cs @@ -1,44 +1,43 @@ -using System.Collections.Generic; -using System.IO; using System.Reflection; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Paths used to determine the +/// +public sealed class RuntimeHashPaths { - /// - /// Paths used to determine the - /// - public sealed class RuntimeHashPaths + private readonly Dictionary _files = new(); + private readonly List _paths = new(); + + public RuntimeHashPaths AddFolder(DirectoryInfo pathInfo) { - private readonly List _paths = new List(); - private readonly Dictionary _files = new Dictionary(); - - public RuntimeHashPaths AddFolder(DirectoryInfo pathInfo) - { - _paths.Add(pathInfo); - return this; - } - - /// - /// Creates a runtime hash based on the assembly provider - /// - /// - /// - public RuntimeHashPaths AddAssemblies(IAssemblyProvider assemblyProvider) - { - foreach (Assembly assembly in assemblyProvider.Assemblies) - { - // TODO: We need to test this on a published website - if (!assembly.IsDynamic && assembly.Location != null) - { - AddFile(new FileInfo(assembly.Location)); - } - } - return this; - } - - public void AddFile(FileInfo fileInfo, bool scanFileContent = false) => _files.Add(fileInfo, scanFileContent); - - public IEnumerable GetFolders() => _paths; - public IReadOnlyDictionary GetFiles() => _files; + _paths.Add(pathInfo); + return this; } + + /// + /// Creates a runtime hash based on the assembly provider + /// + /// + /// + public RuntimeHashPaths AddAssemblies(IAssemblyProvider assemblyProvider) + { + foreach (Assembly assembly in assemblyProvider.Assemblies) + { + // TODO: We need to test this on a published website + if (!assembly.IsDynamic && assembly.Location != null) + { + AddFile(new FileInfo(assembly.Location)); + } + } + + return this; + } + + public void AddFile(FileInfo fileInfo, bool scanFileContent = false) => _files.Add(fileInfo, scanFileContent); + + public IEnumerable GetFolders() => _paths; + + public IReadOnlyDictionary GetFiles() => _files; } diff --git a/src/Umbraco.Core/Composing/SetCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/SetCollectionBuilderBase.cs index 358aab75dd..b686067d30 100644 --- a/src/Umbraco.Core/Composing/SetCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/SetCollectionBuilderBase.cs @@ -1,171 +1,207 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Implements an un-ordered collection builder. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +/// +/// +/// A set collection builder is the most basic collection builder, +/// where items are not ordered. +/// +/// +public abstract class SetCollectionBuilderBase : CollectionBuilderBase + where TBuilder : SetCollectionBuilderBase + where TCollection : class, IBuilderCollection { + protected abstract TBuilder This { get; } + /// - /// Implements an un-ordered collection builder. + /// Clears all types in the collection. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - /// - /// A set collection builder is the most basic collection builder, - /// where items are not ordered. - /// - public abstract class SetCollectionBuilderBase : CollectionBuilderBase - where TBuilder : SetCollectionBuilderBase - where TCollection : class, IBuilderCollection + /// The builder. + public TBuilder Clear() { - protected abstract TBuilder This { get; } + Configure(types => types.Clear()); + return This; + } - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() + /// + /// Adds a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Add() + where T : TItem + { + Configure(types => { - Configure(types => types.Clear()); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Add() - where T : TItem - { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var type = typeof(T); - if (types.Contains(type)) types.Remove(type); - types.Add(type); - }); - return This; - } + types.Remove(type); + } - /// - /// Adds a type to the collection. - /// - /// The type to append. - /// The builder. - public TBuilder Add(Type type) + types.Add(type); + }); + return This; + } + + /// + /// Adds a type to the collection. + /// + /// The type to append. + /// The builder. + public TBuilder Add(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "register"); + if (types.Contains(type)) { + types.Remove(type); + } + + types.Add(type); + }); + return This; + } + + /// + /// Adds types to the collections. + /// + /// The types to append. + /// The builder. + public TBuilder Add(IEnumerable types) + { + Configure(list => + { + foreach (Type type in types) + { + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast EnsureType(type, "register"); - if (types.Contains(type)) types.Remove(type); - types.Add(type); - }); - return This; - } - - /// - /// Adds types to the collections. - /// - /// The types to append. - /// The builder. - public TBuilder Add(IEnumerable types) - { - Configure(list => - { - foreach (var type in types) + if (list.Contains(type)) { - // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast - EnsureType(type, "register"); - if (list.Contains(type)) list.Remove(type); - list.Add(type); + list.Remove(type); } - }); - return This; - } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove() - where T : TItem + list.Add(type); + } + }); + return This; + } + + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove() + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var type = typeof(T); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } + }); + return This; + } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "remove"); + if (types.Contains(type)) { - EnsureType(type, "remove"); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } + }); + return This; + } - /// - /// Replaces a type in the collection. - /// - /// The type to replace. - /// The type to insert. - /// The builder. - /// Throws if the type to replace does not already belong to the collection. - public TBuilder Replace() - where TReplaced : TItem - where T : TItem + /// + /// Replaces a type in the collection. + /// + /// The type to replace. + /// The type to insert. + /// The builder. + /// Throws if the type to replace does not already belong to the collection. + public TBuilder Replace() + where TReplaced : TItem + where T : TItem + { + Configure(types => { - Configure(types => + Type typeReplaced = typeof(TReplaced); + Type type = typeof(T); + if (typeReplaced == type) { - var typeReplaced = typeof(TReplaced); - var type = typeof(T); - if (typeReplaced == type) return; + return; + } - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); + var index = types.IndexOf(typeReplaced); + if (index < 0) + { + throw new InvalidOperationException(); + } - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeReplaced); // in case removing type changed index - types.Insert(index, type); - types.Remove(typeReplaced); - }); - return This; - } + if (types.Contains(type)) + { + types.Remove(type); + } - /// - /// Replaces a type in the collection. - /// - /// The type to replace. - /// The type to insert. - /// The builder. - /// Throws if the type to replace does not already belong to the collection. - public TBuilder Replace(Type typeReplaced, Type type) + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; + } + + /// + /// Replaces a type in the collection. + /// + /// The type to replace. + /// The type to insert. + /// The builder. + /// Throws if the type to replace does not already belong to the collection. + public TBuilder Replace(Type typeReplaced, Type type) + { + Configure(types => { - Configure(types => + EnsureType(typeReplaced, "find"); + EnsureType(type, "register"); + + if (typeReplaced == type) { - EnsureType(typeReplaced, "find"); - EnsureType(type, "register"); + return; + } - if (typeReplaced == type) return; + var index = types.IndexOf(typeReplaced); + if (index < 0) + { + throw new InvalidOperationException(); + } - var index = types.IndexOf(typeReplaced); - if (index < 0) throw new InvalidOperationException(); + if (types.Contains(type)) + { + types.Remove(type); + } - if (types.Contains(type)) types.Remove(type); - index = types.IndexOf(typeReplaced); // in case removing type changed index - types.Insert(index, type); - types.Remove(typeReplaced); - }); - return This; - } + index = types.IndexOf(typeReplaced); // in case removing type changed index + types.Insert(index, type); + types.Remove(typeReplaced); + }); + return This; } } diff --git a/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs index 40ce3d8a46..072a9d99e3 100644 --- a/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs @@ -1,69 +1,71 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides a base class for collections of types. +/// +public abstract class + TypeCollectionBuilderBase : ICollectionBuilder + where TBuilder : TypeCollectionBuilderBase + where TCollection : class, IBuilderCollection { - /// - /// Provides a base class for collections of types. - /// - public abstract class TypeCollectionBuilderBase : ICollectionBuilder - where TBuilder : TypeCollectionBuilderBase - where TCollection : class, IBuilderCollection + private readonly HashSet _types = new(); + + protected abstract TBuilder This { get; } + + public TCollection CreateCollection(IServiceProvider factory) + => factory.CreateInstance(CreateItemsFactory()); + + public void RegisterWith(IServiceCollection services) + => services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, ServiceLifetime.Singleton)); + + public TBuilder Add(Type type) { - private readonly HashSet _types = new HashSet(); - - protected abstract TBuilder This { get; } - - private static Type Validate(Type type, string action) - { - if (!typeof(TConstraint).IsAssignableFrom(type)) - throw new InvalidOperationException($"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TConstraint).FullName}."); - return type; - } - - public TBuilder Add(Type type) - { - _types.Add(Validate(type, "add")); - return This; - } - - public TBuilder Add() - { - Add(typeof(T)); - return This; - } - - public TBuilder Add(IEnumerable types) - { - foreach (var type in types) - { - Add(type); - } - - return This; - } - - public TBuilder Remove(Type type) - { - _types.Remove(Validate(type, "remove")); - return This; - } - - public TBuilder Remove() - { - Remove(typeof(T)); - return This; - } - - public TCollection CreateCollection(IServiceProvider factory) - => factory.CreateInstance(CreateItemsFactory()); - - public void RegisterWith(IServiceCollection services) - => services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, ServiceLifetime.Singleton)); - - // used to resolve a Func> parameter - private Func> CreateItemsFactory() => () => _types; + _types.Add(Validate(type, "add")); + return This; } + + private static Type Validate(Type type, string action) + { + if (!typeof(TConstraint).IsAssignableFrom(type)) + { + throw new InvalidOperationException( + $"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TConstraint).FullName}."); + } + + return type; + } + + public TBuilder Add() + { + Add(typeof(T)); + return This; + } + + public TBuilder Add(IEnumerable types) + { + foreach (Type type in types) + { + Add(type); + } + + return This; + } + + public TBuilder Remove(Type type) + { + _types.Remove(Validate(type, "remove")); + return This; + } + + public TBuilder Remove() + { + Remove(typeof(T)); + return This; + } + + // used to resolve a Func> parameter + private Func> CreateItemsFactory() => () => _types; } diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index dfeac6a731..3ac826880c 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Security; using System.Text; @@ -9,495 +6,502 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Configuration.UmbracoSettings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +public class TypeFinder : ITypeFinder { + // TODO: Kill this - /// - public class TypeFinder : ITypeFinder + /// + /// this is our assembly filter to filter out known types that def don't contain types we'd like to find or plugins + /// + /// + /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then + /// its an exact name match + /// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll" + /// + internal static readonly string[] KnownAssemblyExclusionFilter = { - private readonly ILogger _logger; - private readonly IAssemblyProvider _assemblyProvider; - private volatile HashSet? _localFilteredAssemblyCache; - private readonly object _localFilteredAssemblyCacheLocker = new object(); - private readonly List _notifiedLoadExceptionAssemblies = new List(); - private static readonly ConcurrentDictionary s_typeNamesCache = new ConcurrentDictionary(); + "mscorlib,", "netstandard,", "System,", "Antlr3.", "AutoMapper,", "AutoMapper.", "Autofac,", // DI + "Autofac.", "AzureDirectory,", "Castle.", // DI, tests + "ClientDependency.", "CookComputing.", "CSharpTest.", // BTree for NuCache + "DataAnnotationsExtensions,", "DataAnnotationsExtensions.", "Dynamic,", "Examine,", "Examine.", + "HtmlAgilityPack,", "HtmlAgilityPack.", "HtmlDiff,", "ICSharpCode.", "Iesi.Collections,", // used by NHibernate + "JetBrains.Annotations,", "LightInject.", // DI + "LightInject,", "Lucene.", "Markdown,", "Microsoft.", "MiniProfiler,", "Moq,", "MySql.", "NHibernate,", + "NHibernate.", "Newtonsoft.", "NPoco,", "NuGet.", "RouteDebugger,", "Semver.", "Serilog.", "Serilog,", + "ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog + "System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.", + "Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite", + "ReSharperTestRunner32", // used by resharper testrunner + }; - private readonly ITypeFinderConfig? _typeFinderConfig; - // used for benchmark tests - internal bool QueryWithReferencingAssemblies { get; set; } = true; + private static readonly ConcurrentDictionary TypeNamesCache = new(); - public TypeFinder(ILogger logger, IAssemblyProvider assemblyProvider, ITypeFinderConfig? typeFinderConfig = null) + private readonly IAssemblyProvider _assemblyProvider; + private readonly object _localFilteredAssemblyCacheLocker = new(); + private readonly ILogger _logger; + private readonly List _notifiedLoadExceptionAssemblies = new(); + + private readonly ITypeFinderConfig? _typeFinderConfig; + + private string[]? _assembliesAcceptingLoadExceptions; + private volatile HashSet? _localFilteredAssemblyCache; + + public TypeFinder(ILogger logger, IAssemblyProvider assemblyProvider, ITypeFinderConfig? typeFinderConfig = null) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _assemblyProvider = assemblyProvider; + _typeFinderConfig = typeFinderConfig; + } + + /// + public IEnumerable AssembliesToScan + { + get { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _assemblyProvider = assemblyProvider; - _typeFinderConfig = typeFinderConfig; - } - - private string[]? _assembliesAcceptingLoadExceptions = null; - - private string[] AssembliesAcceptingLoadExceptions - { - get + lock (_localFilteredAssemblyCacheLocker) { - if (_assembliesAcceptingLoadExceptions is not null) + if (_localFilteredAssemblyCache != null) { - return _assembliesAcceptingLoadExceptions; - } - - _assembliesAcceptingLoadExceptions = - _typeFinderConfig?.AssembliesAcceptingLoadExceptions.Where(x => !x.IsNullOrWhiteSpace()).ToArray() ?? - Array.Empty(); - - return _assembliesAcceptingLoadExceptions; - } - } - - - private bool AcceptsLoadExceptions(Assembly a) - { - if (AssembliesAcceptingLoadExceptions.Length == 0) - return false; - if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*") - return true; - var name = a.GetName().Name; // simple name of the assembly - return AssembliesAcceptingLoadExceptions.Any(pattern => - { - if (pattern.Length > name?.Length) - return false; // pattern longer than name - if (pattern.Length == name?.Length) - return pattern.InvariantEquals(name); // same length, must be identical - if (pattern[pattern.Length] != '.') - return false; // pattern is shorter than name, must end with dot - return name?.StartsWith(pattern) ?? false; // and name must start with pattern - }); - } - - - private IEnumerable GetAllAssemblies() => _assemblyProvider.Assemblies; - - /// - public IEnumerable AssembliesToScan - { - get - { - lock (_localFilteredAssemblyCacheLocker) - { - if (_localFilteredAssemblyCache != null) - return _localFilteredAssemblyCache; - - var assemblies = GetFilteredAssemblies(null, KnownAssemblyExclusionFilter); - _localFilteredAssemblyCache = new HashSet(assemblies); return _localFilteredAssemblyCache; } + + IEnumerable assemblies = GetFilteredAssemblies(null, KnownAssemblyExclusionFilter); + _localFilteredAssemblyCache = new HashSet(assemblies); + return _localFilteredAssemblyCache; } } - - /// - /// Return a distinct list of found local Assemblies and excluding the ones passed in and excluding the exclusion list filter - /// - /// - /// - /// - private IEnumerable GetFilteredAssemblies( - IEnumerable? excludeFromResults = null, - string[]? exclusionFilter = null) - { - if (excludeFromResults == null) - excludeFromResults = new HashSet(); - if (exclusionFilter == null) - exclusionFilter = new string[] { }; - - return GetAllAssemblies() - .Where(x => excludeFromResults.Contains(x) == false - && x.GlobalAssemblyCache == false - && exclusionFilter.Any(f => x.FullName?.StartsWith(f) ?? false) == false); - } - - // TODO: Kill this - - /// - /// this is our assembly filter to filter out known types that def don't contain types we'd like to find or plugins - /// - /// - /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match - /// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll" - /// - internal static readonly string[] KnownAssemblyExclusionFilter = { - "mscorlib,", - "netstandard,", - "System,", - "Antlr3.", - "AutoMapper,", - "AutoMapper.", - "Autofac,", // DI - "Autofac.", - "AzureDirectory,", - "Castle.", // DI, tests - "ClientDependency.", - "CookComputing.", - "CSharpTest.", // BTree for NuCache - "DataAnnotationsExtensions,", - "DataAnnotationsExtensions.", - "Dynamic,", - "Examine,", - "Examine.", - "HtmlAgilityPack,", - "HtmlAgilityPack.", - "HtmlDiff,", - "ICSharpCode.", - "Iesi.Collections,", // used by NHibernate - "JetBrains.Annotations,", - "LightInject.", // DI - "LightInject,", - "Lucene.", - "Markdown,", - "Microsoft.", - "MiniProfiler,", - "Moq,", - "MySql.", - "NHibernate,", - "NHibernate.", - "Newtonsoft.", - "NPoco,", - "NuGet.", - "RouteDebugger,", - "Semver.", - "Serilog.", - "Serilog,", - "ServiceStack.", - "SqlCE4Umbraco,", - "Superpower,", // used by Serilog - "System.", - "TidyNet,", - "TidyNet.", - "WebDriver,", - "itextsharp,", - "mscorlib,", - "NUnit,", - "NUnit.", - "NUnit3.", - "Selenium.", - "ImageProcessor", - "MiniProfiler.", - "Owin,", - "SQLite", - "ReSharperTestRunner32" // used by resharper testrunner - }; - - /// - /// Finds any classes derived from the assignTypeFrom Type that contain the attribute TAttribute - /// - /// - /// - /// - /// - /// - public IEnumerable FindClassesOfTypeWithAttribute( - Type assignTypeFrom, - Type attributeType, - IEnumerable? assemblies = null, - bool onlyConcreteClasses = true) - { - var assemblyList = assemblies ?? AssembliesToScan; - - return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses, - //the additional filter will ensure that any found types also have the attribute applied. - t => t.GetCustomAttributes(attributeType, false).Any()); - } - - /// - /// Returns all types found of in the assemblies specified of type T - /// - /// - /// - /// - /// - public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) - { - var assemblyList = assemblies ?? AssembliesToScan; - - return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses); - } - - /// - /// Finds any classes with the attribute. - /// - /// The attribute type - /// The assemblies. - /// if set to true only concrete classes. - /// - public IEnumerable FindClassesWithAttribute( - Type attributeType, - IEnumerable? assemblies = null, - bool onlyConcreteClasses = true) - { - var assemblyList = assemblies ?? AssembliesToScan; - - return GetClassesWithAttribute(attributeType, assemblyList, onlyConcreteClasses); - } - - /// - /// Returns a Type for the string type name - /// - /// - /// - public virtual Type? GetTypeByName(string name) - { - - //NOTE: This will not find types in dynamic assemblies unless those assemblies are already loaded - //into the appdomain. - - - // This is exactly what the BuildManager does, if the type is an assembly qualified type - // name it will find it. - if (TypeNameContainsAssembly(name)) - { - return Type.GetType(name); - } - - // It didn't parse, so try loading from each already loaded assembly and cache it - return s_typeNamesCache.GetOrAdd(name, s => - AppDomain.CurrentDomain.GetAssemblies() - .Select(x => x.GetType(s)) - .FirstOrDefault(x => x != null)); - } - - #region Private methods - - // borrowed from aspnet System.Web.UI.Util - private static bool TypeNameContainsAssembly(string typeName) - { - return CommaIndexInTypeName(typeName) > 0; - } - - // borrowed from aspnet System.Web.UI.Util - private static int CommaIndexInTypeName(string typeName) - { - var num1 = typeName.LastIndexOf(','); - if (num1 < 0) - return -1; - var num2 = typeName.LastIndexOf(']'); - if (num2 > num1) - return -1; - return typeName.IndexOf(',', num2 + 1); - } - - private IEnumerable GetClassesWithAttribute( - Type attributeType, - IEnumerable assemblies, - bool onlyConcreteClasses) - { - if (typeof(Attribute).IsAssignableFrom(attributeType) == false) - throw new ArgumentException("Type " + attributeType + " is not an Attribute type."); - - var candidateAssemblies = new HashSet(assemblies); - var attributeAssemblyIsCandidate = candidateAssemblies.Contains(attributeType.Assembly); - candidateAssemblies.Remove(attributeType.Assembly); - var types = new List(); - - var stack = new Stack(); - stack.Push(attributeType.Assembly); - - if (!QueryWithReferencingAssemblies) - { - foreach (var a in candidateAssemblies) - stack.Push(a); - } - - while (stack.Count > 0) - { - var assembly = stack.Pop(); - - IReadOnlyList? assemblyTypes = null; - if (assembly != attributeType.Assembly || attributeAssemblyIsCandidate) - { - // get all assembly types that can be assigned to baseType - try - { - assemblyTypes = GetTypesWithFormattedException(assembly) - .ToList(); // in try block - } - catch (TypeLoadException ex) - { - _logger.LogError(ex, "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly); - continue; - } - - types.AddRange(assemblyTypes.Where(x => - x.IsClass // only classes - && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed - && x.IsNestedPrivate == false // exclude nested private - && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract - && x.GetCustomAttribute() == null // exclude hidden - && x.GetCustomAttributes(attributeType, false).Any())); // marked with the attribute - } - - if (assembly != attributeType.Assembly && assemblyTypes?.Where(attributeType.IsAssignableFrom).Any() == false) - continue; - - if (QueryWithReferencingAssemblies) - { - foreach (var referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) - { - candidateAssemblies.Remove(referencing); - stack.Push(referencing); - } - } - } - - return types; - } - - /// - /// Finds types that are assignable from the assignTypeFrom parameter and will scan for these types in the assembly - /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type - /// deriving from the base type. - /// - /// - /// - /// - /// An additional filter to apply for what types will actually be included in the return value - /// - private IEnumerable GetClassesWithBaseType( - Type baseType, - IEnumerable assemblies, - bool onlyConcreteClasses, - Func? additionalFilter = null) - { - var candidateAssemblies = new HashSet(assemblies); - var baseTypeAssemblyIsCandidate = candidateAssemblies.Contains(baseType.Assembly); - candidateAssemblies.Remove(baseType.Assembly); - var types = new List(); - - var stack = new Stack(); - stack.Push(baseType.Assembly); - - if (!QueryWithReferencingAssemblies) - { - foreach (var a in candidateAssemblies) - stack.Push(a); - } - - while (stack.Count > 0) - { - var assembly = stack.Pop(); - - // get all assembly types that can be assigned to baseType - IReadOnlyList? assemblyTypes = null; - if (assembly != baseType.Assembly || baseTypeAssemblyIsCandidate) - { - try - { - assemblyTypes = GetTypesWithFormattedException(assembly) - .Where(baseType.IsAssignableFrom) - .ToList(); // in try block - } - catch (TypeLoadException ex) - { - _logger.LogError(ex, "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", assembly); - continue; - } - - types.AddRange(assemblyTypes.Where(x => - x.IsClass // only classes - && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed - && x.IsNestedPrivate == false // exclude nested private - && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract - && x.GetCustomAttribute(false) == null // exclude hidden - && (additionalFilter == null || additionalFilter(x)))); // filter - } - - if (assembly != baseType.Assembly && (assemblyTypes?.All(x => x.IsSealed) ?? false)) - continue; - - if (QueryWithReferencingAssemblies) - { - foreach (var referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) - { - candidateAssemblies.Remove(referencing); - stack.Push(referencing); - } - } - } - - return types; - } - - private IEnumerable GetTypesWithFormattedException(Assembly a) - { - //if the assembly is dynamic, do not try to scan it - if (a.IsDynamic) - return Enumerable.Empty(); - - var getAll = a.GetCustomAttribute() == null; - - try - { - //we need to detect if an assembly is partially trusted, if so we cannot go interrogating all of it's types - //only its exported types, otherwise we'll get exceptions. - return getAll ? a.GetTypes() : a.GetExportedTypes(); - } - catch (TypeLoadException ex) // GetExportedTypes *can* throw TypeLoadException! - { - var sb = new StringBuilder(); - AppendCouldNotLoad(sb, a, getAll); - AppendLoaderException(sb, ex); - - // rethrow as ReflectionTypeLoadException (for consistency) with new message - throw new ReflectionTypeLoadException(new Type[0], new Exception[] { ex }, sb.ToString()); - } - catch (ReflectionTypeLoadException rex) // GetTypes throws ReflectionTypeLoadException - { - var sb = new StringBuilder(); - AppendCouldNotLoad(sb, a, getAll); - foreach (var loaderException in rex.LoaderExceptions.WhereNotNull()) - AppendLoaderException(sb, loaderException); - - var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); - - // rethrow with new message, unless accepted - if (AcceptsLoadExceptions(a) == false) - throw ex; - - // log a warning, and return what we can - lock (_notifiedLoadExceptionAssemblies) - { - if (a.FullName is not null && _notifiedLoadExceptionAssemblies.Contains(a.FullName) == false) - { - _notifiedLoadExceptionAssemblies.Add(a.FullName); - _logger.LogWarning(ex, "Could not load all types from {TypeName}.", a.GetName().Name); - } - } - return rex.Types.WhereNotNull().ToArray(); - } - } - - private static void AppendCouldNotLoad(StringBuilder sb, Assembly a, bool getAll) - { - sb.Append("Could not load "); - sb.Append(getAll ? "all" : "exported"); - sb.Append(" types from \""); - sb.Append(a.FullName); - sb.AppendLine("\" due to LoaderExceptions, skipping:"); - } - - private static void AppendLoaderException(StringBuilder sb, Exception loaderException) - { - sb.Append(". "); - sb.Append(loaderException.GetType().FullName); - - if (loaderException is TypeLoadException tloadex) - { - sb.Append(" on "); - sb.Append(tloadex.TypeName); - } - - sb.Append(": "); - sb.Append(loaderException.Message); - sb.AppendLine(); - } - - #endregion - } + + // used for benchmark tests + internal bool QueryWithReferencingAssemblies { get; set; } = true; + + private string[] AssembliesAcceptingLoadExceptions + { + get + { + if (_assembliesAcceptingLoadExceptions is not null) + { + return _assembliesAcceptingLoadExceptions; + } + + _assembliesAcceptingLoadExceptions = + _typeFinderConfig?.AssembliesAcceptingLoadExceptions.Where(x => !x.IsNullOrWhiteSpace()).ToArray() ?? + Array.Empty(); + + return _assembliesAcceptingLoadExceptions; + } + } + + /// + /// Finds any classes derived from the assignTypeFrom Type that contain the attribute TAttribute + /// + /// + /// + /// + /// + /// + public IEnumerable FindClassesOfTypeWithAttribute( + Type assignTypeFrom, + Type attributeType, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + { + IEnumerable assemblyList = assemblies ?? AssembliesToScan; + + return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses, + + // the additional filter will ensure that any found types also have the attribute applied. + t => t.GetCustomAttributes(attributeType, false).Any()); + } + + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + /// + public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) + { + IEnumerable assemblyList = assemblies ?? AssembliesToScan; + + return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses); + } + + /// + /// Finds any classes with the attribute. + /// + /// The attribute type + /// The assemblies. + /// if set to true only concrete classes. + /// + public IEnumerable FindClassesWithAttribute( + Type attributeType, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + { + IEnumerable assemblyList = assemblies ?? AssembliesToScan; + + return GetClassesWithAttribute(attributeType, assemblyList, onlyConcreteClasses); + } + + /// + /// Returns a Type for the string type name + /// + /// + /// + public virtual Type? GetTypeByName(string name) + { + // NOTE: This will not find types in dynamic assemblies unless those assemblies are already loaded + // into the appdomain. + + // This is exactly what the BuildManager does, if the type is an assembly qualified type + // name it will find it. + if (TypeNameContainsAssembly(name)) + { + return Type.GetType(name); + } + + // It didn't parse, so try loading from each already loaded assembly and cache it + return TypeNamesCache.GetOrAdd(name, s => + AppDomain.CurrentDomain.GetAssemblies() + .Select(x => x.GetType(s)) + .FirstOrDefault(x => x != null)); + } + + #region Private methods + + // borrowed from aspnet System.Web.UI.Util + private static bool TypeNameContainsAssembly(string typeName) => CommaIndexInTypeName(typeName) > 0; + + private bool AcceptsLoadExceptions(Assembly a) + { + if (AssembliesAcceptingLoadExceptions.Length == 0) + { + return false; + } + + if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*") + { + return true; + } + + var name = a.GetName().Name; // simple name of the assembly + return AssembliesAcceptingLoadExceptions.Any(pattern => + { + if (pattern.Length > name?.Length) + { + return false; // pattern longer than name + } + + if (pattern.Length == name?.Length) + { + return pattern.InvariantEquals(name); // same length, must be identical + } + + if (pattern[pattern.Length] != '.') + { + return false; // pattern is shorter than name, must end with dot + } + + return name?.StartsWith(pattern) ?? false; // and name must start with pattern + }); + } + + private IEnumerable GetAllAssemblies() => _assemblyProvider.Assemblies; + + /// + /// Return a distinct list of found local Assemblies and excluding the ones passed in and excluding the exclusion list + /// filter + /// + /// + /// + /// + private IEnumerable GetFilteredAssemblies( + IEnumerable? excludeFromResults = null, + string[]? exclusionFilter = null) + { + if (excludeFromResults == null) + { + excludeFromResults = new HashSet(); + } + + if (exclusionFilter == null) + { + exclusionFilter = new string[] { }; + } + + return GetAllAssemblies() + .Where(x => excludeFromResults.Contains(x) == false + && exclusionFilter.Any(f => x.FullName?.StartsWith(f) ?? false) == false); + } + + // borrowed from aspnet System.Web.UI.Util + private static int CommaIndexInTypeName(string typeName) + { + var num1 = typeName.LastIndexOf(','); + if (num1 < 0) + { + return -1; + } + + var num2 = typeName.LastIndexOf(']'); + if (num2 > num1) + { + return -1; + } + + return typeName.IndexOf(',', num2 + 1); + } + + private static void AppendCouldNotLoad(StringBuilder sb, Assembly a, bool getAll) + { + sb.Append("Could not load "); + sb.Append(getAll ? "all" : "exported"); + sb.Append(" types from \""); + sb.Append(a.FullName); + sb.AppendLine("\" due to LoaderExceptions, skipping:"); + } + + private IEnumerable GetClassesWithAttribute( + Type attributeType, + IEnumerable assemblies, + bool onlyConcreteClasses) + { + if (typeof(Attribute).IsAssignableFrom(attributeType) == false) + { + throw new ArgumentException("Type " + attributeType + " is not an Attribute type."); + } + + var candidateAssemblies = new HashSet(assemblies); + var attributeAssemblyIsCandidate = candidateAssemblies.Contains(attributeType.Assembly); + candidateAssemblies.Remove(attributeType.Assembly); + var types = new List(); + + var stack = new Stack(); + stack.Push(attributeType.Assembly); + + if (!QueryWithReferencingAssemblies) + { + foreach (Assembly a in candidateAssemblies) + { + stack.Push(a); + } + } + + while (stack.Count > 0) + { + Assembly assembly = stack.Pop(); + + IReadOnlyList? assemblyTypes = null; + if (assembly != attributeType.Assembly || attributeAssemblyIsCandidate) + { + // get all assembly types that can be assigned to baseType + try + { + assemblyTypes = GetTypesWithFormattedException(assembly) + .ToList(); // in try block + } + catch (TypeLoadException ex) + { + _logger.LogError( + ex, + "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", + assembly); + continue; + } + + types.AddRange(assemblyTypes.Where(x => + x.IsClass // only classes + && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed + && x.IsNestedPrivate == false // exclude nested private + && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract + && x.GetCustomAttribute() == null // exclude hidden + && x.GetCustomAttributes(attributeType, false).Any())); // marked with the attribute + } + + if (assembly != attributeType.Assembly && + assemblyTypes?.Where(attributeType.IsAssignableFrom).Any() == false) + { + continue; + } + + if (QueryWithReferencingAssemblies) + { + foreach (Assembly referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) + { + candidateAssemblies.Remove(referencing); + stack.Push(referencing); + } + } + } + + return types; + } + + /// + /// Finds types that are assignable from the assignTypeFrom parameter and will scan for these types in the assembly + /// list passed in, however we will only scan assemblies that have a reference to the assignTypeFrom Type or any type + /// deriving from the base type. + /// + /// + /// + /// + /// + /// An additional filter to apply for what types will actually be included in the return + /// value + /// + /// + private IEnumerable GetClassesWithBaseType( + Type baseType, + IEnumerable assemblies, + bool onlyConcreteClasses, + Func? additionalFilter = null) + { + var candidateAssemblies = new HashSet(assemblies); + var baseTypeAssemblyIsCandidate = candidateAssemblies.Contains(baseType.Assembly); + candidateAssemblies.Remove(baseType.Assembly); + var types = new List(); + + var stack = new Stack(); + stack.Push(baseType.Assembly); + + if (!QueryWithReferencingAssemblies) + { + foreach (Assembly a in candidateAssemblies) + { + stack.Push(a); + } + } + + while (stack.Count > 0) + { + Assembly assembly = stack.Pop(); + + // get all assembly types that can be assigned to baseType + IReadOnlyList? assemblyTypes = null; + if (assembly != baseType.Assembly || baseTypeAssemblyIsCandidate) + { + try + { + assemblyTypes = GetTypesWithFormattedException(assembly) + .Where(baseType.IsAssignableFrom) + .ToList(); // in try block + } + catch (TypeLoadException ex) + { + _logger.LogError( + ex, + "Could not query types on {Assembly} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", + assembly); + continue; + } + + types.AddRange(assemblyTypes.Where(x => + x.IsClass // only classes + && (x.IsAbstract == false || x.IsSealed == false) // ie non-static, static is abstract and sealed + && x.IsNestedPrivate == false // exclude nested private + && (onlyConcreteClasses == false || x.IsAbstract == false) // exclude abstract + && x.GetCustomAttribute(false) == null // exclude hidden + && (additionalFilter == null || additionalFilter(x)))); // filter + } + + if (assembly != baseType.Assembly && (assemblyTypes?.All(x => x.IsSealed) ?? false)) + { + continue; + } + + if (QueryWithReferencingAssemblies) + { + foreach (Assembly referencing in TypeHelper.GetReferencingAssemblies(assembly, candidateAssemblies)) + { + candidateAssemblies.Remove(referencing); + stack.Push(referencing); + } + } + } + + return types; + } + + private IEnumerable GetTypesWithFormattedException(Assembly a) + { + // if the assembly is dynamic, do not try to scan it + if (a.IsDynamic) + { + return Enumerable.Empty(); + } + + var getAll = a.GetCustomAttribute() == null; + + try + { + // we need to detect if an assembly is partially trusted, if so we cannot go interrogating all of it's types + // only its exported types, otherwise we'll get exceptions. + return getAll ? a.GetTypes() : a.GetExportedTypes(); + } + + // GetExportedTypes *can* throw TypeLoadException! + catch (TypeLoadException ex) + { + var sb = new StringBuilder(); + AppendCouldNotLoad(sb, a, getAll); + AppendLoaderException(sb, ex); + + // rethrow as ReflectionTypeLoadException (for consistency) with new message + throw new ReflectionTypeLoadException(new Type[0], new Exception[] { ex }, sb.ToString()); + } + + // GetTypes throws ReflectionTypeLoadException + catch (ReflectionTypeLoadException rex) + { + var sb = new StringBuilder(); + AppendCouldNotLoad(sb, a, getAll); + foreach (Exception loaderException in rex.LoaderExceptions.WhereNotNull()) + { + AppendLoaderException(sb, loaderException); + } + + var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); + + // rethrow with new message, unless accepted + if (AcceptsLoadExceptions(a) == false) + { + throw ex; + } + + // log a warning, and return what we can + lock (_notifiedLoadExceptionAssemblies) + { + if (a.FullName is not null && _notifiedLoadExceptionAssemblies.Contains(a.FullName) == false) + { + _notifiedLoadExceptionAssemblies.Add(a.FullName); + _logger.LogWarning(ex, "Could not load all types from {TypeName}.", a.GetName().Name); + } + } + + return rex.Types.WhereNotNull().ToArray(); + } + } + + private static void AppendLoaderException(StringBuilder sb, Exception loaderException) + { + sb.Append(". "); + sb.Append(loaderException.GetType().FullName); + + if (loaderException is TypeLoadException tloadex) + { + sb.Append(" on "); + sb.Append(tloadex.TypeName); + } + + sb.Append(": "); + sb.Append(loaderException.Message); + sb.AppendLine(); + } + + #endregion } diff --git a/src/Umbraco.Core/Composing/TypeFinderConfig.cs b/src/Umbraco.Core/Composing/TypeFinderConfig.cs index 4b5271039f..2fd9283500 100644 --- a/src/Umbraco.Core/Composing/TypeFinderConfig.cs +++ b/src/Umbraco.Core/Composing/TypeFinderConfig.cs @@ -1,36 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// TypeFinder config via appSettings +/// +public class TypeFinderConfig : ITypeFinderConfig { - /// - /// TypeFinder config via appSettings - /// - public class TypeFinderConfig : ITypeFinderConfig + private readonly TypeFinderSettings _settings; + private IEnumerable? _assembliesAcceptingLoadExceptions; + + public TypeFinderConfig(IOptions settings) => _settings = settings.Value; + + public IEnumerable AssembliesAcceptingLoadExceptions { - private readonly TypeFinderSettings _settings; - private IEnumerable? _assembliesAcceptingLoadExceptions; - - public TypeFinderConfig(IOptions settings) => _settings = settings.Value; - - public IEnumerable AssembliesAcceptingLoadExceptions + get { - get + if (_assembliesAcceptingLoadExceptions != null) { - if (_assembliesAcceptingLoadExceptions != null) - { - return _assembliesAcceptingLoadExceptions; - } - - var s = _settings.AssembliesAcceptingLoadExceptions; - return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s) - ? Array.Empty() - : s.Split(',').Select(x => x.Trim()).ToArray(); + return _assembliesAcceptingLoadExceptions; } + + var s = _settings.AssembliesAcceptingLoadExceptions; + return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s) + ? Array.Empty() + : s.Split(',').Select(x => x.Trim()).ToArray(); } } } diff --git a/src/Umbraco.Core/Composing/TypeFinderExtensions.cs b/src/Umbraco.Core/Composing/TypeFinderExtensions.cs index adb920b64a..c67d935716 100644 --- a/src/Umbraco.Core/Composing/TypeFinderExtensions.cs +++ b/src/Umbraco.Core/Composing/TypeFinderExtensions.cs @@ -1,46 +1,52 @@ -using System; -using System.Collections.Generic; using System.Reflection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TypeFinderExtensions { - public static class TypeFinderExtensions - { - /// - /// Finds any classes derived from the type T that contain the attribute TAttribute - /// - /// - /// - /// - /// - /// - /// - public static IEnumerable FindClassesOfTypeWithAttribute(this ITypeFinder typeFinder, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) - where TAttribute : Attribute - => typeFinder.FindClassesOfTypeWithAttribute(typeof(T), typeof(TAttribute), assemblies, onlyConcreteClasses); + /// + /// Finds any classes derived from the type T that contain the attribute TAttribute + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable FindClassesOfTypeWithAttribute( + this ITypeFinder typeFinder, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + where TAttribute : Attribute + => typeFinder.FindClassesOfTypeWithAttribute(typeof(T), typeof(TAttribute), assemblies, onlyConcreteClasses); - /// - /// Returns all types found of in the assemblies specified of type T - /// - /// - /// - /// - /// - /// - public static IEnumerable FindClassesOfType(this ITypeFinder typeFinder, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) - => typeFinder.FindClassesOfType(typeof(T), assemblies, onlyConcreteClasses); + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + /// + /// + public static IEnumerable FindClassesOfType( + this ITypeFinder typeFinder, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + => typeFinder.FindClassesOfType(typeof(T), assemblies, onlyConcreteClasses); - /// - /// Finds the classes with attribute. - /// - /// - /// - /// The assemblies. - /// if set to true only concrete classes. - /// - public static IEnumerable FindClassesWithAttribute(this ITypeFinder typeFinder, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) - where T : Attribute - => typeFinder.FindClassesWithAttribute(typeof(T), assemblies, onlyConcreteClasses); - } + /// + /// Finds the classes with attribute. + /// + /// + /// + /// The assemblies. + /// if set to true only concrete classes. + /// + public static IEnumerable FindClassesWithAttribute( + this ITypeFinder typeFinder, + IEnumerable? assemblies = null, + bool onlyConcreteClasses = true) + where T : Attribute + => typeFinder.FindClassesWithAttribute(typeof(T), assemblies, onlyConcreteClasses); } diff --git a/src/Umbraco.Core/Composing/TypeHelper.cs b/src/Umbraco.Core/Composing/TypeHelper.cs index 08893732a8..6cb5426f77 100644 --- a/src/Umbraco.Core/Composing/TypeHelper.cs +++ b/src/Umbraco.Core/Composing/TypeHelper.cs @@ -1,395 +1,414 @@ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Reflection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// A utility class for type checking, this provides internal caching so that calls to these methods will be faster +/// than doing a manual type check in c# +/// +public static class TypeHelper { + private static readonly ConcurrentDictionary, PropertyInfo[]> GetPropertiesCache + = new(); + + private static readonly ConcurrentDictionary GetFieldsCache = new(); + + private static readonly Assembly[] EmptyAssemblies = new Assembly[0]; /// - /// A utility class for type checking, this provides internal caching so that calls to these methods will be faster - /// than doing a manual type check in c# + /// Based on a type we'll check if it is IEnumerable{T} (or similar) and if so we'll return a List{T}, this will also + /// deal with array types and return List{T} for those too. + /// If it cannot be done, null is returned. /// - public static class TypeHelper + public static IList? CreateGenericEnumerableFromObject(object? obj) { - private static readonly ConcurrentDictionary, PropertyInfo[]> GetPropertiesCache - = new ConcurrentDictionary, PropertyInfo[]>(); - private static readonly ConcurrentDictionary GetFieldsCache - = new ConcurrentDictionary(); - - private static readonly Assembly[] EmptyAssemblies = new Assembly[0]; - - - - /// - /// Based on a type we'll check if it is IEnumerable{T} (or similar) and if so we'll return a List{T}, this will also deal with array types and return List{T} for those too. - /// If it cannot be done, null is returned. - /// - public static IList? CreateGenericEnumerableFromObject(object? obj) + if (obj is null) { - if (obj is null) - { - return null; - } - - var type = obj.GetType(); - - if (type.IsGenericType) - { - var genericTypeDef = type.GetGenericTypeDefinition(); - - if (genericTypeDef == typeof(IEnumerable<>) - || genericTypeDef == typeof(ICollection<>) - || genericTypeDef == typeof(Collection<>) - || genericTypeDef == typeof(IList<>) - || genericTypeDef == typeof(List<>) - //this will occur when Linq is used and we get the odd WhereIterator or DistinctIterators since those are special iterator types - || obj is IEnumerable) - { - //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> - var genericType = typeof(List<>).MakeGenericType(type.GetGenericArguments()); - //pass in obj to fill the list - return (IList?)Activator.CreateInstance(genericType, obj); - } - } - - if (type.IsArray) - { - //if its an array, we'll use a List<> - var typeArguments = type.GetElementType(); - if (typeArguments is not null) - { - Type genericType = typeof(List<>).MakeGenericType(typeArguments); - //pass in obj to fill the list - return (IList?)Activator.CreateInstance(genericType, obj); - } - } - return null; } - /// - /// Checks if the method is actually overriding a base method - /// - /// - /// - public static bool IsOverride(MethodInfo m) + Type type = obj.GetType(); + + if (type.IsGenericType) { - return m.GetBaseDefinition().DeclaringType != m.DeclaringType; - } + Type genericTypeDef = type.GetGenericTypeDefinition(); - /// - /// Find all assembly references that are referencing the assignTypeFrom Type's assembly found in the assemblyList - /// - /// The referenced assembly. - /// A list of assemblies. - /// - /// - /// If the assembly of the assignTypeFrom Type is in the App_Code assembly, then we return nothing since things cannot - /// reference that assembly, same with the global.asax assembly. - /// - public static IReadOnlyList GetReferencingAssemblies(Assembly assembly, IEnumerable assemblies) - { - if (assembly.IsDynamic || assembly.IsAppCodeAssembly() || assembly.IsGlobalAsaxAssembly()) - return EmptyAssemblies; + if (genericTypeDef == typeof(IEnumerable<>) + || genericTypeDef == typeof(ICollection<>) + || genericTypeDef == typeof(Collection<>) + || genericTypeDef == typeof(IList<>) + || genericTypeDef == typeof(List<>) - - // find all assembly references that are referencing the current type's assembly since we - // should only be scanning those assemblies because any other assembly will definitely not - // contain sub type's of the one we're currently looking for - var name = assembly.GetName().Name; - return assemblies.Where(x => x == assembly || name is not null ? HasReference(x, name!) : false).ToList(); - } - - /// - /// Determines if an assembly references another assembly. - /// - /// - /// - /// - public static bool HasReference(Assembly assembly, string name) - { - // ReSharper disable once LoopCanBeConvertedToQuery - no! - foreach (var a in assembly.GetReferencedAssemblies()) + // this will occur when Linq is used and we get the odd WhereIterator or DistinctIterators since those are special iterator types + || obj is IEnumerable) { - if (string.Equals(a.Name, name, StringComparison.Ordinal)) return true; + // if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + Type genericType = typeof(List<>).MakeGenericType(type.GetGenericArguments()); + + // pass in obj to fill the list + return (IList?)Activator.CreateInstance(genericType, obj); } - return false; } - /// - /// Returns true if the type is a class and is not static - /// - /// - /// - public static bool IsNonStaticClass(Type t) + if (type.IsArray) { - return t.IsClass && IsStaticClass(t) == false; - } - - /// - /// Returns true if the type is a static class - /// - /// - /// - /// - /// In IL a static class is abstract and sealed - /// see: http://stackoverflow.com/questions/1175888/determine-if-a-type-is-static - /// - public static bool IsStaticClass(Type type) - { - return type.IsAbstract && type.IsSealed; - } - - /// - /// Finds a lowest base class amongst a collection of types - /// - /// - /// - /// - /// The term 'lowest' refers to the most base class of the type collection. - /// If a base type is not found amongst the type collection then an invalid attempt is returned. - /// - public static Attempt GetLowestBaseType(params Type[] types) - { - if (types.Length == 0) - return Attempt.Fail(); - - if (types.Length == 1) - return Attempt.Succeed(types[0]); - - foreach (var curr in types) + // if its an array, we'll use a List<> + Type? typeArguments = type.GetElementType(); + if (typeArguments is not null) { - var others = types.Except(new[] {curr}); + Type genericType = typeof(List<>).MakeGenericType(typeArguments); - //is the current type a common denominator for all others ? - var isBase = others.All(curr.IsAssignableFrom); - - //if this type is the base for all others - if (isBase) - { - return Attempt.Succeed(curr); - } + // pass in obj to fill the list + return (IList?)Activator.CreateInstance(genericType, obj); } + } + return null; + } + + /// + /// Checks if the method is actually overriding a base method + /// + /// + /// + public static bool IsOverride(MethodInfo m) => m.GetBaseDefinition().DeclaringType != m.DeclaringType; + + /// + /// Find all assembly references that are referencing the assignTypeFrom Type's assembly found in the assemblyList + /// + /// The referenced assembly. + /// A list of assemblies. + /// + /// + /// If the assembly of the assignTypeFrom Type is in the App_Code assembly, then we return nothing since things cannot + /// reference that assembly, same with the global.asax assembly. + /// + public static IReadOnlyList GetReferencingAssemblies(Assembly assembly, IEnumerable assemblies) + { + if (assembly.IsDynamic || assembly.IsAppCodeAssembly() || assembly.IsGlobalAsaxAssembly()) + { + return EmptyAssemblies; + } + + // find all assembly references that are referencing the current type's assembly since we + // should only be scanning those assemblies because any other assembly will definitely not + // contain sub type's of the one we're currently looking for + var name = assembly.GetName().Name; + return assemblies.Where(x => x == assembly || name is not null ? HasReference(x, name!) : false).ToList(); + } + + /// + /// Determines if an assembly references another assembly. + /// + /// + /// + /// + public static bool HasReference(Assembly assembly, string name) + { + // ReSharper disable once LoopCanBeConvertedToQuery - no! + foreach (AssemblyName a in assembly.GetReferencedAssemblies()) + { + if (string.Equals(a.Name, name, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + /// + /// Returns true if the type is a class and is not static + /// + /// + /// + public static bool IsNonStaticClass(Type t) => t.IsClass && IsStaticClass(t) == false; + + /// + /// Returns true if the type is a static class + /// + /// + /// + /// + /// In IL a static class is abstract and sealed + /// see: http://stackoverflow.com/questions/1175888/determine-if-a-type-is-static + /// + public static bool IsStaticClass(Type type) => type.IsAbstract && type.IsSealed; + + /// + /// Finds a lowest base class amongst a collection of types + /// + /// + /// + /// + /// The term 'lowest' refers to the most base class of the type collection. + /// If a base type is not found amongst the type collection then an invalid attempt is returned. + /// + public static Attempt GetLowestBaseType(params Type[] types) + { + if (types.Length == 0) + { return Attempt.Fail(); } - /// - /// Determines whether the type is assignable from the specified implementation, - /// and caches the result across the application using a . - /// - /// The type of the contract. - /// The implementation. - /// - /// true if [is type assignable from] [the specified contract]; otherwise, false. - /// - public static bool IsTypeAssignableFrom(Type contract, Type? implementation) + if (types.Length == 1) { - return contract.IsAssignableFrom(implementation); + return Attempt.Succeed(types[0]); } - /// - /// Determines whether the type is assignable from the specified implementation , - /// and caches the result across the application using a . - /// - /// The type of the contract. - /// The implementation. - public static bool IsTypeAssignableFrom(Type implementation) + foreach (Type curr in types) { - return IsTypeAssignableFrom(typeof(TContract), implementation); - } + IEnumerable others = types.Except(new[] { curr }); - /// - /// Determines whether the object instance is assignable from the specified implementation , - /// and caches the result across the application using a . - /// - /// The type of the contract. - /// The implementation. - public static bool IsTypeAssignableFrom(object implementation) - { - if (implementation == null) throw new ArgumentNullException(nameof(implementation)); - return IsTypeAssignableFrom(implementation.GetType()); - } + // is the current type a common denominator for all others ? + var isBase = others.All(curr.IsAssignableFrom); - /// - /// A method to determine whether represents a value type. - /// - /// The implementation. - public static bool IsValueType(Type implementation) - { - return implementation.IsValueType || implementation.IsPrimitive; - } - - /// - /// A method to determine whether is an implied value type (, or a string). - /// - /// The implementation. - public static bool IsImplicitValueType(Type implementation) - { - return IsValueType(implementation) || implementation.IsEnum || implementation == typeof (string); - } - - /// - /// Returns (and caches) a PropertyInfo from a type - /// - /// - /// - /// - /// - /// - /// - /// - public static PropertyInfo? GetProperty(Type type, string name, - bool mustRead = true, - bool mustWrite = true, - bool includeIndexed = false, - bool caseSensitive = true) - { - return CachedDiscoverableProperties(type, mustRead, mustWrite, includeIndexed) - .FirstOrDefault(x => caseSensitive ? (x.Name == name) : x.Name.InvariantEquals(name)); - } - - /// - /// Gets (and caches) discoverable in the current for a given . - /// - /// The source. - /// - public static FieldInfo[] CachedDiscoverableFields(Type type) - { - return GetFieldsCache.GetOrAdd( - type, - x => type - .GetFields(BindingFlags.Public | BindingFlags.Instance) - .Where(y => y.IsInitOnly == false) - .ToArray()); - } - - /// - /// Gets (and caches) discoverable in the current for a given . - /// - /// The source. - /// true if the properties discovered are readable - /// true if the properties discovered are writable - /// true if the properties discovered are indexable - /// - public static PropertyInfo[] CachedDiscoverableProperties(Type type, bool mustRead = true, bool mustWrite = true, bool includeIndexed = false) - { - return GetPropertiesCache.GetOrAdd( - new Tuple(type, mustRead, mustWrite, includeIndexed), - x => type - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(y => (mustRead == false || y.CanRead) - && (mustWrite == false || y.CanWrite) - && (includeIndexed || y.GetIndexParameters().Any() == false)) - .ToArray()); - } - - #region Match Type - - // TODO: Need to determine if these methods should replace/combine/merge etc with IsTypeAssignableFrom, IsAssignableFromGeneric - - // readings: - // http://stackoverflow.com/questions/2033912/c-sharp-variance-problem-assigning-listderived-as-listbase - // http://stackoverflow.com/questions/2208043/generic-variance-in-c-sharp-4-0 - // http://stackoverflow.com/questions/8401738/c-sharp-casting-generics-covariance-and-contravariance - // http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class - // http://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059 - - private static bool MatchGeneric(Type implementation, Type contract, IDictionary bindings) - { - // trying to match eg List with List - // or List>> with List>> - // classes are NOT invariant so List does not match List - - if (implementation.IsGenericType == false) return false; - - // must have the same generic type definition - var implDef = implementation.GetGenericTypeDefinition(); - var contDef = contract.GetGenericTypeDefinition(); - if (implDef != contDef) return false; - - // must have the same number of generic arguments - var implArgs = implementation.GetGenericArguments(); - var contArgs = contract.GetGenericArguments(); - if (implArgs.Length != contArgs.Length) return false; - - // generic arguments must match - // in insta we should have actual types (eg int, string...) - // in typea we can have generic parameters (eg ) - for (var i = 0; i < implArgs.Length; i++) + // if this type is the base for all others + if (isBase) { - const bool variance = false; // classes are NOT invariant - if (MatchType(implArgs[i], contArgs[i], bindings, variance) == false) - return false; + return Attempt.Succeed(curr); } - - return true; } - public static bool MatchType(Type implementation, Type contract) + return Attempt.Fail(); + } + + /// + /// Determines whether the type is assignable from the specified implementation, + /// and caches the result across the application using a . + /// + /// The type of the contract. + /// The implementation. + /// + /// true if [is type assignable from] [the specified contract]; otherwise, false. + /// + public static bool IsTypeAssignableFrom(Type contract, Type? implementation) => + contract.IsAssignableFrom(implementation); + + /// + /// Determines whether the type is assignable from the specified implementation + /// , + /// and caches the result across the application using a . + /// + /// The type of the contract. + /// The implementation. + public static bool IsTypeAssignableFrom(Type implementation) => + IsTypeAssignableFrom(typeof(TContract), implementation); + + /// + /// Determines whether the object instance is assignable from the specified + /// implementation , + /// and caches the result across the application using a . + /// + /// The type of the contract. + /// The implementation. + public static bool IsTypeAssignableFrom(object implementation) + { + if (implementation == null) { - return MatchType(implementation, contract, new Dictionary()); + throw new ArgumentNullException(nameof(implementation)); } - public static bool MatchType(Type implementation, Type contract, IDictionary bindings, bool variance = true) + return IsTypeAssignableFrom(implementation.GetType()); + } + + /// + /// A method to determine whether represents a value type. + /// + /// The implementation. + public static bool IsValueType(Type implementation) => implementation.IsValueType || implementation.IsPrimitive; + + /// + /// A method to determine whether is an implied value type ( + /// , or a string). + /// + /// The implementation. + public static bool IsImplicitValueType(Type implementation) => + IsValueType(implementation) || implementation.IsEnum || implementation == typeof(string); + + /// + /// Returns (and caches) a PropertyInfo from a type + /// + /// + /// + /// + /// + /// + /// + /// + public static PropertyInfo? GetProperty( + Type type, + string name, + bool mustRead = true, + bool mustWrite = true, + bool includeIndexed = false, + bool caseSensitive = true) => + CachedDiscoverableProperties(type, mustRead, mustWrite, includeIndexed) + .FirstOrDefault(x => caseSensitive ? x.Name == name : x.Name.InvariantEquals(name)); + + /// + /// Gets (and caches) discoverable in the current for a given + /// . + /// + /// The source. + /// + public static FieldInfo[] CachedDiscoverableFields(Type type) => + GetFieldsCache.GetOrAdd( + type, + x => type + .GetFields(BindingFlags.Public | BindingFlags.Instance) + .Where(y => y.IsInitOnly == false) + .ToArray()); + + /// + /// Gets (and caches) discoverable in the current for a given + /// . + /// + /// The source. + /// true if the properties discovered are readable + /// true if the properties discovered are writable + /// true if the properties discovered are indexable + /// + public static PropertyInfo[] CachedDiscoverableProperties(Type type, bool mustRead = true, bool mustWrite = true, bool includeIndexed = false) => + GetPropertiesCache.GetOrAdd( + new Tuple(type, mustRead, mustWrite, includeIndexed), + x => type + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(y => (mustRead == false || y.CanRead) + && (mustWrite == false || y.CanWrite) + && (includeIndexed || y.GetIndexParameters().Any() == false)) + .ToArray()); + + public static bool MatchType(Type implementation, Type contract) => + MatchType(implementation, contract, new Dictionary()); + + #region Match Type + + // TODO: Need to determine if these methods should replace/combine/merge etc with IsTypeAssignableFrom, IsAssignableFromGeneric + + // readings: + // http://stackoverflow.com/questions/2033912/c-sharp-variance-problem-assigning-listderived-as-listbase + // http://stackoverflow.com/questions/2208043/generic-variance-in-c-sharp-4-0 + // http://stackoverflow.com/questions/8401738/c-sharp-casting-generics-covariance-and-contravariance + // http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class + // http://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059 + private static bool MatchGeneric(Type implementation, Type contract, IDictionary bindings) + { + // trying to match eg List with List + // or List>> with List>> + // classes are NOT invariant so List does not match List + if (implementation.IsGenericType == false) { - if (contract.IsGenericType) - { - // eg type is List or List - // if we have variance then List can match IList - // if we don't have variance it can't - must have exact type - - // try to match implementation against contract - if (MatchGeneric(implementation, contract, bindings)) return true; - - // if no variance, fail - if (variance == false) return false; - - // try to match an ancestor of implementation against contract - var t = implementation.BaseType; - while (t != null) - { - if (MatchGeneric(t, contract, bindings)) return true; - t = t.BaseType; - } - - // try to match an interface of implementation against contract - return implementation.GetInterfaces().Any(i => MatchGeneric(i, contract, bindings)); - } - - if (contract.IsGenericParameter) - { - // eg - - if (bindings.ContainsKey(contract.Name)) - { - // already bound: ensure it's compatible - return bindings[contract.Name] == implementation; - } - - // not already bound: bind - bindings[contract.Name] = implementation; - return true; - } - - // not a generic type, not a generic parameter - // so normal class or interface - // about primitive types, value types, etc: - // http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class - // if it's a primitive type... it needs to be == - - if (implementation == contract) return true; - if (contract.IsClass && implementation.IsClass && implementation.IsSubclassOf(contract)) return true; - if (contract.IsInterface && implementation.GetInterfaces().Contains(contract)) return true; - return false; } - #endregion + // must have the same generic type definition + Type implDef = implementation.GetGenericTypeDefinition(); + Type contDef = contract.GetGenericTypeDefinition(); + if (implDef != contDef) + { + return false; + } + + // must have the same number of generic arguments + Type[] implArgs = implementation.GetGenericArguments(); + Type[] contArgs = contract.GetGenericArguments(); + if (implArgs.Length != contArgs.Length) + { + return false; + } + + // generic arguments must match + // in insta we should have actual types (eg int, string...) + // in typea we can have generic parameters (eg ) + for (var i = 0; i < implArgs.Length; i++) + { + const bool variance = false; // classes are NOT invariant + if (MatchType(implArgs[i], contArgs[i], bindings, variance) == false) + { + return false; + } + } + + return true; } + + public static bool MatchType(Type implementation, Type contract, IDictionary bindings, bool variance = true) + { + if (contract.IsGenericType) + { + // eg type is List or List + // if we have variance then List can match IList + // if we don't have variance it can't - must have exact type + + // try to match implementation against contract + if (MatchGeneric(implementation, contract, bindings)) + { + return true; + } + + // if no variance, fail + if (variance == false) + { + return false; + } + + // try to match an ancestor of implementation against contract + Type? t = implementation.BaseType; + while (t != null) + { + if (MatchGeneric(t, contract, bindings)) + { + return true; + } + + t = t.BaseType; + } + + // try to match an interface of implementation against contract + return implementation.GetInterfaces().Any(i => MatchGeneric(i, contract, bindings)); + } + + if (contract.IsGenericParameter) + { + // eg + if (bindings.ContainsKey(contract.Name)) + { + // already bound: ensure it's compatible + return bindings[contract.Name] == implementation; + } + + // not already bound: bind + bindings[contract.Name] = implementation; + return true; + } + + // not a generic type, not a generic parameter + // so normal class or interface + // about primitive types, value types, etc: + // http://stackoverflow.com/questions/1827425/how-to-check-programatically-if-a-type-is-a-struct-or-a-class + // if it's a primitive type... it needs to be == + if (implementation == contract) + { + return true; + } + + if (contract.IsClass && implementation.IsClass && implementation.IsSubclassOf(contract)) + { + return true; + } + + if (contract.IsInterface && implementation.GetInterfaces().Contains(contract)) + { + return true; + } + + return false; + } + + #endregion } diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 6f4d81fc34..7fadd102da 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Microsoft.Extensions.Logging; @@ -10,458 +6,506 @@ using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Composing +namespace Umbraco.Cms.Core.Composing; + +/// +/// Provides methods to find and instantiate types. +/// +/// +/// +/// This class should be used to get all types, the class should never be used +/// directly. +/// +/// In most cases this class is not used directly but through extension methods that retrieve specific types. +/// +public sealed class TypeLoader { + private readonly object _locko = new(); + private readonly ILogger _logger; + + private readonly Dictionary _types = new(); + + private IEnumerable? _assemblies; + /// - /// Provides methods to find and instantiate types. + /// Initializes a new instance of the class. + /// + [Obsolete("Please use an alternative constructor.")] + public TypeLoader( + ITypeFinder typeFinder, + IRuntimeHash runtimeHash, + IAppPolicyCache runtimeCache, + DirectoryInfo localTempPath, + ILogger logger, + IProfiler profiler, + IEnumerable? assembliesToScan = null) + : this(typeFinder, logger, assembliesToScan) + { + } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Please use an alternative constructor.")] + public TypeLoader( + ITypeFinder typeFinder, + IRuntimeHash runtimeHash, + IAppPolicyCache runtimeCache, + DirectoryInfo localTempPath, + ILogger logger, + IProfiler profiler, + bool detectChanges, + IEnumerable? assembliesToScan = null) + : this(typeFinder, logger, assembliesToScan) + { + } + + public TypeLoader( + ITypeFinder typeFinder, + ILogger logger, + IEnumerable? assembliesToScan = null) + { + TypeFinder = typeFinder ?? throw new ArgumentNullException(nameof(typeFinder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _assemblies = assembliesToScan; + } + + /// + /// Returns the underlying + /// + // ReSharper disable once MemberCanBePrivate.Global + public ITypeFinder TypeFinder { get; } + + /// + /// Gets or sets the set of assemblies to scan. /// /// - /// This class should be used to get all types, the class should never be used directly. - /// In most cases this class is not used directly but through extension methods that retrieve specific types. + /// + /// If not explicitly set, defaults to all assemblies except those that are know to not have any of the + /// types we might scan. Because we only scan for application types, this means we can safely exclude GAC + /// assemblies + /// for example. + /// + /// This is for unit tests. /// - public sealed class TypeLoader + // internal for tests + [Obsolete("This will be removed in a future version.")] + public IEnumerable AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan; + + /// + /// Gets the type lists. + /// + /// For unit tests. + // internal for tests + [Obsolete("This will be removed in a future version.")] + public IEnumerable TypeLists => _types.Values; + + /// + /// Sets a type list. + /// + /// For unit tests. + // internal for tests + [Obsolete("This will be removed in a future version.")] + public void AddTypeList(TypeList typeList) { - private readonly ILogger _logger; + Type tobject = typeof(object); // CompositeTypeTypeKey does not support null values + _types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList; + } - private readonly Dictionary _types = new (); - private readonly object _locko = new (); + #region Get Assembly Attributes - private IEnumerable? _assemblies; - - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Please use an alternative constructor.")] - public TypeLoader( - ITypeFinder typeFinder, - IRuntimeHash runtimeHash, - IAppPolicyCache runtimeCache, - DirectoryInfo localTempPath, - ILogger logger, - IProfiler profiler, - IEnumerable? assembliesToScan = null) - : this(typeFinder, logger, assembliesToScan) + /// + /// Gets the assembly attributes of the specified . + /// + /// The attribute types. + /// + /// The assembly attributes of the specified types. + /// + /// attributeTypes + public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes) + { + if (attributeTypes == null) { + throw new ArgumentNullException(nameof(attributeTypes)); } - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Please use an alternative constructor.")] - public TypeLoader( - ITypeFinder typeFinder, - IRuntimeHash runtimeHash, - IAppPolicyCache runtimeCache, - DirectoryInfo localTempPath, - ILogger logger, - IProfiler profiler, - bool detectChanges, - IEnumerable? assembliesToScan = null) - : this(typeFinder, logger, assembliesToScan) + return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); + } + + #endregion + + #region Cache + + // internal for tests + [Obsolete("This will be removed in a future version.")] + public Attempt> TryGetCached(Type baseType, Type attributeType) => + Attempt>.Fail(); + + // internal for tests + [Obsolete("This will be removed in a future version.")] + public Dictionary<(string, string), IEnumerable>? ReadCache() => null; + + // internal for tests + [Obsolete("This will be removed in a future version.")] + public string? GetTypesListFilePath() => null; + + // internal for tests + [Obsolete("This will be removed in a future version.")] + public void WriteCache() + { + } + + /// + /// Clears cache. + /// + /// Generally only used for resetting cache, for example during the install process. + [Obsolete("This will be removed in a future version.")] + public void ClearTypesCache() + { + } + + #endregion + + #region Get Types + + /// + /// Gets class types inheriting from or implementing the specified type + /// + /// The type to inherit from or implement. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetTypes(bool cache = true, IEnumerable? specificAssemblies = null) + { + if (_logger == null) { + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); } - public TypeLoader( - ITypeFinder typeFinder, - ILogger logger, - IEnumerable? assembliesToScan = null) + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { - TypeFinder = typeFinder ?? throw new ArgumentNullException(nameof(typeFinder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _assemblies = assembliesToScan; - } - - /// - /// Returns the underlying - /// - // ReSharper disable once MemberCanBePrivate.Global - public ITypeFinder TypeFinder { get; } - - /// - /// Gets or sets the set of assemblies to scan. - /// - /// - /// If not explicitly set, defaults to all assemblies except those that are know to not have any of the - /// types we might scan. Because we only scan for application types, this means we can safely exclude GAC assemblies - /// for example. - /// This is for unit tests. - /// - // internal for tests - [Obsolete("This will be removed in a future version.")] - public IEnumerable AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan; - - /// - /// Gets the type lists. - /// - /// For unit tests. - // internal for tests - [Obsolete("This will be removed in a future version.")] - public IEnumerable TypeLists => _types.Values; - - /// - /// Sets a type list. - /// - /// For unit tests. - // internal for tests - [Obsolete("This will be removed in a future version.")] - public void AddTypeList(TypeList typeList) - { - var tobject = typeof(object); // CompositeTypeTypeKey does not support null values - _types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList; - } - - #region Cache - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public Attempt> TryGetCached(Type baseType, Type attributeType) - { - return Attempt>.Fail(); - } - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public Dictionary<(string, string), IEnumerable>? ReadCache() => null; - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public string? GetTypesListFilePath() => null; - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public void WriteCache() - { - } - - /// - /// Clears cache. - /// - /// Generally only used for resetting cache, for example during the install process. - [Obsolete("This will be removed in a future version.")] - public void ClearTypesCache() - { - } - - #endregion - - #region Get Assembly Attributes - - /// - /// Gets the assembly attributes of the specified . - /// - /// The attribute types. - /// - /// The assembly attributes of the specified types. - /// - /// attributeTypes - public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes) - { - if (attributeTypes == null) - throw new ArgumentNullException(nameof(attributeTypes)); - - return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); - } - - #endregion - - #region Get Types - - /// - /// Gets class types inheriting from or implementing the specified type - /// - /// The type to inherit from or implement. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types inheriting from or implementing the specified type. - /// Caching is disabled when using specific assemblies. - public IEnumerable GetTypes(bool cache = true, IEnumerable? specificAssemblies = null) - { - if (_logger == null) - { - throw new InvalidOperationException("Cannot get types from a test/blank type loader."); - } - - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - // if not IDiscoverable, directly get types - if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) - { - // warn - _logger.LogDebug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} (slow).", typeof(T).FullName); - - return GetTypesInternal( - typeof(T), null, - () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), - "scanning assemblies", - cache); - } - - // get IDiscoverable and always cache - var discovered = GetTypesInternal( - typeof(IDiscoverable), null, - () => TypeFinder.FindClassesOfType(AssembliesToScan), - "scanning assemblies", - true); - // warn - if (!cache) - { - _logger.LogDebug("Running a non-cached, filter for discoverable type {TypeName} (slowish).", typeof(T).FullName); - } - - // filter the cached discovered types (and maybe cache the result) - return GetTypesInternal( - typeof(T), null, - () => discovered - .Where(x => typeof(T).IsAssignableFrom(x)), - "filtering IDiscoverable", - cache); - } - - /// - /// Gets class types inheriting from or implementing the specified type and marked with the specified attribute. - /// - /// The type to inherit from or implement. - /// The type of the attribute. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types inheriting from or implementing the specified type and marked with the specified attribute. - /// Caching is disabled when using specific assemblies. - public IEnumerable GetTypesWithAttribute(bool cache = true, IEnumerable? specificAssemblies = null) - where TAttribute : Attribute - { - if (_logger == null) - { - throw new InvalidOperationException("Cannot get types from a test/blank type loader."); - } - - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - // if not IDiscoverable, directly get types - if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) - { - _logger.LogDebug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); - - return GetTypesInternal( - typeof(T), typeof(TAttribute), - () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), - "scanning assemblies", - cache); - } - - // get IDiscoverable and always cache - var discovered = GetTypesInternal( - typeof(IDiscoverable), null, - () => TypeFinder.FindClassesOfType(AssembliesToScan), - "scanning assemblies", - true); - - // warn - if (!cache) - { - _logger.LogDebug("Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", typeof(T).FullName, typeof(TAttribute).FullName); - } - - // filter the cached discovered types (and maybe cache the result) - return GetTypesInternal( - typeof(T), typeof(TAttribute), - () => discovered - .Where(x => typeof(T).IsAssignableFrom(x)) - .Where(x => x.GetCustomAttributes(false).Any()), - "filtering IDiscoverable", - cache); - } - - /// - /// Gets class types marked with the specified attribute. - /// - /// The type of the attribute. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types marked with the specified attribute. - /// Caching is disabled when using specific assemblies. - public IEnumerable GetAttributedTypes(bool cache = true, IEnumerable? specificAssemblies = null) - where TAttribute : Attribute - { - if (_logger == null) - { - throw new InvalidOperationException("Cannot get types from a test/blank type loader."); - } - - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - if (!cache) - { - _logger.LogDebug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); - } + _logger.LogDebug( + "Running a full, " + (cache ? string.Empty : "non-") + + "cached, scan for non-discoverable type {TypeName} (slow).", + typeof(T).FullName); return GetTypesInternal( - typeof(object), typeof(TAttribute), - () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), + typeof(T), + null, + () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), "scanning assemblies", cache); } - private IEnumerable GetTypesInternal( - Type baseType, - Type? attributeType, - Func> finder, - string action, - bool cache) - { - // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable - // lock at a time, and we don't have non-upgradeable readers, and quite probably the type - // loader is mostly not going to be used in any kind of massively multi-threaded scenario - so, - // a plain lock is enough + // get IDiscoverable and always cache + IEnumerable discovered = GetTypesInternal( + typeof(IDiscoverable), + null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", + true); - lock (_locko) - { - return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); - } + // warn + if (!cache) + { + _logger.LogDebug( + "Running a non-cached, filter for discoverable type {TypeName} (slowish).", + typeof(T).FullName); } - private static string GetName(Type? baseType, Type? attributeType) + // filter the cached discovered types (and maybe cache the result) + return GetTypesInternal( + typeof(T), + null, + () => discovered.Where(x => typeof(T).IsAssignableFrom(x)), + "filtering IDiscoverable", + cache); + } + + /// + /// Gets class types inheriting from or implementing the specified type and marked with the specified attribute. + /// + /// The type to inherit from or implement. + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type and marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetTypesWithAttribute( + bool cache = true, + IEnumerable? specificAssemblies = null) + where TAttribute : Attribute + { + if (_logger == null) { - var s = attributeType == null ? string.Empty : ("[" + attributeType + "]"); - s += baseType; - return s; + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); } - private IEnumerable GetTypesInternalLocked( - Type? baseType, - Type? attributeType, - Func> finder, - string action, - bool cache) + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { - // check if the TypeList already exists, if so return it, if not we'll create it - var tobject = typeof(object); // CompositeTypeTypeKey does not support null values - var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); - TypeList? typeList = null; + _logger.LogDebug( + "Running a full, " + (cache ? string.Empty : "non-") + + "cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).", + typeof(T).FullName, + typeof(TAttribute).FullName); - if (cache) - { - _types.TryGetValue(listKey, out typeList); // else null - } + return GetTypesInternal( + typeof(T), + typeof(TAttribute), + () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", + cache); + } - // if caching and found, return - if (typeList != null) - { - // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 - _logger.LogDebug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType)); - return typeList.Types; - } + // get IDiscoverable and always cache + IEnumerable discovered = GetTypesInternal( + typeof(IDiscoverable), + null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", + true); - // else proceed, - typeList = new TypeList(baseType, attributeType); + // warn + if (!cache) + { + _logger.LogDebug( + "Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", + typeof(T).FullName, + typeof(TAttribute).FullName); + } - // either we had to scan, or we could not get the types from the cache file - scan now - _logger.LogDebug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); + // filter the cached discovered types (and maybe cache the result) + return GetTypesInternal( + typeof(T), + typeof(TAttribute), + () => discovered + .Where(x => typeof(T).IsAssignableFrom(x)) + .Where(x => x.GetCustomAttributes(false).Any()), + "filtering IDiscoverable", + cache); + } - foreach (var t in finder()) - { - typeList.Add(t); - } + /// + /// Gets class types marked with the specified attribute. + /// + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetAttributedTypes( + bool cache = true, + IEnumerable? specificAssemblies = null) + where TAttribute : Attribute + { + if (_logger == null) + { + throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } - // if we are to cache the results, do so - if (cache) - { - var added = _types.ContainsKey(listKey) == false; - if (added) - { - _types[listKey] = typeList; - } + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; - _logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); - } - else - { - _logger.LogDebug("Got {TypeName}.", GetName(baseType, attributeType)); - } + if (!cache) + { + _logger.LogDebug( + "Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", + typeof(TAttribute).FullName); + } + return GetTypesInternal( + typeof(object), + typeof(TAttribute), + () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", + cache); + } + + private static string GetName(Type? baseType, Type? attributeType) + { + var s = attributeType == null ? string.Empty : "[" + attributeType + "]"; + s += baseType; + return s; + } + + private IEnumerable GetTypesInternal( + Type baseType, + Type? attributeType, + Func> finder, + string action, + bool cache) + { + // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable + // lock at a time, and we don't have non-upgradeable readers, and quite probably the type + // loader is mostly not going to be used in any kind of massively multi-threaded scenario - so, + // a plain lock is enough + lock (_locko) + { + return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + } + } + + private IEnumerable GetTypesInternalLocked( + Type? baseType, + Type? attributeType, + Func> finder, + string action, + bool cache) + { + // check if the TypeList already exists, if so return it, if not we'll create it + Type tobject = typeof(object); // CompositeTypeTypeKey does not support null values + var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); + TypeList? typeList = null; + + if (cache) + { + _types.TryGetValue(listKey, out typeList); // else null + } + + // if caching and found, return + if (typeList != null) + { + // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 + _logger.LogDebug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType)); return typeList.Types; } - #endregion + // else proceed, + typeList = new TypeList(baseType, attributeType); - #region Nested classes and stuff + // either we had to scan, or we could not get the types from the cache file - scan now + _logger.LogDebug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); - /// - /// Represents a list of types obtained by looking for types inheriting/implementing a - /// specified type, and/or marked with a specified attribute type. - /// - public sealed class TypeList + foreach (Type t in finder()) { - private readonly HashSet _types = new HashSet(); - - public TypeList(Type? baseType, Type? attributeType) - { - BaseType = baseType; - AttributeType = attributeType; - } - - public Type? BaseType { get; } - public Type? AttributeType { get; } - - /// - /// Adds a type. - /// - public void Add(Type type) - { - if (BaseType?.IsAssignableFrom(type) == false) - throw new ArgumentException("Base type " + BaseType + " is not assignable from type " + type + ".", nameof(type)); - _types.Add(type); - } - - /// - /// Gets the types. - /// - public IEnumerable Types => _types; + typeList.Add(t); } - /// - /// Represents the error that occurs when a type was not found in the cache type list with the specified TypeResolutionKind. - /// - /// - [Serializable] - internal class CachedTypeNotFoundInFileException : Exception + // if we are to cache the results, do so + if (cache) { - /// - /// Initializes a new instance of the class. - /// - public CachedTypeNotFoundInFileException() - { } + var added = _types.ContainsKey(listKey) == false; + if (added) + { + _types[listKey] = typeList; + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public CachedTypeNotFoundInFileException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public CachedTypeNotFoundInFileException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected CachedTypeNotFoundInFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + _logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); + } + else + { + _logger.LogDebug("Got {TypeName}.", GetName(baseType, attributeType)); } - #endregion + return typeList.Types; } + + #endregion + + #region Nested classes and stuff + + /// + /// Represents a list of types obtained by looking for types inheriting/implementing a + /// specified type, and/or marked with a specified attribute type. + /// + public sealed class TypeList + { + private readonly HashSet _types = new(); + + public TypeList(Type? baseType, Type? attributeType) + { + BaseType = baseType; + AttributeType = attributeType; + } + + public Type? BaseType { get; } + + public Type? AttributeType { get; } + + /// + /// Gets the types. + /// + public IEnumerable Types => _types; + + /// + /// Adds a type. + /// + public void Add(Type type) + { + if (BaseType?.IsAssignableFrom(type) == false) + { + throw new ArgumentException( + "Base type " + BaseType + " is not assignable from type " + type + ".", + nameof(type)); + } + + _types.Add(type); + } + } + + /// + /// Represents the error that occurs when a type was not found in the cache type list with the specified + /// TypeResolutionKind. + /// + /// + [Serializable] + internal class CachedTypeNotFoundInFileException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public CachedTypeNotFoundInFileException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public CachedTypeNotFoundInFileException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public CachedTypeNotFoundInFileException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected CachedTypeNotFoundInFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + #endregion } diff --git a/src/Umbraco.Core/Composing/VaryingRuntimeHash.cs b/src/Umbraco.Core/Composing/VaryingRuntimeHash.cs index eec2adc637..740921974d 100644 --- a/src/Umbraco.Core/Composing/VaryingRuntimeHash.cs +++ b/src/Umbraco.Core/Composing/VaryingRuntimeHash.cs @@ -1,19 +1,13 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// A runtime hash this is always different on each app startup +/// +public sealed class VaryingRuntimeHash : IRuntimeHash { - /// - /// A runtime hash this is always different on each app startup - /// - public sealed class VaryingRuntimeHash : IRuntimeHash - { - private readonly string _hash; + private readonly string _hash; - public VaryingRuntimeHash() - { - _hash = DateTime.Now.Ticks.ToString(); - } + public VaryingRuntimeHash() => _hash = DateTime.Now.Ticks.ToString(); - public string GetHashValue() => _hash; - } + public string GetHashValue() => _hash; } diff --git a/src/Umbraco.Core/Composing/WeightAttribute.cs b/src/Umbraco.Core/Composing/WeightAttribute.cs index 1225abca0c..a69ca4636e 100644 --- a/src/Umbraco.Core/Composing/WeightAttribute.cs +++ b/src/Umbraco.Core/Composing/WeightAttribute.cs @@ -1,25 +1,19 @@ -using System; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Specifies the weight of pretty much anything. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class WeightAttribute : Attribute { /// - /// Specifies the weight of pretty much anything. + /// Initializes a new instance of the class with a weight. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class WeightAttribute : Attribute - { - /// - /// Initializes a new instance of the class with a weight. - /// - /// - public WeightAttribute(int weight) - { - Weight = weight; - } + /// + public WeightAttribute(int weight) => Weight = weight; - /// - /// Gets the weight value. - /// - public int Weight { get; } - } + /// + /// Gets the weight value. + /// + public int Weight { get; } } diff --git a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs index 1eafcce9e0..56b714d35a 100644 --- a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs @@ -1,141 +1,156 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Composing +/// +/// Implements a weighted collection builder. +/// +/// The type of the builder. +/// The type of the collection. +/// The type of the items. +public abstract class WeightedCollectionBuilderBase : CollectionBuilderBase + where TBuilder : WeightedCollectionBuilderBase + where TCollection : class, IBuilderCollection { + private readonly Dictionary _customWeights = new(); + + public virtual int DefaultWeight { get; set; } = 100; + + protected abstract TBuilder This { get; } + /// - /// Implements a weighted collection builder. + /// Clears all types in the collection. /// - /// The type of the builder. - /// The type of the collection. - /// The type of the items. - public abstract class WeightedCollectionBuilderBase : CollectionBuilderBase - where TBuilder : WeightedCollectionBuilderBase - where TCollection : class, IBuilderCollection + /// The builder. + public TBuilder Clear() { - protected abstract TBuilder This { get; } + Configure(types => types.Clear()); + return This; + } - private readonly Dictionary _customWeights = new Dictionary(); - - /// - /// Clears all types in the collection. - /// - /// The builder. - public TBuilder Clear() + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add() + where T : TItem + { + Configure(types => { - Configure(types => types.Clear()); - return This; - } - - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add() - where T : TItem - { - Configure(types => + Type type = typeof(T); + if (types.Contains(type) == false) { - var type = typeof(T); - if (types.Contains(type) == false) types.Add(type); - }); - return This; - } + types.Add(type); + } + }); + return This; + } - /// - /// Adds a type to the collection. - /// - /// The type to add. - /// The builder. - public TBuilder Add(Type type) + /// + /// Adds a type to the collection. + /// + /// The type to add. + /// The builder. + public TBuilder Add(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "register"); + if (types.Contains(type) == false) { + types.Add(type); + } + }); + return This; + } + + /// + /// Adds types to the collection. + /// + /// The types to add. + /// The builder. + public TBuilder Add(IEnumerable types) + { + Configure(list => + { + foreach (Type type in types) + { + // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast EnsureType(type, "register"); - if (types.Contains(type) == false) types.Add(type); - }); - return This; - } - - /// - /// Adds types to the collection. - /// - /// The types to add. - /// The builder. - public TBuilder Add(IEnumerable types) - { - Configure(list => - { - foreach (var type in types) + if (list.Contains(type) == false) { - // would be detected by CollectionBuilderBase when registering, anyways, but let's fail fast - EnsureType(type, "register"); - if (list.Contains(type) == false) list.Add(type); + list.Add(type); } - }); - return This; - } + } + }); + return This; + } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove() - where T : TItem + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove() + where T : TItem + { + Configure(types => { - Configure(types => + Type type = typeof(T); + if (types.Contains(type)) { - var type = typeof(T); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } + }); + return This; + } - /// - /// Removes a type from the collection. - /// - /// The type to remove. - /// The builder. - public TBuilder Remove(Type type) + /// + /// Removes a type from the collection. + /// + /// The type to remove. + /// The builder. + public TBuilder Remove(Type type) + { + Configure(types => { - Configure(types => + EnsureType(type, "remove"); + if (types.Contains(type)) { - EnsureType(type, "remove"); - if (types.Contains(type)) types.Remove(type); - }); - return This; - } + types.Remove(type); + } + }); + return This; + } - /// - /// Changes the default weight of an item - /// - /// The type of item - /// The new weight - /// - public TBuilder SetWeight(int weight) where T : TItem + /// + /// Changes the default weight of an item + /// + /// The type of item + /// The new weight + /// + public TBuilder SetWeight(int weight) + where T : TItem + { + _customWeights[typeof(T)] = weight; + return This; + } + + protected override IEnumerable GetRegisteringTypes(IEnumerable types) + { + var list = types.ToList(); + list.Sort((t1, t2) => GetWeight(t1).CompareTo(GetWeight(t2))); + return list; + } + + protected virtual int GetWeight(Type type) + { + if (_customWeights.ContainsKey(type)) { - _customWeights[typeof(T)] = weight; - return This; + return _customWeights[type]; } - protected override IEnumerable GetRegisteringTypes(IEnumerable types) - { - var list = types.ToList(); - list.Sort((t1, t2) => GetWeight(t1).CompareTo(GetWeight(t2))); - return list; - } - - public virtual int DefaultWeight { get; set; } = 100; - - protected virtual int GetWeight(Type type) - { - if (_customWeights.ContainsKey(type)) - return _customWeights[type]; - var attr = type.GetCustomAttributes(typeof(WeightAttribute), false).OfType().SingleOrDefault(); - return attr?.Weight ?? DefaultWeight; - } + WeightAttribute? attr = type.GetCustomAttributes(typeof(WeightAttribute), false).OfType() + .SingleOrDefault(); + return attr?.Weight ?? DefaultWeight; } } diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs index e69de29bb2..8b13789179 100644 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs @@ -0,0 +1 @@ + diff --git a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs index 829d19bb53..cd256e1b45 100644 --- a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs @@ -6,14 +6,14 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Configuration; /// -/// Configures the named option. +/// Configures the named option. /// public class ConfigureConnectionStrings : IConfigureNamedOptions { private readonly IConfiguration _configuration; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. public ConfigureConnectionStrings(IConfiguration configuration) @@ -38,6 +38,7 @@ public class ConfigureConnectionStrings : IConfigureNamedOptions - /// Determines if file extension is allowed for upload based on (optional) white list and black list - /// held in settings. - /// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted. - /// - public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension) - { - return contentSettings.AllowedUploadFiles.Any(x => x.InvariantEquals(extension)) || - (contentSettings.AllowedUploadFiles.Any() == false && - contentSettings.DisallowedUploadFiles.Any(x => x.InvariantEquals(extension)) == false); - } +namespace Umbraco.Extensions; - /// - /// Gets the auto-fill configuration for a specified property alias. - /// - /// - /// The property type alias. - /// The auto-fill configuration for the specified property alias, or null. - public static ImagingAutoFillUploadField? GetConfig(this ContentSettings contentSettings, string propertyTypeAlias) - { - var autoFillConfigs = contentSettings.Imaging.AutoFillImageProperties; - return autoFillConfigs?.FirstOrDefault(x => x.Alias == propertyTypeAlias); - } +public static class ContentSettingsExtensions +{ + /// + /// Determines if file extension is allowed for upload based on (optional) white list and black list + /// held in settings. + /// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted. + /// + public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension) => + contentSettings.AllowedUploadFiles.Any(x => x.InvariantEquals(extension)) || + (contentSettings.AllowedUploadFiles.Any() == false && + contentSettings.DisallowedUploadFiles.Any(x => x.InvariantEquals(extension)) == false); + + /// + /// Gets the auto-fill configuration for a specified property alias. + /// + /// + /// The property type alias. + /// The auto-fill configuration for the specified property alias, or null. + public static ImagingAutoFillUploadField? GetConfig(this ContentSettings contentSettings, string propertyTypeAlias) + { + ImagingAutoFillUploadField[] autoFillConfigs = contentSettings.Imaging.AutoFillImageProperties; + return autoFillConfigs?.FirstOrDefault(x => x.Alias == propertyTypeAlias); } } diff --git a/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs b/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs index b6b9f067b9..096eac6fe0 100644 --- a/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs +++ b/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs @@ -1,24 +1,23 @@ using System.Reflection; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +internal class EntryAssemblyMetadata : IEntryAssemblyMetadata { - internal class EntryAssemblyMetadata : IEntryAssemblyMetadata + public EntryAssemblyMetadata() { - public EntryAssemblyMetadata() - { - var entryAssembly = Assembly.GetEntryAssembly(); + var entryAssembly = Assembly.GetEntryAssembly(); - Name = entryAssembly - ?.GetName() - ?.Name ?? string.Empty; + Name = entryAssembly + ?.GetName() + ?.Name ?? string.Empty; - InformationalVersion = entryAssembly - ?.GetCustomAttribute() - ?.InformationalVersion ?? string.Empty; - } - - public string Name { get; } - - public string InformationalVersion { get; } + InformationalVersion = entryAssembly + ?.GetCustomAttribute() + ?.InformationalVersion ?? string.Empty; } + + public string Name { get; } + + public string InformationalVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs b/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs index 7655252981..bbf8c67db5 100644 --- a/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs +++ b/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs @@ -1,28 +1,24 @@ -using System; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class HealthCheckSettingsExtensions { - public static class HealthCheckSettingsExtensions + public static TimeSpan GetNotificationDelay(this HealthChecksSettings settings, ICronTabParser cronTabParser, DateTime now, TimeSpan defaultDelay) { - public static TimeSpan GetNotificationDelay(this HealthChecksSettings settings, ICronTabParser cronTabParser, DateTime now, TimeSpan defaultDelay) + // If first run time not set, start with just small delay after application start. + var firstRunTime = settings.Notification.FirstRunTime; + if (string.IsNullOrEmpty(firstRunTime)) { - // If first run time not set, start with just small delay after application start. - var firstRunTime = settings.Notification.FirstRunTime; - if (string.IsNullOrEmpty(firstRunTime)) - { - return defaultDelay; - } - else - { - // Otherwise start at scheduled time according to cron expression, unless within the default delay period. - var firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now); - var delay = firstRunOccurance - now; - return delay < defaultDelay - ? defaultDelay - : delay; - } + return defaultDelay; } + + // Otherwise start at scheduled time according to cron expression, unless within the default delay period. + DateTime firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now); + TimeSpan delay = firstRunOccurance - now; + return delay < defaultDelay + ? defaultDelay + : delay; } } diff --git a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs index 2b22b0f28b..2f49bfd146 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs @@ -1,60 +1,75 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Extensions -{ - public static class GlobalSettingsExtensions - { - private static string? _mvcArea; - private static string? _backOfficePath; +namespace Umbraco.Extensions; - /// - /// Returns the absolute path for the Umbraco back office - /// - /// - /// - /// - public static string GetBackOfficePath(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) +public static class GlobalSettingsExtensions +{ + private static string? _mvcArea; + private static string? _backOfficePath; + + /// + /// Returns the absolute path for the Umbraco back office + /// + /// + /// + /// + public static string GetBackOfficePath(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + { + if (_backOfficePath != null) { - if (_backOfficePath != null) return _backOfficePath; - _backOfficePath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); return _backOfficePath; } - /// - /// This returns the string of the MVC Area route. - /// - /// - /// This will return the MVC area that we will route all custom routes through like surface controllers, etc... - /// We will use the 'Path' (default ~/umbraco) to create it but since it cannot contain '/' and people may specify a path of ~/asdf/asdf/admin - /// we will convert the '/' to '-' and use that as the path. its a bit lame but will work. - /// - /// We also make sure that the virtual directory (SystemDirectories.Root) is stripped off first, otherwise we'd end up with something - /// like "MyVirtualDirectory-Umbraco" instead of just "Umbraco". - /// - public static string GetUmbracoMvcArea(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + _backOfficePath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + return _backOfficePath; + } + + /// + /// This returns the string of the MVC Area route. + /// + /// + /// This will return the MVC area that we will route all custom routes through like surface controllers, etc... + /// We will use the 'Path' (default ~/umbraco) to create it but since it cannot contain '/' and people may specify a + /// path of ~/asdf/asdf/admin + /// we will convert the '/' to '-' and use that as the path. its a bit lame but will work. + /// We also make sure that the virtual directory (SystemDirectories.Root) is stripped off first, otherwise we'd end up + /// with something + /// like "MyVirtualDirectory-Umbraco" instead of just "Umbraco". + /// + public static string GetUmbracoMvcArea(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + { + if (_mvcArea != null) { - if (_mvcArea != null) return _mvcArea; - - _mvcArea = globalSettings.GetUmbracoMvcAreaNoCache(hostingEnvironment); - return _mvcArea; } - internal static string GetUmbracoMvcAreaNoCache(this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + _mvcArea = globalSettings.GetUmbracoMvcAreaNoCache(hostingEnvironment); + + return _mvcArea; + } + + internal static string GetUmbracoMvcAreaNoCache( + this GlobalSettings globalSettings, + IHostingEnvironment hostingEnvironment) + { + var path = string.IsNullOrEmpty(globalSettings.UmbracoPath) + ? string.Empty + : hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + + if (path.IsNullOrWhiteSpace()) { - var path = string.IsNullOrEmpty(globalSettings.UmbracoPath) - ? string.Empty - : hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); - - if (path.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot create an MVC Area path without the umbracoPath specified"); - - if (path.StartsWith(hostingEnvironment.ApplicationVirtualPath)) // beware of TrimStart, see U4-2518 - path = path.Substring(hostingEnvironment.ApplicationVirtualPath.Length); - return path.TrimStart(Constants.CharArrays.Tilde).TrimStart(Constants.CharArrays.ForwardSlash).Replace('/', '-').Trim().ToLower(); + throw new InvalidOperationException("Cannot create an MVC Area path without the umbracoPath specified"); } + + // beware of TrimStart, see U4-2518 + if (path.StartsWith(hostingEnvironment.ApplicationVirtualPath)) + { + path = path[hostingEnvironment.ApplicationVirtualPath.Length..]; + } + + return path.TrimStart(Constants.CharArrays.Tilde).TrimStart(Constants.CharArrays.ForwardSlash).Replace('/', '-') + .Trim().ToLower(); } } diff --git a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs index 27d6820399..44c9c37dfd 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs @@ -1,18 +1,21 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Configuration.Grid -{ - public class GridConfig : IGridConfig - { - public GridConfig(AppCaches appCaches, IManifestParser manifestParser, IJsonSerializer jsonSerializer, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory) - { - EditorsConfig = new GridEditorsConfig(appCaches, hostingEnvironment, manifestParser, jsonSerializer, loggerFactory.CreateLogger()); - } +namespace Umbraco.Cms.Core.Configuration.Grid; - public IGridEditorsConfig EditorsConfig { get; } - } +public class GridConfig : IGridConfig +{ + public GridConfig( + AppCaches appCaches, + IManifestParser manifestParser, + IJsonSerializer jsonSerializer, + IHostingEnvironment hostingEnvironment, + ILoggerFactory loggerFactory) + => EditorsConfig = + new GridEditorsConfig(appCaches, hostingEnvironment, manifestParser, jsonSerializer, loggerFactory.CreateLogger()); + + public IGridEditorsConfig EditorsConfig { get; } } diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index db5d669ce9..11ae329192 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Reflection; using System.Text; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; @@ -10,78 +8,91 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Grid +namespace Umbraco.Cms.Core.Configuration.Grid; + +internal class GridEditorsConfig : IGridEditorsConfig { - internal class GridEditorsConfig : IGridEditorsConfig + private readonly AppCaches _appCaches; + private readonly IHostingEnvironment _hostingEnvironment; + + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private readonly IManifestParser _manifestParser; + + public GridEditorsConfig( + AppCaches appCaches, + IHostingEnvironment hostingEnvironment, + IManifestParser manifestParser, + IJsonSerializer jsonSerializer, + ILogger logger) { - private readonly AppCaches _appCaches; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IManifestParser _manifestParser; + _appCaches = appCaches; + _hostingEnvironment = hostingEnvironment; + _manifestParser = manifestParser; + _jsonSerializer = jsonSerializer; + _logger = logger; + } - private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger _logger; - - public GridEditorsConfig(AppCaches appCaches, IHostingEnvironment hostingEnvironment, IManifestParser manifestParser,IJsonSerializer jsonSerializer, ILogger logger) + public IEnumerable Editors + { + get { - _appCaches = appCaches; - _hostingEnvironment = hostingEnvironment; - _manifestParser = manifestParser; - _jsonSerializer = jsonSerializer; - _logger = logger; - } - - public IEnumerable Editors - { - get + List GetResult() { - List GetResult() + var configFolder = + new DirectoryInfo(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config)); + var editors = new List(); + var gridConfig = Path.Combine(configFolder.FullName, "grid.editors.config.js"); + if (File.Exists(gridConfig)) { - var configFolder = new DirectoryInfo(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config)); - var editors = new List(); - var gridConfig = Path.Combine(configFolder.FullName, "grid.editors.config.js"); - if (File.Exists(gridConfig)) + var sourceString = File.ReadAllText(gridConfig); + + try { - var sourceString = File.ReadAllText(gridConfig); - - try - { - editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", sourceString); - } + editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); } - else// Read default from embedded file + catch (Exception ex) { - var assembly = GetType().Assembly; - var resourceStream = assembly.GetManifestResourceStream( - "Umbraco.Cms.Core.EmbeddedResources.Grid.grid.editors.config.js"); - - if (resourceStream is not null) - { - using var reader = new StreamReader(resourceStream, Encoding.UTF8); - var sourceString = reader.ReadToEnd(); - editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); - } + _logger.LogError( + ex, + "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", + sourceString); } - - // add manifest editors, skip duplicates - foreach (var gridEditor in _manifestParser.CombinedManifest.GridEditors) - { - if (editors.Contains(gridEditor) == false) editors.Add(gridEditor); - } - - return editors; } - //cache the result if debugging is disabled - var result = _hostingEnvironment.IsDebugMode - ? GetResult() - : _appCaches.RuntimeCache.GetCacheItem>(typeof(GridEditorsConfig) + ".Editors",GetResult, TimeSpan.FromMinutes(10)); + // Read default from embedded file + else + { + Assembly assembly = GetType().Assembly; + Stream? resourceStream = assembly.GetManifestResourceStream( + "Umbraco.Cms.Core.EmbeddedResources.Grid.grid.editors.config.js"); - return result!; + if (resourceStream is not null) + { + using var reader = new StreamReader(resourceStream, Encoding.UTF8); + var sourceString = reader.ReadToEnd(); + editors.AddRange(_jsonSerializer.Deserialize>(sourceString)!); + } + } + + // add manifest editors, skip duplicates + foreach (GridEditor gridEditor in _manifestParser.CombinedManifest.GridEditors) + { + if (editors.Contains(gridEditor) == false) + { + editors.Add(gridEditor); + } + } + + return editors; } + + // cache the result if debugging is disabled + List? result = _hostingEnvironment.IsDebugMode + ? GetResult() + : _appCaches.RuntimeCache.GetCacheItem(typeof(GridEditorsConfig) + ".Editors", GetResult, TimeSpan.FromMinutes(10)); + + return result!; } } } diff --git a/src/Umbraco.Core/Configuration/Grid/IGridConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridConfig.cs index d009eddd25..4dd11ee1fc 100644 --- a/src/Umbraco.Core/Configuration/Grid/IGridConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/IGridConfig.cs @@ -1,9 +1,6 @@ -namespace Umbraco.Cms.Core.Configuration.Grid +namespace Umbraco.Cms.Core.Configuration.Grid; + +public interface IGridConfig { - public interface IGridConfig - { - - IGridEditorsConfig EditorsConfig { get; } - - } + IGridEditorsConfig EditorsConfig { get; } } diff --git a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs index bfd3f17cbf..5103e7a328 100644 --- a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs @@ -1,15 +1,18 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.Grid; -namespace Umbraco.Cms.Core.Configuration.Grid +public interface IGridEditorConfig { - public interface IGridEditorConfig - { - string? Name { get; } - string? NameTemplate { get; } - string Alias { get; } - string? View { get; } - string? Render { get; } - string? Icon { get; } - IDictionary Config { get; } - } + string? Name { get; } + + string? NameTemplate { get; } + + string Alias { get; } + + string? View { get; } + + string? Render { get; } + + string? Icon { get; } + + IDictionary Config { get; } } diff --git a/src/Umbraco.Core/Configuration/Grid/IGridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridEditorsConfig.cs index a49ae41d6c..e0d8c8f8d4 100644 --- a/src/Umbraco.Core/Configuration/Grid/IGridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/IGridEditorsConfig.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.Grid; -namespace Umbraco.Cms.Core.Configuration.Grid +public interface IGridEditorsConfig { - public interface IGridEditorsConfig - { - IEnumerable Editors { get; } - } + IEnumerable Editors { get; } } diff --git a/src/Umbraco.Core/Configuration/IConfigManipulator.cs b/src/Umbraco.Core/Configuration/IConfigManipulator.cs index c99f90e5c9..18ce8a5eca 100644 --- a/src/Umbraco.Core/Configuration/IConfigManipulator.cs +++ b/src/Umbraco.Core/Configuration/IConfigManipulator.cs @@ -1,11 +1,14 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public interface IConfigManipulator { - public interface IConfigManipulator - { - void RemoveConnectionString(); - void SaveConnectionString(string connectionString, string? providerName); - void SaveConfigValue(string itemPath, object value); - void SaveDisableRedirectUrlTracking(bool disable); - void SetGlobalId(string id); - } + void RemoveConnectionString(); + + void SaveConnectionString(string connectionString, string? providerName); + + void SaveConfigValue(string itemPath, object value); + + void SaveDisableRedirectUrlTracking(bool disable); + + void SetGlobalId(string id); } diff --git a/src/Umbraco.Core/Configuration/ICronTabParser.cs b/src/Umbraco.Core/Configuration/ICronTabParser.cs index 565d9fa47b..bd3808ecd1 100644 --- a/src/Umbraco.Core/Configuration/ICronTabParser.cs +++ b/src/Umbraco.Core/Configuration/ICronTabParser.cs @@ -1,28 +1,25 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Configuration +/// +/// Defines the contract for that allows the parsing of chrontab expressions. +/// +public interface ICronTabParser { /// - /// Defines the contract for that allows the parsing of chrontab expressions. + /// Returns a value indicating whether a given chrontab expression is valid. /// - public interface ICronTabParser - { - /// - /// Returns a value indicating whether a given chrontab expression is valid. - /// - /// The chrontab expression to parse. - /// The result. - bool IsValidCronTab(string cronTab); + /// The chrontab expression to parse. + /// The result. + bool IsValidCronTab(string cronTab); - /// - /// Returns the next occurence for the given chrontab expression from the given time. - /// - /// The chrontab expression to parse. - /// The date and time to start from. - /// The representing the next occurence. - DateTime GetNextOccurrence(string cronTab, DateTime time); - } + /// + /// Returns the next occurence for the given chrontab expression from the given time. + /// + /// The chrontab expression to parse. + /// The date and time to start from. + /// The representing the next occurence. + DateTime GetNextOccurrence(string cronTab, DateTime time); } diff --git a/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs b/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs index 09ea5058df..857b62bb26 100644 --- a/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs +++ b/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Provides metadata about the entry assembly. +/// +public interface IEntryAssemblyMetadata { /// - /// Provides metadata about the entry assembly. + /// Gets the Name of entry assembly. /// - public interface IEntryAssemblyMetadata - { - /// - /// Gets the Name of entry assembly. - /// - public string Name { get; } + public string Name { get; } - /// - /// Gets the InformationalVersion string for entry assembly. - /// - public string InformationalVersion { get; } - } + /// + /// Gets the InformationalVersion string for entry assembly. + /// + public string InformationalVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/IMemberPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/IMemberPasswordConfiguration.cs index 7bd8ab9ef2..451cf51bc3 100644 --- a/src/Umbraco.Core/Configuration/IMemberPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/IMemberPasswordConfiguration.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// The password configuration for members +/// +public interface IMemberPasswordConfiguration : IPasswordConfiguration { - /// - /// The password configuration for members - /// - public interface IMemberPasswordConfiguration : IPasswordConfiguration - { - } } diff --git a/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs index acfe81ece9..e0e934f550 100644 --- a/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/IPasswordConfiguration.cs @@ -1,50 +1,48 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Password configuration +/// +public interface IPasswordConfiguration { + /// + /// Gets a value for the minimum required length for the password. + /// + int RequiredLength { get; } /// - /// Password configuration + /// Gets a value indicating whether at least one non-letter or digit is required for the password. /// - public interface IPasswordConfiguration - { - /// - /// Gets a value for the minimum required length for the password. - /// - int RequiredLength { get; } + bool RequireNonLetterOrDigit { get; } - /// - /// Gets a value indicating whether at least one non-letter or digit is required for the password. - /// - bool RequireNonLetterOrDigit { get; } + /// + /// Gets a value indicating whether at least one digit is required for the password. + /// + bool RequireDigit { get; } - /// - /// Gets a value indicating whether at least one digit is required for the password. - /// - bool RequireDigit { get; } + /// + /// Gets a value indicating whether at least one lower-case character is required for the password. + /// + bool RequireLowercase { get; } - /// - /// Gets a value indicating whether at least one lower-case character is required for the password. - /// - bool RequireLowercase { get; } + /// + /// Gets a value indicating whether at least one upper-case character is required for the password. + /// + bool RequireUppercase { get; } - /// - /// Gets a value indicating whether at least one upper-case character is required for the password. - /// - bool RequireUppercase { get; } + /// + /// Gets a value for the password hash algorithm type. + /// + string HashAlgorithmType { get; } - /// - /// Gets a value for the password hash algorithm type. - /// - string HashAlgorithmType { get; } - - /// - /// Gets a value for the maximum failed access attempts before lockout. - /// - /// - /// TODO: This doesn't really belong here - /// - int MaxFailedAccessAttemptsBeforeLockout { get; } - } + /// + /// Gets a value for the maximum failed access attempts before lockout. + /// + /// + /// TODO: This doesn't really belong here + /// + int MaxFailedAccessAttemptsBeforeLockout { get; } } diff --git a/src/Umbraco.Core/Configuration/ITypeFinderSettings.cs b/src/Umbraco.Core/Configuration/ITypeFinderSettings.cs index e9842ee4cb..4acbd1dbd8 100644 --- a/src/Umbraco.Core/Configuration/ITypeFinderSettings.cs +++ b/src/Umbraco.Core/Configuration/ITypeFinderSettings.cs @@ -1,8 +1,6 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +[Obsolete("Not used anymore, will be removed in Umbraco 12")]public interface ITypeFinderSettings { - [Obsolete("Not used anymore, will be removed in Umbraco 12")] - public interface ITypeFinderSettings - { - string AssembliesAcceptingLoadExceptions { get; } - } + string AssembliesAcceptingLoadExceptions { get; } } diff --git a/src/Umbraco.Core/Configuration/IUmbracoConfigurationSection.cs b/src/Umbraco.Core/Configuration/IUmbracoConfigurationSection.cs index 4a1e65f13f..5547639b11 100644 --- a/src/Umbraco.Core/Configuration/IUmbracoConfigurationSection.cs +++ b/src/Umbraco.Core/Configuration/IUmbracoConfigurationSection.cs @@ -1,10 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration -{ - /// - /// Represents an Umbraco configuration section which can be used to pass to UmbracoConfiguration.For{T} - /// - public interface IUmbracoConfigurationSection - { +namespace Umbraco.Cms.Core.Configuration; - } +/// +/// Represents an Umbraco configuration section which can be used to pass to UmbracoConfiguration.For{T} +/// +public interface IUmbracoConfigurationSection +{ } diff --git a/src/Umbraco.Core/Configuration/IUmbracoVersion.cs b/src/Umbraco.Core/Configuration/IUmbracoVersion.cs index 2758d9dabf..3672f28dae 100644 --- a/src/Umbraco.Core/Configuration/IUmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/IUmbracoVersion.cs @@ -1,46 +1,45 @@ -using System; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public interface IUmbracoVersion { - public interface IUmbracoVersion - { - /// - /// Gets the non-semantic version of the Umbraco code. - /// - Version Version { get; } + /// + /// Gets the non-semantic version of the Umbraco code. + /// + Version Version { get; } - /// - /// Gets the semantic version comments of the Umbraco code. - /// - string Comment { get; } + /// + /// Gets the semantic version comments of the Umbraco code. + /// + string Comment { get; } - /// - /// Gets the assembly version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// Is the one that the CLR checks for compatibility. Therefore, it changes only on - /// hard-breaking changes (for instance, on new major versions). - /// - Version? AssemblyVersion { get; } + /// + /// Gets the assembly version of the Umbraco code. + /// + /// + /// The assembly version is the value of the . + /// + /// Is the one that the CLR checks for compatibility. Therefore, it changes only on + /// hard-breaking changes (for instance, on new major versions). + /// + /// + Version? AssemblyVersion { get; } - /// - /// Gets the assembly file version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// - Version? AssemblyFileVersion { get; } + /// + /// Gets the assembly file version of the Umbraco code. + /// + /// + /// The assembly version is the value of the . + /// + Version? AssemblyFileVersion { get; } - /// - /// Gets the semantic version of the Umbraco code. - /// - /// - /// The semantic version is the value of the . - /// It is the full version of Umbraco, including comments. - /// - SemVersion SemanticVersion { get; } - - } + /// + /// Gets the semantic version of the Umbraco code. + /// + /// + /// The semantic version is the value of the . + /// It is the full version of Umbraco, including comments. + /// + SemVersion SemanticVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs index db27103a67..c4f86232d3 100644 --- a/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/IUserPasswordConfiguration.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// The password configuration for back office users +/// +public interface IUserPasswordConfiguration : IPasswordConfiguration { - /// - /// The password configuration for back office users - /// - public interface IUserPasswordConfiguration : IPasswordConfiguration - { - } } diff --git a/src/Umbraco.Core/Configuration/LocalTempStorage.cs b/src/Umbraco.Core/Configuration/LocalTempStorage.cs index 696ec7900e..8be409fc2b 100644 --- a/src/Umbraco.Core/Configuration/LocalTempStorage.cs +++ b/src/Umbraco.Core/Configuration/LocalTempStorage.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public enum LocalTempStorage { - public enum LocalTempStorage - { - Unknown = 0, - Default, - EnvironmentTemp - } + Unknown = 0, + Default, + EnvironmentTemp, } diff --git a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs index c7ce20454f..33471ced16 100644 --- a/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/MemberPasswordConfiguration.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// The password configuration for members +/// +public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration { - /// - /// The password configuration for members - /// - public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration + public MemberPasswordConfiguration(IMemberPasswordConfiguration configSettings) + : base(configSettings) { - public MemberPasswordConfiguration(IMemberPasswordConfiguration configSettings) - : base(configSettings) - { - } } } diff --git a/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs b/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs index 646cd7ff9f..3373b7a778 100644 --- a/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ActiveDirectorySettings.cs @@ -1,18 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for active directory settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigActiveDirectory)] +[Obsolete("This is not used anymore. Will be removed in Umbraco 12")]public class ActiveDirectorySettings { /// - /// Typed configuration options for active directory settings. + /// Gets or sets a value for the Active Directory domain. /// - [UmbracoOptions(Constants.Configuration.ConfigActiveDirectory)] - [Obsolete("This is not used anymore. Will be removed in Umbraco 12")] - public class ActiveDirectorySettings - { - /// - /// Gets or sets a value for the Active Directory domain. - /// - public string? Domain { get; set; } - } + public string? Domain { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/Attributes/UmbracoOptionsAttribute.cs b/src/Umbraco.Core/Configuration/Models/Attributes/UmbracoOptionsAttribute.cs index 211b6b3d83..5f42aac545 100644 --- a/src/Umbraco.Core/Configuration/Models/Attributes/UmbracoOptionsAttribute.cs +++ b/src/Umbraco.Core/Configuration/Models/Attributes/UmbracoOptionsAttribute.cs @@ -1,16 +1,11 @@ -using System; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +[AttributeUsage(AttributeTargets.Class)] +public class UmbracoOptionsAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class UmbracoOptionsAttribute : Attribute - { - public string ConfigurationKey { get; } - public bool BindNonPublicProperties { get; set; } + public UmbracoOptionsAttribute(string configurationKey) => ConfigurationKey = configurationKey; - public UmbracoOptionsAttribute(string configurationKey) - { - ConfigurationKey = configurationKey; - } - } + public string ConfigurationKey { get; } + + public bool BindNonPublicProperties { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs index aa82f69d2e..b743fdcdd2 100644 --- a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs @@ -1,40 +1,37 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -using System.Net; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for basic authentication settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigBasicAuth)] +public class BasicAuthSettings { + private const bool StaticEnabled = false; + /// - /// Typed configuration options for basic authentication settings. + /// Gets or sets a value indicating whether to keep the user logged in. /// - [UmbracoOptions(Constants.Configuration.ConfigBasicAuth)] - public class BasicAuthSettings - { - private const bool StaticEnabled = false; - - /// - /// Gets or sets a value indicating whether to keep the user logged in. - /// - [DefaultValue(StaticEnabled)] - public bool Enabled { get; set; } = StaticEnabled; + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; - public string[] AllowedIPs { get; set; } = Array.Empty(); - public SharedSecret SharedSecret { get; set; } = new SharedSecret(); + public string[] AllowedIPs { get; set; } = Array.Empty(); + public SharedSecret SharedSecret { get; set; } = new SharedSecret(); - public bool RedirectToLoginPage { get; set; } = false; + public bool RedirectToLoginPage { get; set; } = false; - } - - public class SharedSecret - { - private const string StaticHeaderName = "X-Authentication-Shared-Secret"; - - [DefaultValue(StaticHeaderName)] - public string? HeaderName { get; set; } = StaticHeaderName; - public string? Value { get; set; } - } +} + +public class SharedSecret +{ + private const string StaticHeaderName = "X-Authentication-Shared-Secret"; + + [DefaultValue(StaticHeaderName)] + public string? HeaderName { get; set; } = StaticHeaderName; + public string? Value { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/CharItem.cs b/src/Umbraco.Core/Configuration/Models/CharItem.cs index a74b0c0a8b..625033a82a 100644 --- a/src/Umbraco.Core/Configuration/Models/CharItem.cs +++ b/src/Umbraco.Core/Configuration/Models/CharItem.cs @@ -1,17 +1,16 @@ using Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Cms.Core.Configuration.Models -{ - public class CharItem : IChar - { - /// - /// The character to replace - /// - public string Char { get; set; } = null!; +namespace Umbraco.Cms.Core.Configuration.Models; - /// - /// The replacement character - /// - public string Replacement { get; set; } = null!; - } +public class CharItem : IChar +{ + /// + /// The character to replace + /// + public string Char { get; set; } = null!; + + /// + /// The replacement character + /// + public string Replacement { get; set; } = null!; } diff --git a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs index f4703adf92..a5161eca86 100644 --- a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs @@ -8,17 +8,17 @@ namespace Umbraco.Cms.Core.Configuration.Models; public class ConnectionStrings // TODO: Rename to [Umbraco]ConnectionString (since v10 this only contains a single connection string) { /// - /// The default provider name when not present in configuration. + /// The default provider name when not present in configuration. /// public const string DefaultProviderName = "Microsoft.Data.SqlClient"; /// - /// The DataDirectory placeholder. + /// The DataDirectory placeholder. /// public const string DataDirectoryPlaceholder = ConfigurationExtensions.DataDirectoryPlaceholder; /// - /// The postfix used to identify a connection strings provider setting. + /// The postfix used to identify a connection strings provider setting. /// public const string ProviderNamePostfix = ConfigurationExtensions.ProviderNamePostfix; diff --git a/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs index 19d636ed34..74376a3ed2 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs @@ -1,36 +1,35 @@ using System.ComponentModel; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Typed configuration options for content dashboard settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigContentDashboard)] +public class ContentDashboardSettings { + private const string DefaultContentDashboardPath = "cms"; + /// - /// Typed configuration options for content dashboard settings. + /// Gets a value indicating whether the content dashboard should be available to all users. /// - [UmbracoOptions(Constants.Configuration.ConfigContentDashboard)] - public class ContentDashboardSettings - { - private const string DefaultContentDashboardPath = "cms"; + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + public bool AllowContentDashboardAccessToAllUsers { get; set; } = true; - /// - /// Gets a value indicating whether the content dashboard should be available to all users. - /// - /// - /// true if the dashboard is visible for all user groups; otherwise, false - /// and the default access rules for that dashboard will be in use. - /// - public bool AllowContentDashboardAccessToAllUsers { get; set; } = true; + /// + /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. + /// + /// The URL path. + [DefaultValue(DefaultContentDashboardPath)] + public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; - /// - /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. - /// - /// The URL path. - [DefaultValue(DefaultContentDashboardPath)] - public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; - - /// - /// Gets the allowed addresses to retrieve data for the content dashboard. - /// - /// The URLs. - public string[]? ContentDashboardUrlAllowlist { get; set; } - } + /// + /// Gets the allowed addresses to retrieve data for the content dashboard. + /// + /// The URLs. + public string[]? ContentDashboardUrlAllowlist { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs b/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs index 6a6d3a8e61..415240e017 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentErrorPage.cs @@ -1,55 +1,53 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Configuration.Models.Validation; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration for a content error page. +/// +public class ContentErrorPage : ValidatableEntryBase { /// - /// Typed configuration for a content error page. + /// Gets or sets a value for the content Id. /// - public class ContentErrorPage : ValidatableEntryBase - { - /// - /// Gets or sets a value for the content Id. - /// - public int ContentId { get; set; } + public int ContentId { get; set; } - /// - /// Gets or sets a value for the content key. - /// - public Guid ContentKey { get; set; } + /// + /// Gets or sets a value for the content key. + /// + public Guid ContentKey { get; set; } - /// - /// Gets or sets a value for the content XPath. - /// - public string? ContentXPath { get; set; } + /// + /// Gets or sets a value for the content XPath. + /// + public string? ContentXPath { get; set; } - /// - /// Gets a value indicating whether the field is populated. - /// - public bool HasContentId => ContentId != 0; + /// + /// Gets a value indicating whether the field is populated. + /// + public bool HasContentId => ContentId != 0; - /// - /// Gets a value indicating whether the field is populated. - /// - public bool HasContentKey => ContentKey != Guid.Empty; + /// + /// Gets a value indicating whether the field is populated. + /// + public bool HasContentKey => ContentKey != Guid.Empty; - /// - /// Gets a value indicating whether the field is populated. - /// - public bool HasContentXPath => !string.IsNullOrEmpty(ContentXPath); + /// + /// Gets a value indicating whether the field is populated. + /// + public bool HasContentXPath => !string.IsNullOrEmpty(ContentXPath); - /// - /// Gets or sets a value for the content culture. - /// - [Required] - public string Culture { get; set; } = null!; + /// + /// Gets or sets a value for the content culture. + /// + [Required] + public string Culture { get; set; } = null!; - internal override bool IsValid() => - base.IsValid() && - ((HasContentId ? 1 : 0) + (HasContentKey ? 1 : 0) + (HasContentXPath ? 1 : 0) == 1); - } + internal override bool IsValid() => + base.IsValid() && + (HasContentId ? 1 : 0) + (HasContentKey ? 1 : 0) + (HasContentXPath ? 1 : 0) == 1; } diff --git a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs index 2e109fe310..4634f6efb9 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs @@ -1,39 +1,37 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for content imaging settings. +/// +public class ContentImagingSettings { - /// - /// Typed configuration options for content imaging settings. - /// - public class ContentImagingSettings + internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; + + private static readonly ImagingAutoFillUploadField[] DefaultImagingAutoFillUploadField = { - private static readonly ImagingAutoFillUploadField[] s_defaultImagingAutoFillUploadField = + new() { - new ImagingAutoFillUploadField - { - Alias = Constants.Conventions.Media.File, - WidthFieldAlias = Constants.Conventions.Media.Width, - HeightFieldAlias = Constants.Conventions.Media.Height, - ExtensionFieldAlias = Constants.Conventions.Media.Extension, - LengthFieldAlias = Constants.Conventions.Media.Bytes, - } - }; + Alias = Constants.Conventions.Media.File, + WidthFieldAlias = Constants.Conventions.Media.Width, + HeightFieldAlias = Constants.Conventions.Media.Height, + ExtensionFieldAlias = Constants.Conventions.Media.Extension, + LengthFieldAlias = Constants.Conventions.Media.Bytes, + }, + }; - internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; + /// + /// Gets or sets a value for the collection of accepted image file extensions. + /// + [DefaultValue(StaticImageFileTypes)] + public string[] ImageFileTypes { get; set; } = StaticImageFileTypes.Split(','); - /// - /// Gets or sets a value for the collection of accepted image file extensions. - /// - [DefaultValue(StaticImageFileTypes)] - public string[] ImageFileTypes { get; set; } = StaticImageFileTypes.Split(','); - - /// - /// Gets or sets a value for the imaging autofill following media file upload fields. - /// - public ImagingAutoFillUploadField[] AutoFillImageProperties { get; set; } = s_defaultImagingAutoFillUploadField; - } + /// + /// Gets or sets a value for the imaging autofill following media file upload fields. + /// + public ImagingAutoFillUploadField[] AutoFillImageProperties { get; set; } = DefaultImagingAutoFillUploadField; } diff --git a/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs index c23eac75b2..ce5c3aebf3 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentNotificationSettings.cs @@ -3,24 +3,23 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for content notification settings. +/// +public class ContentNotificationSettings { + internal const bool StaticDisableHtmlEmail = false; + /// - /// Typed configuration options for content notification settings. + /// Gets or sets a value for the email address for notifications. /// - public class ContentNotificationSettings - { - internal const bool StaticDisableHtmlEmail = false; + public string? Email { get; set; } - /// - /// Gets or sets a value for the email address for notifications. - /// - public string? Email { get; set; } - - /// - /// Gets or sets a value indicating whether HTML email notifications should be disabled. - /// - [DefaultValue(StaticDisableHtmlEmail)] - public bool DisableHtmlEmail { get; set; } = StaticDisableHtmlEmail; - } + /// + /// Gets or sets a value indicating whether HTML email notifications should be disabled. + /// + [DefaultValue(StaticDisableHtmlEmail)] + public bool DisableHtmlEmail { get; set; } = StaticDisableHtmlEmail; } diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index e6e5c7006f..f0532a7203 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -1,23 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Macros; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for content settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigContent)] - public class ContentSettings - { +namespace Umbraco.Cms.Core.Configuration.Models; - internal const bool StaticResolveUrlsFromTextString = false; - internal const string StaticDefaultPreviewBadge = - @" +/// +/// Typed configuration options for content settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigContent)] +public class ContentSettings +{ + internal const bool StaticResolveUrlsFromTextString = false; + + internal const string StaticDefaultPreviewBadge = + @"
Preview mode @@ -151,98 +149,97 @@ namespace Umbraco.Cms.Core.Configuration.Models "; - internal const string StaticMacroErrors = "Inline"; - internal const string StaticDisallowedUploadFiles = "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx"; - internal const bool StaticShowDeprecatedPropertyEditors = false; - internal const string StaticLoginBackgroundImage = "assets/img/login.jpg"; - internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg"; - internal const bool StaticHideBackOfficeLogo = false; - internal const bool StaticDisableDeleteWhenReferenced = false; - internal const bool StaticDisableUnpublishWhenReferenced = false; + internal const string StaticMacroErrors = "Inline"; + internal const string StaticDisallowedUploadFiles = "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx"; + internal const bool StaticShowDeprecatedPropertyEditors = false; + internal const string StaticLoginBackgroundImage = "assets/img/login.jpg"; + internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg"; + internal const bool StaticHideBackOfficeLogo = false; + internal const bool StaticDisableDeleteWhenReferenced = false; + internal const bool StaticDisableUnpublishWhenReferenced = false; - /// - /// Gets or sets a value for the content notification settings. - /// - public ContentNotificationSettings Notifications { get; set; } = new ContentNotificationSettings(); + /// + /// Gets or sets a value for the content notification settings. + /// + public ContentNotificationSettings Notifications { get; set; } = new(); - /// - /// Gets or sets a value for the content imaging settings. - /// - public ContentImagingSettings Imaging { get; set; } = new ContentImagingSettings(); + /// + /// Gets or sets a value for the content imaging settings. + /// + public ContentImagingSettings Imaging { get; set; } = new(); - /// - /// Gets or sets a value indicating whether URLs should be resolved from text strings. - /// - [DefaultValue(StaticResolveUrlsFromTextString)] - public bool ResolveUrlsFromTextString { get; set; } = StaticResolveUrlsFromTextString; + /// + /// Gets or sets a value indicating whether URLs should be resolved from text strings. + /// + [DefaultValue(StaticResolveUrlsFromTextString)] + public bool ResolveUrlsFromTextString { get; set; } = StaticResolveUrlsFromTextString; - /// - /// Gets or sets a value for the collection of error pages. - /// - public ContentErrorPage[] Error404Collection { get; set; } = Array.Empty(); + /// + /// Gets or sets a value for the collection of error pages. + /// + public ContentErrorPage[] Error404Collection { get; set; } = Array.Empty(); - /// - /// Gets or sets a value for the preview badge mark-up. - /// - [DefaultValue(StaticDefaultPreviewBadge)] - public string PreviewBadge { get; set; } = StaticDefaultPreviewBadge; + /// + /// Gets or sets a value for the preview badge mark-up. + /// + [DefaultValue(StaticDefaultPreviewBadge)] + public string PreviewBadge { get; set; } = StaticDefaultPreviewBadge; - /// - /// Gets or sets a value for the macro error behaviour. - /// - [DefaultValue(StaticMacroErrors)] - public MacroErrorBehaviour MacroErrors { get; set; } = Enum.Parse(StaticMacroErrors); + /// + /// Gets or sets a value for the macro error behaviour. + /// + [DefaultValue(StaticMacroErrors)] + public MacroErrorBehaviour MacroErrors { get; set; } = Enum.Parse(StaticMacroErrors); - /// - /// Gets or sets a value for the collection of file extensions that are disallowed for upload. - /// - [DefaultValue(StaticDisallowedUploadFiles)] - public IEnumerable DisallowedUploadFiles { get; set; } = StaticDisallowedUploadFiles.Split(','); + /// + /// Gets or sets a value for the collection of file extensions that are disallowed for upload. + /// + [DefaultValue(StaticDisallowedUploadFiles)] + public IEnumerable DisallowedUploadFiles { get; set; } = StaticDisallowedUploadFiles.Split(','); - /// - /// Gets or sets a value for the collection of file extensions that are allowed for upload. - /// - public IEnumerable AllowedUploadFiles { get; set; } = Array.Empty(); + /// + /// Gets or sets a value for the collection of file extensions that are allowed for upload. + /// + public IEnumerable AllowedUploadFiles { get; set; } = Array.Empty(); - /// - /// Gets or sets a value indicating whether deprecated property editors should be shown. - /// - [DefaultValue(StaticShowDeprecatedPropertyEditors)] - public bool ShowDeprecatedPropertyEditors { get; set; } = StaticShowDeprecatedPropertyEditors; + /// + /// Gets or sets a value indicating whether deprecated property editors should be shown. + /// + [DefaultValue(StaticShowDeprecatedPropertyEditors)] + public bool ShowDeprecatedPropertyEditors { get; set; } = StaticShowDeprecatedPropertyEditors; - /// - /// Gets or sets a value for the path to the login screen background image. - /// - [DefaultValue(StaticLoginBackgroundImage)] - public string LoginBackgroundImage { get; set; } = StaticLoginBackgroundImage; + /// + /// Gets or sets a value for the path to the login screen background image. + /// + [DefaultValue(StaticLoginBackgroundImage)] + public string LoginBackgroundImage { get; set; } = StaticLoginBackgroundImage; - /// - /// Gets or sets a value for the path to the login screen logo image. - /// - [DefaultValue(StaticLoginLogoImage)] - public string LoginLogoImage { get; set; } = StaticLoginLogoImage; + /// + /// Gets or sets a value for the path to the login screen logo image. + /// + [DefaultValue(StaticLoginLogoImage)] + public string LoginLogoImage { get; set; } = StaticLoginLogoImage; - /// - /// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not. - /// - [DefaultValue(StaticHideBackOfficeLogo)] - public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo; + /// + /// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not. + /// + [DefaultValue(StaticHideBackOfficeLogo)] + public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo; - /// - /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items. - /// - [DefaultValue(StaticDisableDeleteWhenReferenced)] - public bool DisableDeleteWhenReferenced { get; set; } = StaticDisableDeleteWhenReferenced; + /// + /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items. + /// + [DefaultValue(StaticDisableDeleteWhenReferenced)] + public bool DisableDeleteWhenReferenced { get; set; } = StaticDisableDeleteWhenReferenced; - /// - /// Gets or sets a value indicating whether to disable the unpublishing of items referenced by other items. - /// - [DefaultValue(StaticDisableUnpublishWhenReferenced)] - public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced; + /// + /// Gets or sets a value indicating whether to disable the unpublishing of items referenced by other items. + /// + [DefaultValue(StaticDisableUnpublishWhenReferenced)] + public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced; - /// - /// Get or sets the model representing the global content version cleanup policy - /// - public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new ContentVersionCleanupPolicySettings(); - } + /// + /// Get or sets the model representing the global content version cleanup policy + /// + public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new(); } diff --git a/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs index bd460058eb..ed721382a9 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs @@ -1,33 +1,31 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Model representing the global content version cleanup policy +/// +public class ContentVersionCleanupPolicySettings { + private const bool StaticEnableCleanup = false; + private const int StaticKeepAllVersionsNewerThanDays = 7; + private const int StaticKeepLatestVersionPerDayForDays = 90; + /// - /// Model representing the global content version cleanup policy + /// Gets or sets a value indicating whether or not the cleanup job should be executed. /// - public class ContentVersionCleanupPolicySettings - { - private const bool StaticEnableCleanup = false; - private const int StaticKeepAllVersionsNewerThanDays = 7; - private const int StaticKeepLatestVersionPerDayForDays = 90; + [DefaultValue(StaticEnableCleanup)] + public bool EnableCleanup { get; set; } = StaticEnableCleanup; - /// - /// Gets or sets a value indicating whether or not the cleanup job should be executed. - /// - [DefaultValue(StaticEnableCleanup)] - public bool EnableCleanup { get; set; } = StaticEnableCleanup; + /// + /// Gets or sets the number of days where all historical content versions are kept. + /// + [DefaultValue(StaticKeepAllVersionsNewerThanDays)] + public int KeepAllVersionsNewerThanDays { get; set; } = StaticKeepAllVersionsNewerThanDays; - /// - /// Gets or sets the number of days where all historical content versions are kept. - /// - [DefaultValue(StaticKeepAllVersionsNewerThanDays)] - public int KeepAllVersionsNewerThanDays { get; set; } = StaticKeepAllVersionsNewerThanDays; - - /// - /// Gets or sets the number of days where the latest historical content version for that day are kept. - /// - [DefaultValue(StaticKeepLatestVersionPerDayForDays)] - public int KeepLatestVersionPerDayForDays { get; set; } = StaticKeepLatestVersionPerDayForDays; - - } + /// + /// Gets or sets the number of days where the latest historical content version for that day are kept. + /// + [DefaultValue(StaticKeepLatestVersionPerDayForDays)] + public int KeepLatestVersionPerDayForDays { get; set; } = StaticKeepLatestVersionPerDayForDays; } diff --git a/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs b/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs index 58810a3462..052d37c5fe 100644 --- a/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/CoreDebugSettings.cs @@ -3,27 +3,26 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for core debug settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigCoreDebug)] +public class CoreDebugSettings { + internal const bool StaticLogIncompletedScopes = false; + internal const bool StaticDumpOnTimeoutThreadAbort = false; + /// - /// Typed configuration options for core debug settings. + /// Gets or sets a value indicating whether incompleted scopes should be logged. /// - [UmbracoOptions(Constants.Configuration.ConfigCoreDebug)] - public class CoreDebugSettings - { - internal const bool StaticLogIncompletedScopes = false; - internal const bool StaticDumpOnTimeoutThreadAbort = false; + [DefaultValue(StaticLogIncompletedScopes)] + public bool LogIncompletedScopes { get; set; } = StaticLogIncompletedScopes; - /// - /// Gets or sets a value indicating whether incompleted scopes should be logged. - /// - [DefaultValue(StaticLogIncompletedScopes)] - public bool LogIncompletedScopes { get; set; } = StaticLogIncompletedScopes; - - /// - /// Gets or sets a value indicating whether memory dumps on thread abort should be taken. - /// - [DefaultValue(StaticDumpOnTimeoutThreadAbort)] - public bool DumpOnTimeoutThreadAbort { get; set; } = StaticDumpOnTimeoutThreadAbort; - } + /// + /// Gets or sets a value indicating whether memory dumps on thread abort should be taken. + /// + [DefaultValue(StaticDumpOnTimeoutThreadAbort)] + public bool DumpOnTimeoutThreadAbort { get; set; } = StaticDumpOnTimeoutThreadAbort; } diff --git a/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs b/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs index f1320a199f..a083b164a8 100644 --- a/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs @@ -1,43 +1,43 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for database server messaging settings. +/// +public class DatabaseServerMessengerSettings { + internal const int StaticMaxProcessingInstructionCount = 1000; + internal const string StaticTimeToRetainInstructions = "2.00:00:00"; // TimeSpan.FromDays(2); + internal const string StaticTimeBetweenSyncOperations = "00:00:05"; // TimeSpan.FromSeconds(5); + internal const string StaticTimeBetweenPruneOperations = "00:01:00"; // TimeSpan.FromMinutes(1); + /// - /// Typed configuration options for database server messaging settings. + /// Gets or sets a value for the maximum number of instructions that can be processed at startup; otherwise the server + /// cold-boots (rebuilds its caches). /// - public class DatabaseServerMessengerSettings - { - internal const int StaticMaxProcessingInstructionCount = 1000; - internal const string StaticTimeToRetainInstructions = "2.00:00:00"; // TimeSpan.FromDays(2); - internal const string StaticTimeBetweenSyncOperations = "00:00:05"; // TimeSpan.FromSeconds(5); - internal const string StaticTimeBetweenPruneOperations = "00:01:00"; // TimeSpan.FromMinutes(1); + [DefaultValue(StaticMaxProcessingInstructionCount)] + public int MaxProcessingInstructionCount { get; set; } = StaticMaxProcessingInstructionCount; - /// - /// Gets or sets a value for the maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches). - /// - [DefaultValue(StaticMaxProcessingInstructionCount)] - public int MaxProcessingInstructionCount { get; set; } = StaticMaxProcessingInstructionCount; + /// + /// Gets or sets a value for the time to keep instructions in the database; records older than this number will be + /// pruned. + /// + [DefaultValue(StaticTimeToRetainInstructions)] + public TimeSpan TimeToRetainInstructions { get; set; } = TimeSpan.Parse(StaticTimeToRetainInstructions); - /// - /// Gets or sets a value for the time to keep instructions in the database; records older than this number will be pruned. - /// - [DefaultValue(StaticTimeToRetainInstructions)] - public TimeSpan TimeToRetainInstructions { get; set; } = TimeSpan.Parse(StaticTimeToRetainInstructions); + /// + /// Gets or sets a value for the time to wait between each sync operations. + /// + [DefaultValue(StaticTimeBetweenSyncOperations)] + public TimeSpan TimeBetweenSyncOperations { get; set; } = TimeSpan.Parse(StaticTimeBetweenSyncOperations); - /// - /// Gets or sets a value for the time to wait between each sync operations. - /// - [DefaultValue(StaticTimeBetweenSyncOperations)] - public TimeSpan TimeBetweenSyncOperations { get; set; } = TimeSpan.Parse(StaticTimeBetweenSyncOperations); - - /// - /// Gets or sets a value for the time to wait between each prune operations. - /// - [DefaultValue(StaticTimeBetweenPruneOperations)] - public TimeSpan TimeBetweenPruneOperations { get; set; } = TimeSpan.Parse(StaticTimeBetweenPruneOperations); - } + /// + /// Gets or sets a value for the time to wait between each prune operations. + /// + [DefaultValue(StaticTimeBetweenPruneOperations)] + public TimeSpan TimeBetweenPruneOperations { get; set; } = TimeSpan.Parse(StaticTimeBetweenPruneOperations); } diff --git a/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs b/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs index 91d293f272..80aefeae6e 100644 --- a/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs @@ -1,29 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for database server registrar settings. +/// +public class DatabaseServerRegistrarSettings { + internal const string StaticWaitTimeBetweenCalls = "00:01:00"; + internal const string StaticStaleServerTimeout = "00:02:00"; + /// - /// Typed configuration options for database server registrar settings. + /// Gets or sets a value for the amount of time to wait between calls to the database on the background thread. /// - public class DatabaseServerRegistrarSettings - { - internal const string StaticWaitTimeBetweenCalls = "00:01:00"; - internal const string StaticStaleServerTimeout = "00:02:00"; + [DefaultValue(StaticWaitTimeBetweenCalls)] + public TimeSpan WaitTimeBetweenCalls { get; set; } = TimeSpan.Parse(StaticWaitTimeBetweenCalls); - /// - /// Gets or sets a value for the amount of time to wait between calls to the database on the background thread. - /// - [DefaultValue(StaticWaitTimeBetweenCalls)] - public TimeSpan WaitTimeBetweenCalls { get; set; } = TimeSpan.Parse(StaticWaitTimeBetweenCalls); - - /// - /// Gets or sets a value for the time span to wait before considering a server stale, after it has last been accessed. - /// - [DefaultValue(StaticStaleServerTimeout)] - public TimeSpan StaleServerTimeout { get; set; } = TimeSpan.Parse(StaticStaleServerTimeout); - } + /// + /// Gets or sets a value for the time span to wait before considering a server stale, after it has last been accessed. + /// + [DefaultValue(StaticStaleServerTimeout)] + public TimeSpan StaleServerTimeout { get; set; } = TimeSpan.Parse(StaticStaleServerTimeout); } diff --git a/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs b/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs index a24ec5b923..f055aca7ab 100644 --- a/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs @@ -1,28 +1,25 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// Typed configuration options for disabled healthcheck settings. +/// +public class DisabledHealthCheckSettings { /// - /// Typed configuration options for disabled healthcheck settings. + /// Gets or sets a value for the healthcheck Id to disable. /// - public class DisabledHealthCheckSettings - { - /// - /// Gets or sets a value for the healthcheck Id to disable. - /// - public Guid Id { get; set; } + public Guid Id { get; set; } - /// - /// Gets or sets a value for the date the healthcheck was disabled. - /// - public DateTime DisabledOn { get; set; } + /// + /// Gets or sets a value for the date the healthcheck was disabled. + /// + public DateTime DisabledOn { get; set; } - /// - /// Gets or sets a value for Id of the user that disabled the healthcheck. - /// - public int DisabledBy { get; set; } - } + /// + /// Gets or sets a value for Id of the user that disabled the healthcheck. + /// + public int DisabledBy { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs b/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs index 0d48453071..ebf99c03dd 100644 --- a/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ExceptionFilterSettings.cs @@ -3,20 +3,19 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for exception filter settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigExceptionFilter)] - public class ExceptionFilterSettings - { - internal const bool StaticDisabled = false; +namespace Umbraco.Cms.Core.Configuration.Models; - /// - /// Gets or sets a value indicating whether the exception filter is disabled. - /// - [DefaultValue(StaticDisabled)] - public bool Disabled { get; set; } = StaticDisabled; - } +/// +/// Typed configuration options for exception filter settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigExceptionFilter)] +public class ExceptionFilterSettings +{ + internal const bool StaticDisabled = false; + + /// + /// Gets or sets a value indicating whether the exception filter is disabled. + /// + [DefaultValue(StaticDisabled)] + public bool Disabled { get; set; } = StaticDisabled; } diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 8d00d58198..5351da317c 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -1,227 +1,232 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for global settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigGlobal)] +public class GlobalSettings { + internal const string + StaticReservedPaths = + "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/,"; // must end with a comma! + + internal const string StaticReservedUrls = "~/.well-known,"; // must end with a comma! + internal const string StaticTimeOut = "00:20:00"; + internal const string StaticDefaultUILanguage = "en-US"; + internal const bool StaticHideTopLevelNodeFromPath = true; + internal const bool StaticUseHttps = false; + internal const int StaticVersionCheckPeriod = 7; + internal const string StaticUmbracoPath = Constants.System.DefaultUmbracoPath; + internal const string StaticIconsPath = "umbraco/assets/icons"; + internal const string StaticUmbracoCssPath = "~/css"; + internal const string StaticUmbracoScriptsPath = "~/scripts"; + internal const string StaticUmbracoMediaPath = "~/media"; + internal const bool StaticInstallMissingDatabase = false; + internal const bool StaticDisableElectionForSingleServer = false; + internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; + internal const string StaticDistributedLockingReadLockDefaultTimeout = "00:01:00"; + internal const string StaticDistributedLockingWriteLockDefaultTimeout = "00:00:05"; + internal const bool StaticSanitizeTinyMce = false; + internal const int StaticMainDomReleaseSignalPollingInterval = 2000; + /// - /// Typed configuration options for global settings. + /// Gets or sets a value for the reserved URLs (must end with a comma). /// - [UmbracoOptions(Constants.Configuration.ConfigGlobal)] - public class GlobalSettings - { - internal const string StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/,"; // must end with a comma! - internal const string StaticReservedUrls = "~/.well-known,"; // must end with a comma! - internal const string StaticTimeOut = "00:20:00"; - internal const string StaticDefaultUILanguage = "en-US"; - internal const bool StaticHideTopLevelNodeFromPath = true; - internal const bool StaticUseHttps = false; - internal const int StaticVersionCheckPeriod = 7; - internal const string StaticUmbracoPath = Constants.System.DefaultUmbracoPath; - internal const string StaticIconsPath = "umbraco/assets/icons"; - internal const string StaticUmbracoCssPath = "~/css"; - internal const string StaticUmbracoScriptsPath = "~/scripts"; - internal const string StaticUmbracoMediaPath = "~/media"; - internal const bool StaticInstallMissingDatabase = false; - internal const bool StaticDisableElectionForSingleServer = false; - internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; - internal const string StaticDistributedLockingReadLockDefaultTimeout = "00:01:00"; - internal const string StaticDistributedLockingWriteLockDefaultTimeout = "00:00:05"; - internal const bool StaticSanitizeTinyMce = false; - internal const int StaticMainDomReleaseSignalPollingInterval = 2000; + [DefaultValue(StaticReservedUrls)] + public string ReservedUrls { get; set; } = StaticReservedUrls; - /// - /// Gets or sets a value for the reserved URLs (must end with a comma). - /// - [DefaultValue(StaticReservedUrls)] - public string ReservedUrls { get; set; } = StaticReservedUrls; + /// + /// Gets or sets a value for the reserved paths (must end with a comma). + /// + [DefaultValue(StaticReservedPaths)] + public string ReservedPaths { get; set; } = StaticReservedPaths; - /// - /// Gets or sets a value for the reserved paths (must end with a comma). - /// - [DefaultValue(StaticReservedPaths)] - public string ReservedPaths { get; set; } = StaticReservedPaths; + /// + /// Gets or sets a value for the back-office login timeout. + /// + [DefaultValue(StaticTimeOut)] + public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut); - /// - /// Gets or sets a value for the back-office login timeout. - /// - [DefaultValue(StaticTimeOut)] - public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut); + /// + /// Gets or sets a value for the default UI language. + /// + [DefaultValue(StaticDefaultUILanguage)] + public string DefaultUILanguage { get; set; } = StaticDefaultUILanguage; - /// - /// Gets or sets a value for the default UI language. - /// - [DefaultValue(StaticDefaultUILanguage)] - public string DefaultUILanguage { get; set; } = StaticDefaultUILanguage; + /// + /// Gets or sets a value indicating whether to hide the top level node from the path. + /// + [DefaultValue(StaticHideTopLevelNodeFromPath)] + public bool HideTopLevelNodeFromPath { get; set; } = StaticHideTopLevelNodeFromPath; - /// - /// Gets or sets a value indicating whether to hide the top level node from the path. - /// - [DefaultValue(StaticHideTopLevelNodeFromPath)] - public bool HideTopLevelNodeFromPath { get; set; } = StaticHideTopLevelNodeFromPath; + /// + /// Gets or sets a value indicating whether HTTPS should be used. + /// + [DefaultValue(StaticUseHttps)] + public bool UseHttps { get; set; } = StaticUseHttps; - /// - /// Gets or sets a value indicating whether HTTPS should be used. - /// - [DefaultValue(StaticUseHttps)] - public bool UseHttps { get; set; } = StaticUseHttps; + /// + /// Gets or sets a value for the version check period in days. + /// + [DefaultValue(StaticVersionCheckPeriod)] + public int VersionCheckPeriod { get; set; } = StaticVersionCheckPeriod; - /// - /// Gets or sets a value for the version check period in days. - /// - [DefaultValue(StaticVersionCheckPeriod)] - public int VersionCheckPeriod { get; set; } = StaticVersionCheckPeriod; + /// + /// Gets or sets a value for the Umbraco back-office path. + /// + [DefaultValue(StaticUmbracoPath)] + public string UmbracoPath { get; set; } = StaticUmbracoPath; - /// - /// Gets or sets a value for the Umbraco back-office path. - /// - [DefaultValue(StaticUmbracoPath)] - public string UmbracoPath { get; set; } = StaticUmbracoPath; + /// + /// Gets or sets a value for the Umbraco icons path. + /// + /// + /// TODO: Umbraco cannot be hard coded here that is what UmbracoPath is for + /// so this should not be a normal get set it has to have dynamic ability to return the correct + /// path given UmbracoPath if this hasn't been explicitly set. + /// + [DefaultValue(StaticIconsPath)] + public string IconsPath { get; set; } = StaticIconsPath; - /// - /// Gets or sets a value for the Umbraco icons path. - /// - /// - /// TODO: Umbraco cannot be hard coded here that is what UmbracoPath is for - /// so this should not be a normal get set it has to have dynamic ability to return the correct - /// path given UmbracoPath if this hasn't been explicitly set. - /// - [DefaultValue(StaticIconsPath)] - public string IconsPath { get; set; } = StaticIconsPath; + /// + /// Gets or sets a value for the Umbraco CSS path. + /// + [DefaultValue(StaticUmbracoCssPath)] + public string UmbracoCssPath { get; set; } = StaticUmbracoCssPath; - /// - /// Gets or sets a value for the Umbraco CSS path. - /// - [DefaultValue(StaticUmbracoCssPath)] - public string UmbracoCssPath { get; set; } = StaticUmbracoCssPath; + /// + /// Gets or sets a value for the Umbraco scripts path. + /// + [DefaultValue(StaticUmbracoScriptsPath)] + public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath; - /// - /// Gets or sets a value for the Umbraco scripts path. - /// - [DefaultValue(StaticUmbracoScriptsPath)] - public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath; + /// + /// Gets or sets a value for the Umbraco media request path. + /// + [DefaultValue(StaticUmbracoMediaPath)] + public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath; - /// - /// Gets or sets a value for the Umbraco media request path. - /// - [DefaultValue(StaticUmbracoMediaPath)] - public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath; + /// + /// Gets or sets a value for the physical Umbraco media root path (falls back to when + /// empty). + /// + /// + /// If the value is a virtual path, it's resolved relative to the webroot. + /// + public string UmbracoMediaPhysicalRootPath { get; set; } = null!; - /// - /// Gets or sets a value for the physical Umbraco media root path (falls back to when empty). - /// - /// - /// If the value is a virtual path, it's resolved relative to the webroot. - /// - public string UmbracoMediaPhysicalRootPath { get; set; } = null!; + /// + /// Gets or sets a value indicating whether to install the database when it is missing. + /// + [DefaultValue(StaticInstallMissingDatabase)] + public bool InstallMissingDatabase { get; set; } = StaticInstallMissingDatabase; - /// - /// Gets or sets a value indicating whether to install the database when it is missing. - /// - [DefaultValue(StaticInstallMissingDatabase)] - public bool InstallMissingDatabase { get; set; } = StaticInstallMissingDatabase; + /// + /// Gets or sets a value indicating whether to disable the election for a single server. + /// + [DefaultValue(StaticDisableElectionForSingleServer)] + public bool DisableElectionForSingleServer { get; set; } = StaticDisableElectionForSingleServer; - /// - /// Gets or sets a value indicating whether to disable the election for a single server. - /// - [DefaultValue(StaticDisableElectionForSingleServer)] - public bool DisableElectionForSingleServer { get; set; } = StaticDisableElectionForSingleServer; + /// + /// Gets or sets a value for the database factory server version. + /// + public string DatabaseFactoryServerVersion { get; set; } = string.Empty; - /// - /// Gets or sets a value for the database factory server version. - /// - public string DatabaseFactoryServerVersion { get; set; } = string.Empty; + /// + /// Gets or sets a value for the main dom lock. + /// + public string MainDomLock { get; set; } = string.Empty; - /// - /// Gets or sets a value for the main dom lock. - /// - public string MainDomLock { get; set; } = string.Empty; + /// + /// Gets or sets a value to discriminate MainDom boundaries. + /// + /// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero + /// downtime deployments. + /// + /// + public string MainDomKeyDiscriminator { get; set; } = string.Empty; - /// - /// Gets or sets a value to discriminate MainDom boundaries. - /// - /// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero downtime deployments. - /// - /// - public string MainDomKeyDiscriminator { get; set; } = string.Empty; + /// + /// Gets or sets the duration (in milliseconds) for which the MainDomLock release signal polling task should sleep. + /// + /// + /// Doesn't apply to MainDomSemaphoreLock. + /// + /// The default value is 2000ms. + /// + /// + [DefaultValue(StaticMainDomReleaseSignalPollingInterval)] + public int MainDomReleaseSignalPollingInterval { get; set; } = StaticMainDomReleaseSignalPollingInterval; - /// - /// Gets or sets the duration (in milliseconds) for which the MainDomLock release signal polling task should sleep. - /// - /// - /// Doesn't apply to MainDomSemaphoreLock. - /// - /// The default value is 2000ms. - /// - /// - [DefaultValue(StaticMainDomReleaseSignalPollingInterval)] - public int MainDomReleaseSignalPollingInterval { get; set; } = StaticMainDomReleaseSignalPollingInterval; + /// + /// Gets or sets the telemetry ID. + /// + public string Id { get; set; } = string.Empty; - /// - /// Gets or sets the telemetry ID. - /// - public string Id { get; set; } = string.Empty; + /// + /// Gets or sets a value for the path to the no content view. + /// + [DefaultValue(StaticNoNodesViewPath)] + public string NoNodesViewPath { get; set; } = StaticNoNodesViewPath; - /// - /// Gets or sets a value for the path to the no content view. - /// - [DefaultValue(StaticNoNodesViewPath)] - public string NoNodesViewPath { get; set; } = StaticNoNodesViewPath; + /// + /// Gets or sets a value for the database server registrar settings. + /// + public DatabaseServerRegistrarSettings DatabaseServerRegistrar { get; set; } = new(); - /// - /// Gets or sets a value for the database server registrar settings. - /// - public DatabaseServerRegistrarSettings DatabaseServerRegistrar { get; set; } = new DatabaseServerRegistrarSettings(); + /// + /// Gets or sets a value for the database server messenger settings. + /// + public DatabaseServerMessengerSettings DatabaseServerMessenger { get; set; } = new(); - /// - /// Gets or sets a value for the database server messenger settings. - /// - public DatabaseServerMessengerSettings DatabaseServerMessenger { get; set; } = new DatabaseServerMessengerSettings(); + /// + /// Gets or sets a value for the SMTP settings. + /// + public SmtpSettings? Smtp { get; set; } - /// - /// Gets or sets a value for the SMTP settings. - /// - public SmtpSettings? Smtp { get; set; } + /// + /// Gets a value indicating whether SMTP is configured. + /// + public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); - /// - /// Gets a value indicating whether SMTP is configured. - /// - public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); + /// + /// Gets a value indicating whether there is a physical pickup directory configured. + /// + public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); - /// - /// Gets a value indicating whether there is a physical pickup directory configured. - /// - public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); + /// + /// Gets or sets a value indicating whether TinyMCE scripting sanitization should be applied. + /// + [DefaultValue(StaticSanitizeTinyMce)] + public bool SanitizeTinyMce { get; set; } = StaticSanitizeTinyMce; - /// - /// Gets or sets a value indicating whether TinyMCE scripting sanitization should be applied. - /// - [DefaultValue(StaticSanitizeTinyMce)] - public bool SanitizeTinyMce { get; set; } = StaticSanitizeTinyMce; + /// + /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed read lock. + /// + /// + /// The default value is 60 seconds. + /// + [DefaultValue(StaticDistributedLockingReadLockDefaultTimeout)] + public TimeSpan DistributedLockingReadLockDefaultTimeout { get; set; } = + TimeSpan.Parse(StaticDistributedLockingReadLockDefaultTimeout); - /// - /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed read lock. - /// - /// - /// The default value is 60 seconds. - /// - [DefaultValue(StaticDistributedLockingReadLockDefaultTimeout)] - public TimeSpan DistributedLockingReadLockDefaultTimeout { get; set; } = TimeSpan.Parse(StaticDistributedLockingReadLockDefaultTimeout); + /// + /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed write lock. + /// + /// + /// The default value is 5 seconds. + /// + [DefaultValue(StaticDistributedLockingWriteLockDefaultTimeout)] + public TimeSpan DistributedLockingWriteLockDefaultTimeout { get; set; } = + TimeSpan.Parse(StaticDistributedLockingWriteLockDefaultTimeout); - /// - /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed write lock. - /// - /// - /// The default value is 5 seconds. - /// - [DefaultValue(StaticDistributedLockingWriteLockDefaultTimeout)] - public TimeSpan DistributedLockingWriteLockDefaultTimeout { get; set; } = TimeSpan.Parse(StaticDistributedLockingWriteLockDefaultTimeout); - - /// - /// Gets or sets a value representing the DistributedLockingMechanism to use. - /// - public string DistributedLockingMechanism { get; set; } = string.Empty; - } + /// + /// Gets or sets a value representing the DistributedLockingMechanism to use. + /// + public string DistributedLockingMechanism { get; set; } = string.Empty; } diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs index 2fc621a482..c973f59025 100644 --- a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs @@ -1,42 +1,41 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.HealthChecks; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for healthcheck notification method settings. +/// +public class HealthChecksNotificationMethodSettings { + internal const bool StaticEnabled = false; + internal const string StaticVerbosity = "Summary"; // Enum + internal const bool StaticFailureOnly = false; + /// - /// Typed configuration options for healthcheck notification method settings. + /// Gets or sets a value indicating whether the health check notification method is enabled. /// - public class HealthChecksNotificationMethodSettings - { - internal const bool StaticEnabled = false; - internal const string StaticVerbosity = "Summary"; // Enum - internal const bool StaticFailureOnly = false; + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; - /// - /// Gets or sets a value indicating whether the health check notification method is enabled. - /// - [DefaultValue(StaticEnabled)] - public bool Enabled { get; set; } = StaticEnabled; + /// + /// Gets or sets a value for the health check notifications reporting verbosity. + /// + [DefaultValue(StaticVerbosity)] + public HealthCheckNotificationVerbosity Verbosity { get; set; } = + Enum.Parse(StaticVerbosity); - /// - /// Gets or sets a value for the health check notifications reporting verbosity. - /// - [DefaultValue(StaticVerbosity)] - public HealthCheckNotificationVerbosity Verbosity { get; set; } = Enum.Parse(StaticVerbosity); + /// + /// Gets or sets a value indicating whether the health check notifications should occur on failures only. + /// + [DefaultValue(StaticFailureOnly)] + public bool FailureOnly { get; set; } = StaticFailureOnly; - /// - /// Gets or sets a value indicating whether the health check notifications should occur on failures only. - /// - [DefaultValue(StaticFailureOnly)] - public bool FailureOnly { get; set; } = StaticFailureOnly; - - /// - /// Gets or sets a value providing provider specific settings for the health check notification method. - /// - public IDictionary Settings { get; set; } = new Dictionary(); - } + /// + /// Gets or sets a value providing provider specific settings for the health check notification method. + /// + public IDictionary Settings { get; set; } = new Dictionary(); } diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs index 6e082da19f..6e81c48c7c 100644 --- a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs @@ -1,46 +1,44 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for healthcheck notification settings. +/// +public class HealthChecksNotificationSettings { + internal const bool StaticEnabled = false; + internal const string StaticPeriod = "1.00:00:00"; // TimeSpan.FromHours(24); + /// - /// Typed configuration options for healthcheck notification settings. + /// Gets or sets a value indicating whether health check notifications are enabled. /// - public class HealthChecksNotificationSettings - { - internal const bool StaticEnabled = false; - internal const string StaticPeriod = "1.00:00:00"; //TimeSpan.FromHours(24); + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; - /// - /// Gets or sets a value indicating whether health check notifications are enabled. - /// - [DefaultValue(StaticEnabled)] - public bool Enabled { get; set; } = StaticEnabled; + /// + /// Gets or sets a value for the first run time of a healthcheck notification in crontab format. + /// + public string FirstRunTime { get; set; } = string.Empty; - /// - /// Gets or sets a value for the first run time of a healthcheck notification in crontab format. - /// - public string FirstRunTime { get; set; } = string.Empty; + /// + /// Gets or sets a value for the period of the healthcheck notification. + /// + [DefaultValue(StaticPeriod)] + public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod); - /// - /// Gets or sets a value for the period of the healthcheck notification. - /// - [DefaultValue(StaticPeriod)] - public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod); + /// + /// Gets or sets a value for the collection of health check notification methods. + /// + public IDictionary NotificationMethods { get; set; } = + new Dictionary(); - /// - /// Gets or sets a value for the collection of health check notification methods. - /// - public IDictionary NotificationMethods { get; set; } = new Dictionary(); - - /// - /// Gets or sets a value for the collection of health checks that are disabled for notifications. - /// - public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); - } + /// + /// Gets or sets a value for the collection of health checks that are disabled for notifications. + /// + public IEnumerable DisabledChecks { get; set; } = + Enumerable.Empty(); } diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs index 0d232b9a9b..6ae79e9743 100644 --- a/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs @@ -1,25 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// Typed configuration options for healthchecks settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigHealthChecks)] +public class HealthChecksSettings { /// - /// Typed configuration options for healthchecks settings. + /// Gets or sets a value for the collection of healthchecks that are disabled. /// - [UmbracoOptions(Constants.Configuration.ConfigHealthChecks)] - public class HealthChecksSettings - { - /// - /// Gets or sets a value for the collection of healthchecks that are disabled. - /// - public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); + public IEnumerable DisabledChecks { get; set; } = + Enumerable.Empty(); - /// - /// Gets or sets a value for the healthcheck notification settings. - /// - public HealthChecksNotificationSettings Notification { get; set; } = new HealthChecksNotificationSettings(); - } + /// + /// Gets or sets a value for the healthcheck notification settings. + /// + public HealthChecksNotificationSettings Notification { get; set; } = new(); } diff --git a/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs b/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs index b608b5c155..01d028f883 100644 --- a/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigHelpPage)] +public class HelpPageSettings { - [UmbracoOptions(Constants.Configuration.ConfigHelpPage)] - public class HelpPageSettings - { - /// - /// Gets or sets the allowed addresses to retrieve data for the content dashboard. - /// - public string[]? HelpPageUrlAllowList { get; set; } - } + /// + /// Gets or sets the allowed addresses to retrieve data for the content dashboard. + /// + public string[]? HelpPageUrlAllowList { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs index 8f5f47a566..2329c73d66 100644 --- a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs @@ -3,38 +3,38 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for hosting settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigHosting)] +public class HostingSettings { + internal const string StaticLocalTempStorageLocation = "Default"; + internal const bool StaticDebug = false; + /// - /// Typed configuration options for hosting settings. + /// Gets or sets a value for the application virtual path. /// - [UmbracoOptions(Constants.Configuration.ConfigHosting)] - public class HostingSettings - { - internal const string StaticLocalTempStorageLocation = "Default"; - internal const bool StaticDebug = false; + public string? ApplicationVirtualPath { get; set; } - /// - /// Gets or sets a value for the application virtual path. - /// - public string? ApplicationVirtualPath { get; set; } + /// + /// Gets or sets a value for the location of temporary files. + /// + [DefaultValue(StaticLocalTempStorageLocation)] + public LocalTempStorage LocalTempStorageLocation { get; set; } = + Enum.Parse(StaticLocalTempStorageLocation); - /// - /// Gets or sets a value for the location of temporary files. - /// - [DefaultValue(StaticLocalTempStorageLocation)] - public LocalTempStorage LocalTempStorageLocation { get; set; } = Enum.Parse(StaticLocalTempStorageLocation); + /// + /// Gets or sets a value indicating whether umbraco is running in [debug mode]. + /// + /// true if [debug mode]; otherwise, false. + [DefaultValue(StaticDebug)] + public bool Debug { get; set; } = StaticDebug; - /// - /// Gets or sets a value indicating whether umbraco is running in [debug mode]. - /// - /// true if [debug mode]; otherwise, false. - [DefaultValue(StaticDebug)] - public bool Debug { get; set; } = StaticDebug; - - /// - /// Gets or sets a value specifying the name of the site. - /// - public string? SiteName { get; set; } - } + /// + /// Gets or sets a value specifying the name of the site. + /// + public string? SiteName { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs b/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs index 8a0a1658b2..3bcf91b0be 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingAutoFillUploadField.cs @@ -4,41 +4,40 @@ using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Configuration.Models.Validation; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for image autofill upload settings. +/// +public class ImagingAutoFillUploadField : ValidatableEntryBase { /// - /// Typed configuration options for image autofill upload settings. + /// Gets or sets a value for the alias of the image upload property. /// - public class ImagingAutoFillUploadField : ValidatableEntryBase - { - /// - /// Gets or sets a value for the alias of the image upload property. - /// - [Required] - public string Alias { get; set; } = null!; + [Required] + public string Alias { get; set; } = null!; - /// - /// Gets or sets a value for the width field alias of the image upload property. - /// - [Required] - public string WidthFieldAlias { get; set; } = null!; + /// + /// Gets or sets a value for the width field alias of the image upload property. + /// + [Required] + public string WidthFieldAlias { get; set; } = null!; - /// - /// Gets or sets a value for the height field alias of the image upload property. - /// - [Required] - public string HeightFieldAlias { get; set; } = null!; + /// + /// Gets or sets a value for the height field alias of the image upload property. + /// + [Required] + public string HeightFieldAlias { get; set; } = null!; - /// - /// Gets or sets a value for the length field alias of the image upload property. - /// - [Required] - public string LengthFieldAlias { get; set; } = null!; + /// + /// Gets or sets a value for the length field alias of the image upload property. + /// + [Required] + public string LengthFieldAlias { get; set; } = null!; - /// - /// Gets or sets a value for the extension field alias of the image upload property. - /// - [Required] - public string ExtensionFieldAlias { get; set; } = null!; - } + /// + /// Gets or sets a value for the extension field alias of the image upload property. + /// + [Required] + public string ExtensionFieldAlias { get; set; } = null!; } diff --git a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs index b3bdddc211..a433c5d300 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs @@ -1,51 +1,48 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -using System.IO; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for image cache settings. +/// +public class ImagingCacheSettings { + internal const string StaticBrowserMaxAge = "7.00:00:00"; + internal const string StaticCacheMaxAge = "365.00:00:00"; + internal const int StaticCacheHashLength = 12; + internal const int StaticCacheFolderDepth = 8; + internal const string StaticCacheFolder = Constants.SystemDirectories.TempData + "/MediaCache"; + /// - /// Typed configuration options for image cache settings. + /// Gets or sets a value for the browser image cache maximum age. /// - public class ImagingCacheSettings - { - internal const string StaticBrowserMaxAge = "7.00:00:00"; - internal const string StaticCacheMaxAge = "365.00:00:00"; - internal const int StaticCacheHashLength = 12; - internal const int StaticCacheFolderDepth = 8; - internal const string StaticCacheFolder = Constants.SystemDirectories.TempData + "/MediaCache"; + [DefaultValue(StaticBrowserMaxAge)] + public TimeSpan BrowserMaxAge { get; set; } = TimeSpan.Parse(StaticBrowserMaxAge); - /// - /// Gets or sets a value for the browser image cache maximum age. - /// - [DefaultValue(StaticBrowserMaxAge)] - public TimeSpan BrowserMaxAge { get; set; } = TimeSpan.Parse(StaticBrowserMaxAge); + /// + /// Gets or sets a value for the image cache maximum age. + /// + [DefaultValue(StaticCacheMaxAge)] + public TimeSpan CacheMaxAge { get; set; } = TimeSpan.Parse(StaticCacheMaxAge); - /// - /// Gets or sets a value for the image cache maximum age. - /// - [DefaultValue(StaticCacheMaxAge)] - public TimeSpan CacheMaxAge { get; set; } = TimeSpan.Parse(StaticCacheMaxAge); + /// + /// Gets or sets a value for the image cache hash length. + /// + [DefaultValue(StaticCacheHashLength)] + public uint CacheHashLength { get; set; } = StaticCacheHashLength; - /// - /// Gets or sets a value for the image cache hash length. - /// - [DefaultValue(StaticCacheHashLength)] - public uint CacheHashLength { get; set; } = StaticCacheHashLength; + /// + /// Gets or sets a value for the image cache folder depth. + /// + [DefaultValue(StaticCacheFolderDepth)] + public uint CacheFolderDepth { get; set; } = StaticCacheFolderDepth; - /// - /// Gets or sets a value for the image cache folder depth. - /// - [DefaultValue(StaticCacheFolderDepth)] - public uint CacheFolderDepth { get; set; } = StaticCacheFolderDepth; - - /// - /// Gets or sets a value for the image cache folder. - /// - [DefaultValue(StaticCacheFolder)] - public string CacheFolder { get; set; } = StaticCacheFolder; - } + /// + /// Gets or sets a value for the image cache folder. + /// + [DefaultValue(StaticCacheFolder)] + public string CacheFolder { get; set; } = StaticCacheFolder; } diff --git a/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs index ff02fdc522..dc4585bf9c 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingResizeSettings.cs @@ -3,26 +3,25 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for image resize settings. +/// +public class ImagingResizeSettings { + internal const int StaticMaxWidth = 5000; + internal const int StaticMaxHeight = 5000; + /// - /// Typed configuration options for image resize settings. + /// Gets or sets a value for the maximim resize width. /// - public class ImagingResizeSettings - { - internal const int StaticMaxWidth = 5000; - internal const int StaticMaxHeight = 5000; + [DefaultValue(StaticMaxWidth)] + public int MaxWidth { get; set; } = StaticMaxWidth; - /// - /// Gets or sets a value for the maximim resize width. - /// - [DefaultValue(StaticMaxWidth)] - public int MaxWidth { get; set; } = StaticMaxWidth; - - /// - /// Gets or sets a value for the maximim resize height. - /// - [DefaultValue(StaticMaxHeight)] - public int MaxHeight { get; set; } = StaticMaxHeight; - } + /// + /// Gets or sets a value for the maximim resize height. + /// + [DefaultValue(StaticMaxHeight)] + public int MaxHeight { get; set; } = StaticMaxHeight; } diff --git a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs index fde303343c..8232746ead 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs @@ -1,22 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for imaging settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigImaging)] +public class ImagingSettings { /// - /// Typed configuration options for imaging settings. + /// Gets or sets a value for imaging cache settings. /// - [UmbracoOptions(Constants.Configuration.ConfigImaging)] - public class ImagingSettings - { - /// - /// Gets or sets a value for imaging cache settings. - /// - public ImagingCacheSettings Cache { get; set; } = new ImagingCacheSettings(); + public ImagingCacheSettings Cache { get; set; } = new(); - /// - /// Gets or sets a value for imaging resize settings. - /// - public ImagingResizeSettings Resize { get; set; } = new ImagingResizeSettings(); - } + /// + /// Gets or sets a value for imaging resize settings. + /// + public ImagingResizeSettings Resize { get; set; } = new(); } diff --git a/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs b/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs index c140463b4a..8c18495d55 100644 --- a/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/IndexCreatorSettings.cs @@ -1,20 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// Typed configuration options for index creator settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigExamine)] +public class IndexCreatorSettings { /// - /// Typed configuration options for index creator settings. + /// Gets or sets a value for lucene directory factory type. /// - [UmbracoOptions(Constants.Configuration.ConfigExamine)] - public class IndexCreatorSettings - { - /// - /// Gets or sets a value for lucene directory factory type. - /// - public LuceneDirectoryFactory LuceneDirectoryFactory { get; set; } - - } + public LuceneDirectoryFactory LuceneDirectoryFactory { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/InstallDefaultDataSettings.cs b/src/Umbraco.Core/Configuration/Models/InstallDefaultDataSettings.cs index 377e893bbf..25789b397b 100644 --- a/src/Umbraco.Core/Configuration/Models/InstallDefaultDataSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/InstallDefaultDataSettings.cs @@ -1,73 +1,74 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// An enumeration of options available for control over installation of default Umbraco data. +/// +public enum InstallDefaultDataOption { /// - /// An enumeration of options available for control over installation of default Umbraco data. + /// Do not install any items of this type (other than Umbraco defined essential ones). /// - public enum InstallDefaultDataOption - { - /// - /// Do not install any items of this type (other than Umbraco defined essential ones). - /// - None, - - /// - /// Only install the default data specified in the - /// - Values, - - /// - /// Install all default data, except that specified in the - /// - ExceptValues, - - /// - /// Install all default data. - /// - All - } + None, /// - /// Typed configuration options for installation of default data. + /// Only install the default data specified in the /// - public class InstallDefaultDataSettings - { - /// - /// Gets or sets a value indicating whether to create default data on installation. - /// - public InstallDefaultDataOption InstallData { get; set; } = InstallDefaultDataOption.All; + Values, - /// - /// Gets or sets a value indicating which default data (languages, data types, etc.) should be created when is - /// set to or . - /// - /// - /// - /// For languages, the values provided should be the ISO codes for the languages to be included or excluded, e.g. "en-US". - /// If removing the single default language, ensure that a different one is created via some other means (such - /// as a restore from Umbraco Deploy schema data). - /// - /// - /// For data types, the values provided should be the Guid values used by Umbraco for the data type, listed at: - /// - /// Some data types - such as the string label - cannot be excluded from install as they are required for core Umbraco - /// functionality. - /// Otherwise take care not to remove data types required for default Umbraco media and member types, unless you also - /// choose to exclude them. - /// - /// - /// For media types, the values provided should be the Guid values used by Umbraco for the media type, listed at: - /// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs. - /// - /// - /// For member types, the values provided should be the Guid values used by Umbraco for the member type, listed at: - /// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs. - /// - /// - public IList Values { get; set; } = new List(); - } + /// + /// Install all default data, except that specified in the + /// + ExceptValues, + + /// + /// Install all default data. + /// + All, +} + +/// +/// Typed configuration options for installation of default data. +/// +public class InstallDefaultDataSettings +{ + /// + /// Gets or sets a value indicating whether to create default data on installation. + /// + public InstallDefaultDataOption InstallData { get; set; } = InstallDefaultDataOption.All; + + /// + /// Gets or sets a value indicating which default data (languages, data types, etc.) should be created when + /// is + /// set to or . + /// + /// + /// + /// For languages, the values provided should be the ISO codes for the languages to be included or excluded, e.g. + /// "en-US". + /// If removing the single default language, ensure that a different one is created via some other means (such + /// as a restore from Umbraco Deploy schema data). + /// + /// + /// For data types, the values provided should be the Guid values used by Umbraco for the data type, listed at: + /// + /// Some data types - such as the string label - cannot be excluded from install as they are required for core + /// Umbraco + /// functionality. + /// Otherwise take care not to remove data types required for default Umbraco media and member types, unless you + /// also + /// choose to exclude them. + /// + /// + /// For media types, the values provided should be the Guid values used by Umbraco for the media type, listed at: + /// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs. + /// + /// + /// For member types, the values provided should be the Guid values used by Umbraco for the member type, listed at: + /// https://github.com/umbraco/Umbraco-CMS/blob/v9/dev/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs. + /// + /// + public IList Values { get; set; } = new List(); } diff --git a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs index 297e1dff87..64cd61ad26 100644 --- a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs @@ -3,27 +3,26 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for keep alive settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigKeepAlive)] +public class KeepAliveSettings { + internal const bool StaticDisableKeepAliveTask = false; + internal const string StaticKeepAlivePingUrl = "~/api/keepalive/ping"; + /// - /// Typed configuration options for keep alive settings. + /// Gets or sets a value indicating whether the keep alive task is disabled. /// - [UmbracoOptions(Constants.Configuration.ConfigKeepAlive)] - public class KeepAliveSettings - { - internal const bool StaticDisableKeepAliveTask = false; - internal const string StaticKeepAlivePingUrl = "~/api/keepalive/ping"; + [DefaultValue(StaticDisableKeepAliveTask)] + public bool DisableKeepAliveTask { get; set; } = StaticDisableKeepAliveTask; - /// - /// Gets or sets a value indicating whether the keep alive task is disabled. - /// - [DefaultValue(StaticDisableKeepAliveTask)] - public bool DisableKeepAliveTask { get; set; } = StaticDisableKeepAliveTask; - - /// - /// Gets or sets a value for the keep alive ping URL. - /// - [DefaultValue(StaticKeepAlivePingUrl)] - public string KeepAlivePingUrl { get; set; } = StaticKeepAlivePingUrl; - } + /// + /// Gets or sets a value for the keep alive ping URL. + /// + [DefaultValue(StaticKeepAlivePingUrl)] + public string KeepAlivePingUrl { get; set; } = StaticKeepAlivePingUrl; } diff --git a/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs b/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs index c3909ed619..b44d70a46a 100644 --- a/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/LegacyPasswordMigrationSettings.cs @@ -3,28 +3,27 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for legacy machine key settings used for migration of members from a v8 solution. +/// +[UmbracoOptions(Constants.Configuration.ConfigLegacyPasswordMigration)] +public class LegacyPasswordMigrationSettings { + private const string StaticDecryptionKey = ""; + /// - /// Typed configuration options for legacy machine key settings used for migration of members from a v8 solution. + /// Gets the decryption algorithm. /// - [UmbracoOptions(Constants.Configuration.ConfigLegacyPasswordMigration)] - public class LegacyPasswordMigrationSettings - { - private const string StaticDecryptionKey = ""; + /// + /// Currently only AES is supported. This should include all machine keys generated by Umbraco. + /// + public string MachineKeyDecryption => "AES"; - /// - /// Gets the decryption algorithm. - /// - /// - /// Currently only AES is supported. This should include all machine keys generated by Umbraco. - /// - public string MachineKeyDecryption => "AES"; - - /// - /// Gets or sets the decryption hex-formatted string key found in legacy web.config machineKey configuration-element. - /// - [DefaultValue(StaticDecryptionKey)] - public string MachineKeyDecryptionKey { get; set; } = StaticDecryptionKey; - } + /// + /// Gets or sets the decryption hex-formatted string key found in legacy web.config machineKey configuration-element. + /// + [DefaultValue(StaticDecryptionKey)] + public string MachineKeyDecryptionKey { get; set; } = StaticDecryptionKey; } diff --git a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs index 2075921c3f..37b671926c 100644 --- a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs @@ -1,23 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for logging settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigLogging)] - public class LoggingSettings - { - internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24); +namespace Umbraco.Cms.Core.Configuration.Models; - /// - /// Gets or sets a value for the maximum age of a log file. - /// - [DefaultValue(StaticMaxLogAge)] - public TimeSpan MaxLogAge { get; set; } = TimeSpan.Parse(StaticMaxLogAge); - } +/// +/// Typed configuration options for logging settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigLogging)] +public class LoggingSettings +{ + internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24); + + /// + /// Gets or sets a value for the maximum age of a log file. + /// + [DefaultValue(StaticMaxLogAge)] + public TimeSpan MaxLogAge { get; set; } = TimeSpan.Parse(StaticMaxLogAge); } diff --git a/src/Umbraco.Core/Configuration/Models/LuceneDirectoryFactory.cs b/src/Umbraco.Core/Configuration/Models/LuceneDirectoryFactory.cs index 5f06a850f1..3b0e974c08 100644 --- a/src/Umbraco.Core/Configuration/Models/LuceneDirectoryFactory.cs +++ b/src/Umbraco.Core/Configuration/Models/LuceneDirectoryFactory.cs @@ -1,24 +1,23 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +public enum LuceneDirectoryFactory { - public enum LuceneDirectoryFactory - { - /// - /// The index will operate from the default location: Umbraco/Data/Temp/ExamineIndexes - /// - Default, + /// + /// The index will operate from the default location: Umbraco/Data/Temp/ExamineIndexes + /// + Default, - /// - /// The index will operate on a local index created in the processes %temp% location and - /// will replicate back to main storage in Umbraco/Data/Temp/ExamineIndexes - /// - SyncedTempFileSystemDirectoryFactory, + /// + /// The index will operate on a local index created in the processes %temp% location and + /// will replicate back to main storage in Umbraco/Data/Temp/ExamineIndexes + /// + SyncedTempFileSystemDirectoryFactory, - /// - /// The index will operate only in the processes %temp% directory location - /// - TempFileSystemDirectoryFactory - } + /// + /// The index will operate only in the processes %temp% directory location + /// + TempFileSystemDirectoryFactory, } diff --git a/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs b/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs index fa4f0725f7..1e884a150f 100644 --- a/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/MemberPasswordConfigurationSettings.cs @@ -3,47 +3,46 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for member password settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigMemberPassword)] +public class MemberPasswordConfigurationSettings : IPasswordConfiguration { - /// - /// Typed configuration options for member password settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigMemberPassword)] - public class MemberPasswordConfigurationSettings : IPasswordConfiguration - { - internal const int StaticRequiredLength = 10; - internal const bool StaticRequireNonLetterOrDigit = false; - internal const bool StaticRequireDigit = false; - internal const bool StaticRequireLowercase = false; - internal const bool StaticRequireUppercase = false; - internal const int StaticMaxFailedAccessAttemptsBeforeLockout = 5; + internal const int StaticRequiredLength = 10; + internal const bool StaticRequireNonLetterOrDigit = false; + internal const bool StaticRequireDigit = false; + internal const bool StaticRequireLowercase = false; + internal const bool StaticRequireUppercase = false; + internal const int StaticMaxFailedAccessAttemptsBeforeLockout = 5; - /// - [DefaultValue(StaticRequiredLength)] - public int RequiredLength { get; set; } = StaticRequiredLength; + /// + [DefaultValue(StaticRequiredLength)] + public int RequiredLength { get; set; } = StaticRequiredLength; - /// - [DefaultValue(StaticRequireNonLetterOrDigit)] - public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; + /// + [DefaultValue(StaticRequireNonLetterOrDigit)] + public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; - /// - [DefaultValue(StaticRequireDigit)] - public bool RequireDigit { get; set; } = StaticRequireDigit; + /// + [DefaultValue(StaticRequireDigit)] + public bool RequireDigit { get; set; } = StaticRequireDigit; - /// - [DefaultValue(StaticRequireLowercase)] - public bool RequireLowercase { get; set; } = StaticRequireLowercase; + /// + [DefaultValue(StaticRequireLowercase)] + public bool RequireLowercase { get; set; } = StaticRequireLowercase; - /// - [DefaultValue(StaticRequireUppercase)] - public bool RequireUppercase { get; set; } = StaticRequireUppercase; + /// + [DefaultValue(StaticRequireUppercase)] + public bool RequireUppercase { get; set; } = StaticRequireUppercase; - /// - [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] - public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; + /// + [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] + public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; - /// - [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] - public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; - } + /// + [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] + public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; } diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs index 73d046de32..fdb7bac0ef 100644 --- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs @@ -2,83 +2,80 @@ // See LICENSE for more details. using System.ComponentModel; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for models builder settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigModelsBuilder, BindNonPublicProperties = true)] +public class ModelsBuilderSettings { + internal const string StaticModelsMode = "InMemoryAuto"; + internal const string StaticModelsDirectory = "~/umbraco/models"; + internal const bool StaticAcceptUnsafeModelsDirectory = false; + internal const int StaticDebugLevel = 0; + private bool _flagOutOfDateModels = true; + /// - /// Typed configuration options for models builder settings. + /// Gets or sets a value for the models mode. /// - [UmbracoOptions(Constants.Configuration.ConfigModelsBuilder, BindNonPublicProperties = true)] - public class ModelsBuilderSettings + [DefaultValue(StaticModelsMode)] + public ModelsMode ModelsMode { get; set; } = Enum.Parse(StaticModelsMode); + + /// + /// Gets or sets a value for models namespace. + /// + /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. + [DefaultValue(Constants.ModelsBuilder.DefaultModelsNamespace)] + public string ModelsNamespace { get; set; } = Constants.ModelsBuilder.DefaultModelsNamespace; + + /// + /// Gets or sets a value indicating whether we should flag out-of-date models. + /// + /// + /// Models become out-of-date when data types or content types are updated. When this + /// setting is activated the ~/umbraco/models/PureLive/ood.txt file is then created. When models are + /// generated through the dashboard, the files is cleared. Default value is false. + /// + public bool FlagOutOfDateModels { - private bool _flagOutOfDateModels = true; - internal const string StaticModelsMode = "InMemoryAuto"; - internal const string StaticModelsDirectory = "~/umbraco/models"; - internal const bool StaticAcceptUnsafeModelsDirectory = false; - internal const int StaticDebugLevel = 0; + get => _flagOutOfDateModels; - /// - /// Gets or sets a value for the models mode. - /// - [DefaultValue(StaticModelsMode)] - public ModelsMode ModelsMode { get; set; } = Enum.Parse(StaticModelsMode); - - /// - /// Gets or sets a value for models namespace. - /// - /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. - [DefaultValue(Constants.ModelsBuilder.DefaultModelsNamespace)] - public string ModelsNamespace { get; set; } = Constants.ModelsBuilder.DefaultModelsNamespace; - - /// - /// Gets or sets a value indicating whether we should flag out-of-date models. - /// - /// - /// Models become out-of-date when data types or content types are updated. When this - /// setting is activated the ~/umbraco/models/PureLive/ood.txt file is then created. When models are - /// generated through the dashboard, the files is cleared. Default value is false. - /// - public bool FlagOutOfDateModels + set { - get => _flagOutOfDateModels; - - set + if (!ModelsMode.IsAuto()) { - if (!ModelsMode.IsAuto()) - { - _flagOutOfDateModels = false; - return; - } - - _flagOutOfDateModels = value; + _flagOutOfDateModels = false; + return; } + + _flagOutOfDateModels = value; } - - /// - /// Gets or sets a value for the models directory. - /// - /// Default is ~/umbraco/models but that can be changed. - [DefaultValue(StaticModelsDirectory)] - public string ModelsDirectory { get; set; } = StaticModelsDirectory; - - - /// - /// Gets or sets a value indicating whether to accept an unsafe value for ModelsDirectory. - /// - /// - /// An unsafe value is an absolute path, or a relative path pointing outside - /// of the website root. - /// - [DefaultValue(StaticAcceptUnsafeModelsDirectory)] - public bool AcceptUnsafeModelsDirectory { get; set; } = StaticAcceptUnsafeModelsDirectory; - - /// - /// Gets or sets a value indicating the debug log level. - /// - /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe). - [DefaultValue(StaticDebugLevel)] - public int DebugLevel { get; set; } = StaticDebugLevel; } + + /// + /// Gets or sets a value for the models directory. + /// + /// Default is ~/umbraco/models but that can be changed. + [DefaultValue(StaticModelsDirectory)] + public string ModelsDirectory { get; set; } = StaticModelsDirectory; + + /// + /// Gets or sets a value indicating whether to accept an unsafe value for ModelsDirectory. + /// + /// + /// An unsafe value is an absolute path, or a relative path pointing outside + /// of the website root. + /// + [DefaultValue(StaticAcceptUnsafeModelsDirectory)] + public bool AcceptUnsafeModelsDirectory { get; set; } = StaticAcceptUnsafeModelsDirectory; + + /// + /// Gets or sets a value indicating the debug log level. + /// + /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe). + [DefaultValue(StaticDebugLevel)] + public int DebugLevel { get; set; } = StaticDebugLevel; } diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs index 8f889b10c3..0506ddb98b 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs @@ -1,14 +1,13 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// The serializer type that nucache uses to persist documents in the database. +/// +public enum NuCacheSerializerType { - /// - /// The serializer type that nucache uses to persist documents in the database. - /// - public enum NuCacheSerializerType - { - MessagePack = 1, // Default - JSON = 2 - } + MessagePack = 1, // Default + JSON = 2, } diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs index ee41fc32d3..b88dbb5d0d 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs @@ -3,41 +3,41 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for NuCache settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigNuCache)] +public class NuCacheSettings { + internal const string StaticNuCacheSerializerType = "MessagePack"; + internal const int StaticSqlPageSize = 1000; + internal const int StaticKitBatchSize = 1; + /// - /// Typed configuration options for NuCache settings. + /// Gets or sets a value defining the BTree block size. /// - [UmbracoOptions(Constants.Configuration.ConfigNuCache)] - public class NuCacheSettings - { - internal const string StaticNuCacheSerializerType = "MessagePack"; - internal const int StaticSqlPageSize = 1000; - internal const int StaticKitBatchSize = 1; + public int? BTreeBlockSize { get; set; } - /// - /// Gets or sets a value defining the BTree block size. - /// - public int? BTreeBlockSize { get; set; } + /// + /// The serializer type that nucache uses to persist documents in the database. + /// + [DefaultValue(StaticNuCacheSerializerType)] + public NuCacheSerializerType NuCacheSerializerType { get; set; } = + Enum.Parse(StaticNuCacheSerializerType); - /// - /// The serializer type that nucache uses to persist documents in the database. - /// - [DefaultValue(StaticNuCacheSerializerType)] - public NuCacheSerializerType NuCacheSerializerType { get; set; } = Enum.Parse(StaticNuCacheSerializerType); + /// + /// The paging size to use for nucache SQL queries. + /// + [DefaultValue(StaticSqlPageSize)] + public int SqlPageSize { get; set; } = StaticSqlPageSize; - /// - /// The paging size to use for nucache SQL queries. - /// - [DefaultValue(StaticSqlPageSize)] - public int SqlPageSize { get; set; } = StaticSqlPageSize; + /// + /// The size to use for nucache Kit batches. Higher value means more content loaded into memory at a time. + /// + [DefaultValue(StaticKitBatchSize)] + public int KitBatchSize { get; set; } = StaticKitBatchSize; - /// - /// The size to use for nucache Kit batches. Higher value means more content loaded into memory at a time. - /// - [DefaultValue(StaticKitBatchSize)] - public int KitBatchSize { get; set; } = StaticKitBatchSize; - - public bool UnPublishedContentCompression { get; set; } = false; - } + public bool UnPublishedContentCompression { get; set; } = false; } diff --git a/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs b/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs index 27968fdcd2..ee48d5a642 100644 --- a/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs @@ -3,38 +3,41 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for package migration settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigPackageMigration)] +public class PackageMigrationSettings { + private const bool StaticRunSchemaAndContentMigrations = true; + private const bool StaticAllowComponentOverrideOfRunSchemaAndContentMigrations = true; + /// - /// Typed configuration options for package migration settings. + /// Gets or sets a value indicating whether package migration steps that install schema and content should run. /// - [UmbracoOptions(Constants.Configuration.ConfigPackageMigration)] - public class PackageMigrationSettings - { - private const bool StaticRunSchemaAndContentMigrations = true; - private const bool StaticAllowComponentOverrideOfRunSchemaAndContentMigrations = true; + /// + /// By default this is true and schema and content defined in a package migration are installed. + /// Using configuration, administrators can optionally switch this off in certain environments. + /// Deployment tools such as Umbraco Deploy can also configure this option to run or not run these migration + /// steps as is appropriate for normal use of the tool. + /// + [DefaultValue(StaticRunSchemaAndContentMigrations)] + public bool RunSchemaAndContentMigrations { get; set; } = StaticRunSchemaAndContentMigrations; - /// - /// Gets or sets a value indicating whether package migration steps that install schema and content should run. - /// - /// - /// By default this is true and schema and content defined in a package migration are installed. - /// Using configuration, administrators can optionally switch this off in certain environments. - /// Deployment tools such as Umbraco Deploy can also configure this option to run or not run these migration - /// steps as is appropriate for normal use of the tool. - /// - [DefaultValue(StaticRunSchemaAndContentMigrations)] - public bool RunSchemaAndContentMigrations { get; set; } = StaticRunSchemaAndContentMigrations; - - /// - /// Gets or sets a value indicating whether components can override the configured value for . - /// - /// - /// By default this is true and components can override the configured setting for . - /// If an administrator wants explicit control over which environments migration steps installing schema and content can run, - /// they can set this to false. Components should respect this and not override the configuration. - /// - [DefaultValue(StaticAllowComponentOverrideOfRunSchemaAndContentMigrations)] - public bool AllowComponentOverrideOfRunSchemaAndContentMigrations { get; set; } = StaticAllowComponentOverrideOfRunSchemaAndContentMigrations; - } + /// + /// Gets or sets a value indicating whether components can override the configured value for + /// . + /// + /// + /// By default this is true and components can override the configured setting for + /// . + /// If an administrator wants explicit control over which environments migration steps installing schema and content + /// can run, + /// they can set this to false. Components should respect this and not override the configuration. + /// + [DefaultValue(StaticAllowComponentOverrideOfRunSchemaAndContentMigrations)] + public bool AllowComponentOverrideOfRunSchemaAndContentMigrations { get; set; } = + StaticAllowComponentOverrideOfRunSchemaAndContentMigrations; } diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs index 45a9bc98ed..0c5d39f47a 100644 --- a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs @@ -1,27 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Configuration.UmbracoSettings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for request handler settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigRequestHandler)] - public class RequestHandlerSettings - { - internal const bool StaticAddTrailingSlash = true; - internal const string StaticConvertUrlsToAscii = "try"; - internal const bool StaticEnableDefaultCharReplacements = true; +namespace Umbraco.Cms.Core.Configuration.Models; - internal static readonly Umbraco.Cms.Core.Configuration.Models.CharItem[] DefaultCharCollection = - { +/// +/// Typed configuration options for request handler settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigRequestHandler)] +public class RequestHandlerSettings +{ + internal const bool StaticAddTrailingSlash = true; + internal const string StaticConvertUrlsToAscii = "try"; + internal const bool StaticEnableDefaultCharReplacements = true; + + internal static readonly CharItem[] DefaultCharCollection = + { new () { Char = " ", Replacement = "-" }, new () { Char = "\"", Replacement = string.Empty }, new () { Char = "'", Replacement = string.Empty }, @@ -45,46 +42,46 @@ namespace Umbraco.Cms.Core.Configuration.Models new () { Char = "ß", Replacement = "ss" }, new () { Char = "|", Replacement = "-" }, new () { Char = "<", Replacement = string.Empty }, - new () { Char = ">", Replacement = string.Empty } - }; + new () { Char = ">", Replacement = string.Empty }, + }; - /// - /// Gets or sets a value indicating whether to add a trailing slash to URLs. - /// - [DefaultValue(StaticAddTrailingSlash)] - public bool AddTrailingSlash { get; set; } = StaticAddTrailingSlash; + /// + /// Gets or sets a value indicating whether to add a trailing slash to URLs. + /// + [DefaultValue(StaticAddTrailingSlash)] + public bool AddTrailingSlash { get; set; } = StaticAddTrailingSlash; - /// - /// Gets or sets a value indicating whether to convert URLs to ASCII (valid values: "true", "try" or "false"). - /// - [DefaultValue(StaticConvertUrlsToAscii)] - public string ConvertUrlsToAscii { get; set; } = StaticConvertUrlsToAscii; + /// + /// Gets or sets a value indicating whether to convert URLs to ASCII (valid values: "true", "try" or "false"). + /// + [DefaultValue(StaticConvertUrlsToAscii)] + public string ConvertUrlsToAscii { get; set; } = StaticConvertUrlsToAscii; - /// - /// Gets a value indicating whether URLs should be converted to ASCII. - /// - public bool ShouldConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("true"); + /// + /// Gets a value indicating whether URLs should be converted to ASCII. + /// + public bool ShouldConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("true"); - /// - /// Gets a value indicating whether URLs should be tried to be converted to ASCII. - /// - public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try"); + /// + /// Gets a value indicating whether URLs should be tried to be converted to ASCII. + /// + public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try"); - /// - /// Disable all default character replacements - /// - [DefaultValue(StaticEnableDefaultCharReplacements)] - public bool EnableDefaultCharReplacements { get; set; } = StaticEnableDefaultCharReplacements; + /// + /// Disable all default character replacements + /// + [DefaultValue(StaticEnableDefaultCharReplacements)] + public bool EnableDefaultCharReplacements { get; set; } = StaticEnableDefaultCharReplacements; - /// - /// Add additional character replacements, or override defaults - /// - [Obsolete("Use the GetCharReplacements extension method in the Umbraco.Extensions namespace instead. Scheduled for removal in V11")] - public IEnumerable CharCollection { get; set; } = DefaultCharCollection; + /// + /// Add additional character replacements, or override defaults + /// + [Obsolete( + "Use the GetCharReplacements extension method in the Umbraco.Extensions namespace instead. Scheduled for removal in V11")] + public IEnumerable CharCollection { get; set; } = DefaultCharCollection; - /// - /// Add additional character replacements, or override defaults - /// - public IEnumerable? UserDefinedCharCollection { get; set; } - } + /// + /// Add additional character replacements, or override defaults + /// + public IEnumerable? UserDefinedCharCollection { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs index cd82376c57..55fa7b2c5f 100644 --- a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs @@ -1,111 +1,157 @@ -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigRichTextEditor)] +public class RichTextEditorSettings { - [UmbracoOptions(Constants.Configuration.ConfigRichTextEditor)] - public class RichTextEditorSettings + internal const string StaticValidElements = + "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption"; + + internal const string StaticInvalidElements = "font"; + + private static readonly string[] Default_plugins = { - internal const string StaticValidElements = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption"; - internal const string StaticInvalidElements = "font"; + "paste", "anchor", "charmap", "table", "lists", "advlist", "hr", "autolink", "directionality", "tabfocus", + "searchreplace", + }; - private static readonly string[] s_default_plugins = new[] + private static readonly RichTextEditorCommand[] Default_commands = + { + new RichTextEditorCommand { - "paste", - "anchor", - "charmap", - "table", - "lists", - "advlist", - "hr", - "autolink", - "directionality", - "tabfocus", - "searchreplace" - }; - private static readonly RichTextEditorCommand[] s_default_commands = new [] + Alias = "ace", Name = "Source code editor", Mode = RichTextEditorCommandMode.Insert, + }, + new RichTextEditorCommand { - new RichTextEditorCommand(){Alias = "ace" , Name = "Source code editor" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "removeformat" , Name = "Remove format" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "undo" , Name = "Undo" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "redo" , Name = "Redo" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "cut" , Name = "Cut" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "copy" , Name = "Copy" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "paste" , Name = "Paste" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "styleselect" , Name = "Style select" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "bold" , Name = "Bold" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "italic" , Name = "Italic" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "underline" , Name = "Underline" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "strikethrough" , Name = "Strikethrough" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "alignleft" , Name = "Justify left" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "aligncenter" , Name = "Justify center" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "alignright" , Name = "Justify right" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "alignjustify" , Name = "Justify full" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "bullist" , Name = "Bullet list" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "numlist" , Name = "Numbered list" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "outdent" , Name = "Decrease indent" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "indent" , Name = "Increase indent" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "link" , Name = "Insert/edit link" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "unlink" , Name = "Remove link" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "anchor" , Name = "Anchor" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "umbmediapicker" , Name = "Image" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "umbmacro" , Name = "Macro" , Mode = RichTextEditorCommandMode.All}, - new RichTextEditorCommand(){Alias = "table" , Name = "Table" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "umbembeddialog" , Name = "Embed" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "hr" , Name = "Horizontal rule" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "subscript" , Name = "Subscript" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "superscript" , Name = "Superscript" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "charmap" , Name = "Character map" , Mode = RichTextEditorCommandMode.Insert}, - new RichTextEditorCommand(){Alias = "rtl" , Name = "Right to left" , Mode = RichTextEditorCommandMode.Selection}, - new RichTextEditorCommand(){Alias = "ltr" , Name = "Left to right" , Mode = RichTextEditorCommandMode.Selection}, - }; - - private static readonly IDictionary s_default_custom_config = new Dictionary() + Alias = "removeformat", Name = "Remove format", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand { Alias = "undo", Name = "Undo", Mode = RichTextEditorCommandMode.Insert }, + new RichTextEditorCommand { Alias = "redo", Name = "Redo", Mode = RichTextEditorCommandMode.Insert }, + new RichTextEditorCommand { Alias = "cut", Name = "Cut", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand { Alias = "copy", Name = "Copy", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand { Alias = "paste", Name = "Paste", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand { - ["entity_encoding"] = "raw" - }; - - /// - /// HTML RichText Editor TinyMCE Commands - /// - /// WB-TODO Custom Array of objects - public RichTextEditorCommand[] Commands { get; set; } = s_default_commands; - - /// - /// HTML RichText Editor TinyMCE Plugins - /// - public string[] Plugins { get; set; } = s_default_plugins; - - /// - /// HTML RichText Editor TinyMCE Custom Config - /// - /// WB-TODO Custom Dictionary - public IDictionary CustomConfig { get; set; } = s_default_custom_config; - - /// - /// - /// - [DefaultValue(StaticValidElements)] - public string ValidElements { get; set; } = StaticValidElements; - - /// - /// Invalid HTML elements for RichText Editor - /// - [DefaultValue(StaticInvalidElements)] - public string InvalidElements { get; set; } = StaticInvalidElements; - - public class RichTextEditorCommand + Alias = "styleselect", Name = "Style select", Mode = RichTextEditorCommandMode.All, + }, + new RichTextEditorCommand { Alias = "bold", Name = "Bold", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand { Alias = "italic", Name = "Italic", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand { - [Required] - public string Alias { get; set; } = null!; + Alias = "underline", Name = "Underline", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "strikethrough", Name = "Strikethrough", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "alignleft", Name = "Justify left", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "aligncenter", Name = "Justify center", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "alignright", Name = "Justify right", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "alignjustify", Name = "Justify full", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand { Alias = "bullist", Name = "Bullet list", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand { Alias = "numlist", Name = "Numbered list", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand + { + Alias = "outdent", Name = "Decrease indent", Mode = RichTextEditorCommandMode.All, + }, + new RichTextEditorCommand + { + Alias = "indent", Name = "Increase indent", Mode = RichTextEditorCommandMode.All, + }, + new RichTextEditorCommand { Alias = "link", Name = "Insert/edit link", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand + { + Alias = "unlink", Name = "Remove link", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand { Alias = "anchor", Name = "Anchor", Mode = RichTextEditorCommandMode.Selection }, + new RichTextEditorCommand + { + Alias = "umbmediapicker", Name = "Image", Mode = RichTextEditorCommandMode.Insert, + }, + new RichTextEditorCommand { Alias = "umbmacro", Name = "Macro", Mode = RichTextEditorCommandMode.All }, + new RichTextEditorCommand { Alias = "table", Name = "Table", Mode = RichTextEditorCommandMode.Insert }, + new RichTextEditorCommand + { + Alias = "umbembeddialog", Name = "Embed", Mode = RichTextEditorCommandMode.Insert, + }, + new RichTextEditorCommand { Alias = "hr", Name = "Horizontal rule", Mode = RichTextEditorCommandMode.Insert }, + new RichTextEditorCommand + { + Alias = "subscript", Name = "Subscript", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "superscript", Name = "Superscript", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "charmap", Name = "Character map", Mode = RichTextEditorCommandMode.Insert, + }, + new RichTextEditorCommand + { + Alias = "rtl", Name = "Right to left", Mode = RichTextEditorCommandMode.Selection, + }, + new RichTextEditorCommand + { + Alias = "ltr", Name = "Left to right", Mode = RichTextEditorCommandMode.Selection, + }, + }; - [Required] - public string Name { get; set; } = null!; + private static readonly IDictionary Default_custom_config = + new Dictionary { ["entity_encoding"] = "raw" }; - [Required] - public RichTextEditorCommandMode Mode { get; set; } - } + /// + /// HTML RichText Editor TinyMCE Commands + /// + /// WB-TODO Custom Array of objects + public RichTextEditorCommand[] Commands { get; set; } = Default_commands; + + /// + /// HTML RichText Editor TinyMCE Plugins + /// + public string[] Plugins { get; set; } = Default_plugins; + + /// + /// HTML RichText Editor TinyMCE Custom Config + /// + /// WB-TODO Custom Dictionary + public IDictionary CustomConfig { get; set; } = Default_custom_config; + + /// + /// + [DefaultValue(StaticValidElements)] + public string ValidElements { get; set; } = StaticValidElements; + + /// + /// Invalid HTML elements for RichText Editor + /// + [DefaultValue(StaticInvalidElements)] + public string InvalidElements { get; set; } = StaticInvalidElements; + + public class RichTextEditorCommand + { + [Required] + public string Alias { get; set; } = null!; + + [Required] + public string Name { get; set; } = null!; + + [Required] + public RichTextEditorCommandMode Mode { get; set; } } } diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationCacheBuster.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationCacheBuster.cs index db1e1526e5..37426fa84f 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationCacheBuster.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationCacheBuster.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +public enum RuntimeMinificationCacheBuster { - public enum RuntimeMinificationCacheBuster - { - Version, - AppDomain, - Timestamp - } + Version, + AppDomain, + Timestamp, } diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs index 643e83bcac..09c55c784b 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs @@ -1,30 +1,30 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigRuntimeMinification)] +public class RuntimeMinificationSettings { - [UmbracoOptions(Constants.Configuration.ConfigRuntimeMinification)] - public class RuntimeMinificationSettings - { - internal const bool StaticUseInMemoryCache = false; - internal const string StaticCacheBuster = "Version"; - internal const string? StaticVersion = null; + internal const bool StaticUseInMemoryCache = false; + internal const string StaticCacheBuster = "Version"; + internal const string? StaticVersion = null; - /// - /// Use in memory cache - /// - [DefaultValue(StaticUseInMemoryCache)] - public bool UseInMemoryCache { get; set; } = StaticUseInMemoryCache; + /// + /// Use in memory cache + /// + [DefaultValue(StaticUseInMemoryCache)] + public bool UseInMemoryCache { get; set; } = StaticUseInMemoryCache; - /// - /// The cache buster type to use - /// - [DefaultValue(StaticCacheBuster)] - public RuntimeMinificationCacheBuster CacheBuster { get; set; } = Enum.Parse(StaticCacheBuster); + /// + /// The cache buster type to use + /// + [DefaultValue(StaticCacheBuster)] + public RuntimeMinificationCacheBuster CacheBuster { get; set; } = + Enum.Parse(StaticCacheBuster); - /// - /// The unique version string used if CacheBuster is 'Version'. - /// - [DefaultValue(StaticVersion)] - public string? Version { get; set; } = StaticVersion; - } + /// + /// The unique version string used if CacheBuster is 'Version'. + /// + [DefaultValue(StaticVersion)] + public string? Version { get; set; } = StaticVersion; } diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs index ef67d40102..ac4e51a1c2 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs @@ -1,22 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for runtime settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigRuntime)] +public class RuntimeSettings { /// - /// Typed configuration options for runtime settings. + /// Gets or sets a value for the maximum query string length. /// - [UmbracoOptions(Constants.Configuration.ConfigRuntime)] - public class RuntimeSettings - { - /// - /// Gets or sets a value for the maximum query string length. - /// - public int? MaxQueryStringLength { get; set; } + public int? MaxQueryStringLength { get; set; } - /// - /// Gets or sets a value for the maximum request length in kb. - /// - public int? MaxRequestLength { get; set; } - } + /// + /// Gets or sets a value for the maximum request length in kb. + /// + public int? MaxRequestLength { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 5ec94381b4..241b796d16 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -3,81 +3,85 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for security settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigSecurity)] +public class SecuritySettings { + internal const bool StaticMemberBypassTwoFactorForExternalLogins = true; + internal const bool StaticUserBypassTwoFactorForExternalLogins = true; + internal const bool StaticKeepUserLoggedIn = false; + internal const bool StaticHideDisabledUsersInBackOffice = false; + internal const bool StaticAllowPasswordReset = true; + internal const string StaticAuthCookieName = "UMB_UCONTEXT"; + + internal const string StaticAllowedUserNameCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\"; + /// - /// Typed configuration options for security settings. + /// Gets or sets a value indicating whether to keep the user logged in. /// - [UmbracoOptions(Constants.Configuration.ConfigSecurity)] - public class SecuritySettings - { - internal const bool StaticMemberBypassTwoFactorForExternalLogins = true; - internal const bool StaticUserBypassTwoFactorForExternalLogins = true; - internal const bool StaticKeepUserLoggedIn = false; - internal const bool StaticHideDisabledUsersInBackOffice = false; - internal const bool StaticAllowPasswordReset = true; - internal const string StaticAuthCookieName = "UMB_UCONTEXT"; - internal const string StaticAllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\"; + [DefaultValue(StaticKeepUserLoggedIn)] + public bool KeepUserLoggedIn { get; set; } = StaticKeepUserLoggedIn; - /// - /// Gets or sets a value indicating whether to keep the user logged in. - /// - [DefaultValue(StaticKeepUserLoggedIn)] - public bool KeepUserLoggedIn { get; set; } = StaticKeepUserLoggedIn; + /// + /// Gets or sets a value indicating whether to hide disabled users in the back-office. + /// + [DefaultValue(StaticHideDisabledUsersInBackOffice)] + public bool HideDisabledUsersInBackOffice { get; set; } = StaticHideDisabledUsersInBackOffice; - /// - /// Gets or sets a value indicating whether to hide disabled users in the back-office. - /// - [DefaultValue(StaticHideDisabledUsersInBackOffice)] - public bool HideDisabledUsersInBackOffice { get; set; } = StaticHideDisabledUsersInBackOffice; + /// + /// Gets or sets a value indicating whether to allow user password reset. + /// + [DefaultValue(StaticAllowPasswordReset)] + public bool AllowPasswordReset { get; set; } = StaticAllowPasswordReset; - /// - /// Gets or sets a value indicating whether to allow user password reset. - /// - [DefaultValue(StaticAllowPasswordReset)] - public bool AllowPasswordReset { get; set; } = StaticAllowPasswordReset; + /// + /// Gets or sets a value for the authorization cookie name. + /// + [DefaultValue(StaticAuthCookieName)] + public string AuthCookieName { get; set; } = StaticAuthCookieName; - /// - /// Gets or sets a value for the authorization cookie name. - /// - [DefaultValue(StaticAuthCookieName)] - public string AuthCookieName { get; set; } = StaticAuthCookieName; + /// + /// Gets or sets a value for the authorization cookie domain. + /// + public string? AuthCookieDomain { get; set; } - /// - /// Gets or sets a value for the authorization cookie domain. - /// - public string? AuthCookieDomain { get; set; } + /// + /// Gets or sets a value indicating whether the user's email address is to be considered as their username. + /// + public bool UsernameIsEmail { get; set; } = true; - /// - /// Gets or sets a value indicating whether the user's email address is to be considered as their username. - /// - public bool UsernameIsEmail { get; set; } = true; + /// + /// Gets or sets the set of allowed characters for a username + /// + [DefaultValue(StaticAllowedUserNameCharacters)] + public string AllowedUserNameCharacters { get; set; } = StaticAllowedUserNameCharacters; - /// - /// Gets or sets the set of allowed characters for a username - /// - [DefaultValue(StaticAllowedUserNameCharacters)] - public string AllowedUserNameCharacters { get; set; } = StaticAllowedUserNameCharacters; + /// + /// Gets or sets a value for the user password settings. + /// + public UserPasswordConfigurationSettings? UserPassword { get; set; } - /// - /// Gets or sets a value for the user password settings. - /// - public UserPasswordConfigurationSettings? UserPassword { get; set; } + /// + /// Gets or sets a value for the member password settings. + /// + public MemberPasswordConfigurationSettings? MemberPassword { get; set; } - /// - /// Gets or sets a value for the member password settings. - /// - public MemberPasswordConfigurationSettings? MemberPassword { get; set; } - /// - /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for members. Thereby rely on the External login and potential 2FA at that provider. - /// - [DefaultValue(StaticMemberBypassTwoFactorForExternalLogins)] - public bool MemberBypassTwoFactorForExternalLogins { get; set; } = StaticMemberBypassTwoFactorForExternalLogins; + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login + /// for members. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticMemberBypassTwoFactorForExternalLogins)] + public bool MemberBypassTwoFactorForExternalLogins { get; set; } = StaticMemberBypassTwoFactorForExternalLogins; - /// - /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for users. Thereby rely on the External login and potential 2FA at that provider. - /// - [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] - public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; - } + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login + /// for users. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] + public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; } diff --git a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs index 54b9ad6c84..5a9ec1b94f 100644 --- a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs @@ -6,91 +6,95 @@ using System.ComponentModel.DataAnnotations; using System.Net.Mail; using Umbraco.Cms.Core.Configuration.Models.Validation; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Matches MailKit.Security.SecureSocketOptions and defined locally to avoid having to take +/// a dependency on this external library into Umbraco.Core. +/// +/// +public enum SecureSocketOptions { /// - /// Matches MailKit.Security.SecureSocketOptions and defined locally to avoid having to take - /// a dependency on this external library into Umbraco.Core. + /// No SSL or TLS encryption should be used. /// - /// - public enum SecureSocketOptions - { - /// - /// No SSL or TLS encryption should be used. - /// - None = 0, - - /// - /// Allow the IMailService to decide which SSL or TLS options to use (default). If the server does not support SSL or TLS, then the connection will continue without any encryption. - /// - Auto = 1, - - /// - /// The connection should use SSL or TLS encryption immediately. - /// - SslOnConnect = 2, - - /// - /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the server. If the server does not support the STARTTLS extension, then the connection will fail and a NotSupportedException will be thrown. - /// - StartTls = 3, - - /// - /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the server, but only if the server supports the STARTTLS extension. - /// - StartTlsWhenAvailable = 4 - } + None = 0, /// - /// Typed configuration options for SMTP settings. + /// Allow the IMailService to decide which SSL or TLS options to use (default). If the server does not support SSL or + /// TLS, then the connection will continue without any encryption. /// - public class SmtpSettings : ValidatableEntryBase - { - internal const string StaticSecureSocketOptions = "Auto"; - internal const string StaticDeliveryMethod = "Network"; + Auto = 1, - /// - /// Gets or sets a value for the SMTP from address to use for messages. - /// - [Required] - [EmailAddress] - public string From { get; set; } = null!; + /// + /// The connection should use SSL or TLS encryption immediately. + /// + SslOnConnect = 2, - /// - /// Gets or sets a value for the SMTP host. - /// - public string? Host { get; set; } + /// + /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the + /// server. If the server does not support the STARTTLS extension, then the connection will fail and a + /// NotSupportedException will be thrown. + /// + StartTls = 3, - /// - /// Gets or sets a value for the SMTP port. - /// - public int Port { get; set; } - - /// - /// Gets or sets a value for the secure socket options. - /// - [DefaultValue(StaticSecureSocketOptions)] - public SecureSocketOptions SecureSocketOptions { get; set; } = Enum.Parse(StaticSecureSocketOptions); - - /// - /// Gets or sets a value for the SMTP pick-up directory. - /// - public string? PickupDirectoryLocation { get; set; } - - /// - /// Gets or sets a value for the SMTP delivery method. - /// - [DefaultValue(StaticDeliveryMethod)] - public SmtpDeliveryMethod DeliveryMethod { get; set; } = Enum.Parse(StaticDeliveryMethod); - - /// - /// Gets or sets a value for the SMTP user name. - /// - public string? Username { get; set; } - - /// - /// Gets or sets a value for the SMTP password. - /// - public string? Password { get; set; } - } + /// + /// Elevates the connection to use TLS encryption immediately after reading the greeting and capabilities of the + /// server, but only if the server supports the STARTTLS extension. + /// + StartTlsWhenAvailable = 4, +} + +/// +/// Typed configuration options for SMTP settings. +/// +public class SmtpSettings : ValidatableEntryBase +{ + internal const string StaticSecureSocketOptions = "Auto"; + internal const string StaticDeliveryMethod = "Network"; + + /// + /// Gets or sets a value for the SMTP from address to use for messages. + /// + [Required] + [EmailAddress] + public string From { get; set; } = null!; + + /// + /// Gets or sets a value for the SMTP host. + /// + public string? Host { get; set; } + + /// + /// Gets or sets a value for the SMTP port. + /// + public int Port { get; set; } + + /// + /// Gets or sets a value for the secure socket options. + /// + [DefaultValue(StaticSecureSocketOptions)] + public SecureSocketOptions SecureSocketOptions { get; set; } = + Enum.Parse(StaticSecureSocketOptions); + + /// + /// Gets or sets a value for the SMTP pick-up directory. + /// + public string? PickupDirectoryLocation { get; set; } + + /// + /// Gets or sets a value for the SMTP delivery method. + /// + [DefaultValue(StaticDeliveryMethod)] + public SmtpDeliveryMethod DeliveryMethod { get; set; } = Enum.Parse(StaticDeliveryMethod); + + /// + /// Gets or sets a value for the SMTP user name. + /// + public string? Username { get; set; } + + /// + /// Gets or sets a value for the SMTP password. + /// + public string? Password { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/TourSettings.cs b/src/Umbraco.Core/Configuration/Models/TourSettings.cs index cdc54dfe1f..aaf2063c64 100644 --- a/src/Umbraco.Core/Configuration/Models/TourSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/TourSettings.cs @@ -3,20 +3,19 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models -{ - /// - /// Typed configuration options for tour settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigTours)] - public class TourSettings - { - internal const bool StaticEnableTours = true; +namespace Umbraco.Cms.Core.Configuration.Models; - /// - /// Gets or sets a value indicating whether back-office tours are enabled. - /// - [DefaultValue(StaticEnableTours)] - public bool EnableTours { get; set; } = StaticEnableTours; - } +/// +/// Typed configuration options for tour settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigTours)] +public class TourSettings +{ + internal const bool StaticEnableTours = true; + + /// + /// Gets or sets a value indicating whether back-office tours are enabled. + /// + [DefaultValue(StaticEnableTours)] + public bool EnableTours { get; set; } = StaticEnableTours; } diff --git a/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs b/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs index 30ef3718f4..f281bbc31a 100644 --- a/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs @@ -1,28 +1,25 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for type finder settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigTypeFinder)] +public class TypeFinderSettings { /// - /// Typed configuration options for type finder settings. + /// Gets or sets a value for the assemblies that accept load exceptions during type finder operations. /// - [UmbracoOptions(Constants.Configuration.ConfigTypeFinder)] - public class TypeFinderSettings - { - /// - /// Gets or sets a value for the assemblies that accept load exceptions during type finder operations. - /// - [Required] - public string AssembliesAcceptingLoadExceptions { get; set; } = null!; + [Required] + public string AssembliesAcceptingLoadExceptions { get; set; } = null!; - /// - /// By default the entry assemblies for scanning plugin types is the Umbraco DLLs. If you require - /// scanning for plugins based on different root referenced assemblies you can add the assembly name to this list. - /// - public IEnumerable? AdditionalEntryAssemblies { get; set; } - } + /// + /// By default the entry assemblies for scanning plugin types is the Umbraco DLLs. If you require + /// scanning for plugins based on different root referenced assemblies you can add the assembly name to this list. + /// + public IEnumerable? AdditionalEntryAssemblies { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs index d016e3547b..bec6d77bfb 100644 --- a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs @@ -1,30 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +/// +/// Typed configuration options for the plugins. +/// +[UmbracoOptions(Constants.Configuration.ConfigPlugins)] +public class UmbracoPluginSettings { /// - /// Typed configuration options for the plugins. + /// Gets or sets the allowed file extensions (including the period ".") that should be accessible from the browser. /// - [UmbracoOptions(Constants.Configuration.ConfigPlugins)] - public class UmbracoPluginSettings + /// WB-TODO + public ISet BrowsableFileExtensions { get; set; } = new HashSet(new[] { - /// - /// Gets or sets the allowed file extensions (including the period ".") that should be accessible from the browser. - /// - /// WB-TODO - public ISet BrowsableFileExtensions { get; set; } = new HashSet(new[] - { - ".html", // markup - ".css", // styles - ".js", // scripts - ".jpg", ".jpeg", ".gif", ".png", ".svg", // images - ".eot", ".ttf", ".woff", // fonts - ".xml", ".json", ".config", // configurations - ".lic", // license - ".map" // js map files - }); - } + ".html", // markup + ".css", // styles + ".js", // scripts + ".jpg", ".jpeg", ".gif", ".png", ".svg", // images + ".eot", ".ttf", ".woff", // fonts + ".xml", ".json", ".config", // configurations + ".lic", // license + ".map", // js map files + }); } diff --git a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs index 08a4af5667..577fb9a2d9 100644 --- a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs @@ -4,57 +4,58 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for unattended settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigUnattended)] +public class UnattendedSettings { + private const bool StaticInstallUnattended = false; + private const bool StaticUpgradeUnattended = false; + /// - /// Typed configuration options for unattended settings. + /// Gets or sets a value indicating whether unattended installs are enabled. /// - [UmbracoOptions(Constants.Configuration.ConfigUnattended)] - public class UnattendedSettings - { - private const bool StaticInstallUnattended = false; - private const bool StaticUpgradeUnattended = false; + /// + /// + /// By default, when a database connection string is configured and it is possible to connect to + /// the database, but the database is empty, the runtime enters the Install level. + /// If this option is set to true an unattended install will be performed and the runtime enters + /// the Run level. + /// + /// + [DefaultValue(StaticInstallUnattended)] + public bool InstallUnattended { get; set; } = StaticInstallUnattended; - /// - /// Gets or sets a value indicating whether unattended installs are enabled. - /// - /// - /// By default, when a database connection string is configured and it is possible to connect to - /// the database, but the database is empty, the runtime enters the Install level. - /// If this option is set to true an unattended install will be performed and the runtime enters - /// the Run level. - /// - [DefaultValue(StaticInstallUnattended)] - public bool InstallUnattended { get; set; } = StaticInstallUnattended; + /// + /// Gets or sets a value indicating whether unattended upgrades are enabled. + /// + [DefaultValue(StaticUpgradeUnattended)] + public bool UpgradeUnattended { get; set; } = StaticUpgradeUnattended; - /// - /// Gets or sets a value indicating whether unattended upgrades are enabled. - /// - [DefaultValue(StaticUpgradeUnattended)] - public bool UpgradeUnattended { get; set; } = StaticUpgradeUnattended; + /// + /// Gets or sets a value indicating whether unattended package migrations are enabled. + /// + /// + /// This is true by default. + /// + public bool PackageMigrationsUnattended { get; set; } = true; - /// - /// Gets or sets a value indicating whether unattended package migrations are enabled. - /// - /// - /// This is true by default. - /// - public bool PackageMigrationsUnattended { get; set; } = true; + /// + /// Gets or sets a value to use for creating a user with a name for Unattended Installs + /// + public string? UnattendedUserName { get; set; } = null; - /// - /// Gets or sets a value to use for creating a user with a name for Unattended Installs - /// - public string? UnattendedUserName { get; set; } = null; + /// + /// Gets or sets a value to use for creating a user with an email for Unattended Installs + /// + [EmailAddress] + public string? UnattendedUserEmail { get; set; } = null; - /// - /// Gets or sets a value to use for creating a user with an email for Unattended Installs - /// - [EmailAddress] - public string? UnattendedUserEmail { get; set; } = null; - - /// - /// Gets or sets a value to use for creating a user with a password for Unattended Installs - /// - public string? UnattendedUserPassword { get; set; } = null; - } + /// + /// Gets or sets a value to use for creating a user with a password for Unattended Installs + /// + public string? UnattendedUserPassword { get; set; } = null; } diff --git a/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs b/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs index b53e98f712..156f90419c 100644 --- a/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UserPasswordConfigurationSettings.cs @@ -3,47 +3,46 @@ using System.ComponentModel; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for user password settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigUserPassword)] +public class UserPasswordConfigurationSettings : IPasswordConfiguration { - /// - /// Typed configuration options for user password settings. - /// - [UmbracoOptions(Constants.Configuration.ConfigUserPassword)] - public class UserPasswordConfigurationSettings : IPasswordConfiguration - { - internal const int StaticRequiredLength = 10; - internal const bool StaticRequireNonLetterOrDigit = false; - internal const bool StaticRequireDigit = false; - internal const bool StaticRequireLowercase = false; - internal const bool StaticRequireUppercase = false; - internal const int StaticMaxFailedAccessAttemptsBeforeLockout = 5; + internal const int StaticRequiredLength = 10; + internal const bool StaticRequireNonLetterOrDigit = false; + internal const bool StaticRequireDigit = false; + internal const bool StaticRequireLowercase = false; + internal const bool StaticRequireUppercase = false; + internal const int StaticMaxFailedAccessAttemptsBeforeLockout = 5; - /// - [DefaultValue(StaticRequiredLength)] - public int RequiredLength { get; set; } = StaticRequiredLength; + /// + [DefaultValue(StaticRequiredLength)] + public int RequiredLength { get; set; } = StaticRequiredLength; - /// - [DefaultValue(StaticRequireNonLetterOrDigit)] - public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; + /// + [DefaultValue(StaticRequireNonLetterOrDigit)] + public bool RequireNonLetterOrDigit { get; set; } = StaticRequireNonLetterOrDigit; - /// - [DefaultValue(StaticRequireDigit)] - public bool RequireDigit { get; set; } = StaticRequireDigit; + /// + [DefaultValue(StaticRequireDigit)] + public bool RequireDigit { get; set; } = StaticRequireDigit; - /// - [DefaultValue(StaticRequireLowercase)] - public bool RequireLowercase { get; set; } = StaticRequireLowercase; + /// + [DefaultValue(StaticRequireLowercase)] + public bool RequireLowercase { get; set; } = StaticRequireLowercase; - /// - [DefaultValue(StaticRequireUppercase)] - public bool RequireUppercase { get; set; } = StaticRequireUppercase; + /// + [DefaultValue(StaticRequireUppercase)] + public bool RequireUppercase { get; set; } = StaticRequireUppercase; - /// - [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] - public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; + /// + [DefaultValue(Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)] + public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName; - /// - [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] - public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; - } + /// + [DefaultValue(StaticMaxFailedAccessAttemptsBeforeLockout)] + public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = StaticMaxFailedAccessAttemptsBeforeLockout; } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs b/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs index ca5d4d11e5..447a27f026 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs @@ -1,75 +1,73 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Base class for configuration validators. +/// +public abstract class ConfigurationValidatorBase { /// - /// Base class for configuration validators. + /// Validates that a string is one of a set of valid values. /// - public abstract class ConfigurationValidatorBase + /// Configuration path from where the setting is found. + /// The value to check. + /// The set of valid values. + /// A message to output if the value does not match. + /// True if valid, false if not. + public bool ValidateStringIsOneOfValidValues(string configPath, string value, IEnumerable validValues, out string message) { - /// - /// Validates that a string is one of a set of valid values. - /// - /// Configuration path from where the setting is found. - /// The value to check. - /// The set of valid values. - /// A message to output if the value does not match. - /// True if valid, false if not. - public bool ValidateStringIsOneOfValidValues(string configPath, string value, IEnumerable validValues, out string message) + if (!validValues.InvariantContains(value)) { - if (!validValues.InvariantContains(value)) - { - message = $"Configuration entry {configPath} contains an invalid value '{value}', it should be one of the following: '{string.Join(", ", validValues)}'."; - return false; - } - - message = string.Empty; - return true; + message = + $"Configuration entry {configPath} contains an invalid value '{value}', it should be one of the following: '{string.Join(", ", validValues)}'."; + return false; } - /// - /// Validates that a collection of objects are all valid based on their data annotations. - /// - /// Configuration path from where the setting is found. - /// The values to check. - /// Description of validation appended to message if validation fails. - /// A message to output if the value does not match. - /// True if valid, false if not. - public bool ValidateCollection(string configPath, IEnumerable values, string validationDescription, out string message) - { - if (values.Any(x => !x.IsValid())) - { - message = $"Configuration entry {configPath} contains one or more invalid values. {validationDescription}."; - return false; - } + message = string.Empty; + return true; + } - message = string.Empty; - return true; + /// + /// Validates that a collection of objects are all valid based on their data annotations. + /// + /// Configuration path from where the setting is found. + /// The values to check. + /// Description of validation appended to message if validation fails. + /// A message to output if the value does not match. + /// True if valid, false if not. + public bool ValidateCollection(string configPath, IEnumerable values, string validationDescription, out string message) + { + if (values.Any(x => !x.IsValid())) + { + message = $"Configuration entry {configPath} contains one or more invalid values. {validationDescription}."; + return false; } - /// - /// Validates a configuration entry is valid if provided. - /// - /// Configuration path from where the setting is found. - /// The value to check. - /// Description of validation appended to message if validation fails. - /// A message to output if the value does not match. - /// True if valid, false if not. - public bool ValidateOptionalEntry(string configPath, ValidatableEntryBase? value, string validationDescription, out string message) - { - if (value != null && !value.IsValid()) - { - message = $"Configuration entry {configPath} contains one or more invalid values. {validationDescription}."; - return false; - } + message = string.Empty; + return true; + } - message = string.Empty; - return true; + /// + /// Validates a configuration entry is valid if provided. + /// + /// Configuration path from where the setting is found. + /// The value to check. + /// Description of validation appended to message if validation fails. + /// A message to output if the value does not match. + /// True if valid, false if not. + public bool ValidateOptionalEntry(string configPath, ValidatableEntryBase? value, string validationDescription, out string message) + { + if (value != null && !value.IsValid()) + { + message = $"Configuration entry {configPath} contains one or more invalid values. {validationDescription}."; + return false; } + + message = string.Empty; + return true; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs index d21d6277bf..0798014600 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs @@ -1,36 +1,42 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class ContentSettingsValidator : ConfigurationValidatorBase, IValidateOptions { - /// - /// Validator for configuration representated as . - /// - public class ContentSettingsValidator : ConfigurationValidatorBase, IValidateOptions + /// + public ValidateOptionsResult Validate(string name, ContentSettings options) { - /// - public ValidateOptionsResult Validate(string name, ContentSettings options) + if (!ValidateError404Collection(options.Error404Collection, out var message)) { - if (!ValidateError404Collection(options.Error404Collection, out string message)) - { - return ValidateOptionsResult.Fail(message); - } - - if (!ValidateAutoFillImageProperties(options.Imaging.AutoFillImageProperties, out message)) - { - return ValidateOptionsResult.Fail(message); - } - - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail(message); } - private bool ValidateError404Collection(IEnumerable values, out string message) => - ValidateCollection($"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Error404Collection)}", values, "Culture and one and only one of ContentId, ContentKey and ContentXPath must be specified for each entry", out message); + if (!ValidateAutoFillImageProperties(options.Imaging.AutoFillImageProperties, out message)) + { + return ValidateOptionsResult.Fail(message); + } - private bool ValidateAutoFillImageProperties(IEnumerable values, out string message) => - ValidateCollection($"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Imaging)}:{nameof(ContentSettings.Imaging.AutoFillImageProperties)}", values, "Alias, WidthFieldAlias, HeightFieldAlias, LengthFieldAlias and ExtensionFieldAlias must be specified for each entry", out message); + return ValidateOptionsResult.Success; } + + private bool ValidateError404Collection(IEnumerable values, out string message) => + ValidateCollection( + $"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Error404Collection)}", + values, + "Culture and one and only one of ContentId, ContentKey and ContentXPath must be specified for each entry", + out message); + + private bool ValidateAutoFillImageProperties(IEnumerable values, out string message) => + ValidateCollection( + $"{Constants.Configuration.ConfigContent}:{nameof(ContentSettings.Imaging)}:{nameof(ContentSettings.Imaging.AutoFillImageProperties)}", + values, + "Alias, WidthFieldAlias, HeightFieldAlias, LengthFieldAlias and ExtensionFieldAlias must be specified for each entry", + out message); } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs index 31d0779626..32ad130c33 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs @@ -1,48 +1,51 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class GlobalSettingsValidator + : ConfigurationValidatorBase, IValidateOptions { - /// - /// Validator for configuration representated as . - /// - public class GlobalSettingsValidator - : ConfigurationValidatorBase, IValidateOptions + /// + public ValidateOptionsResult Validate(string name, GlobalSettings options) { - /// - public ValidateOptionsResult Validate(string name, GlobalSettings options) + if (!ValidateSmtpSetting(options.Smtp, out var message)) { - if (!ValidateSmtpSetting(options.Smtp, out var message)) - { - return ValidateOptionsResult.Fail(message); - } - - if (!ValidateSqlWriteLockTimeOutSetting(options.DistributedLockingWriteLockDefaultTimeout, out var message2)) - { - return ValidateOptionsResult.Fail(message2); - } - - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail(message); } - private bool ValidateSmtpSetting(SmtpSettings? value, out string message) => - ValidateOptionalEntry($"{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.Smtp)}", value, "A valid From email address is required", out message); - - private bool ValidateSqlWriteLockTimeOutSetting(TimeSpan configuredTimeOut, out string message) { - // Only apply this setting if it's not excessively high or low - const int minimumTimeOut = 100; - const int maximumTimeOut = 20000; - if (configuredTimeOut.TotalMilliseconds < minimumTimeOut || configuredTimeOut.TotalMilliseconds > maximumTimeOut) // between 0.1 and 20 seconds - { - message = $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.DistributedLockingWriteLockDefaultTimeout)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; - return false; - } - - message = string.Empty; - return true; + if (!ValidateSqlWriteLockTimeOutSetting(options.DistributedLockingWriteLockDefaultTimeout, out var message2)) + { + return ValidateOptionsResult.Fail(message2); } + + return ValidateOptionsResult.Success; + } + + private bool ValidateSmtpSetting(SmtpSettings? value, out string message) => + ValidateOptionalEntry($"{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.Smtp)}", value, "A valid From email address is required", out message); + + private bool ValidateSqlWriteLockTimeOutSetting(TimeSpan configuredTimeOut, out string message) + { + // Only apply this setting if it's not excessively high or low + const int minimumTimeOut = 100; + const int maximumTimeOut = 20000; + + // between 0.1 and 20 seconds + if (configuredTimeOut.TotalMilliseconds < minimumTimeOut || + configuredTimeOut.TotalMilliseconds > maximumTimeOut) + { + message = + $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.DistributedLockingWriteLockDefaultTimeout)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; + return false; + } + + message = string.Empty; + return true; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs index a8b63f39a0..ac0e1651ea 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs @@ -3,45 +3,47 @@ using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class HealthChecksSettingsValidator : ConfigurationValidatorBase, IValidateOptions { + private readonly ICronTabParser _cronTabParser; + /// - /// Validator for configuration representated as . + /// Initializes a new instance of the class. /// - public class HealthChecksSettingsValidator : ConfigurationValidatorBase, IValidateOptions + /// Helper for parsing crontab expressions. + public HealthChecksSettingsValidator(ICronTabParser cronTabParser) => _cronTabParser = cronTabParser; + + /// + public ValidateOptionsResult Validate(string name, HealthChecksSettings options) { - private readonly ICronTabParser _cronTabParser; - - /// - /// Initializes a new instance of the class. - /// - /// Helper for parsing crontab expressions. - public HealthChecksSettingsValidator(ICronTabParser cronTabParser) => _cronTabParser = cronTabParser; - - /// - public ValidateOptionsResult Validate(string name, HealthChecksSettings options) + if (!ValidateNotificationFirstRunTime(options.Notification.FirstRunTime, out var message)) { - if (!ValidateNotificationFirstRunTime(options.Notification.FirstRunTime, out var message)) - { - return ValidateOptionsResult.Fail(message); - } - - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail(message); } - private bool ValidateNotificationFirstRunTime(string value, out string message) => - ValidateOptionalCronTab($"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", value, out message); + return ValidateOptionsResult.Success; + } - private bool ValidateOptionalCronTab(string configPath, string value, out string message) + private bool ValidateNotificationFirstRunTime(string value, out string message) => + ValidateOptionalCronTab( + $"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", + value, + out message); + + private bool ValidateOptionalCronTab(string configPath, string value, out string message) + { + if (!string.IsNullOrEmpty(value) && !_cronTabParser.IsValidCronTab(value)) { - if (!string.IsNullOrEmpty(value) && !_cronTabParser.IsValidCronTab(value)) - { - message = $"Configuration entry {configPath} contains an invalid cron expression."; - return false; - } - - message = string.Empty; - return true; + message = $"Configuration entry {configPath} contains an invalid cron expression."; + return false; } + + message = string.Empty; + return true; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs index 6260341c18..4a1872cf30 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/RequestHandlerSettingsValidator.cs @@ -3,28 +3,28 @@ using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class RequestHandlerSettingsValidator : ConfigurationValidatorBase, IValidateOptions { - /// - /// Validator for configuration representated as . - /// - public class RequestHandlerSettingsValidator : ConfigurationValidatorBase, IValidateOptions + /// + public ValidateOptionsResult Validate(string name, RequestHandlerSettings options) { - /// - public ValidateOptionsResult Validate(string name, RequestHandlerSettings options) + if (!ValidateConvertUrlsToAscii(options.ConvertUrlsToAscii, out var message)) { - if (!ValidateConvertUrlsToAscii(options.ConvertUrlsToAscii, out var message)) - { - return ValidateOptionsResult.Fail(message); - } - - return ValidateOptionsResult.Success; + return ValidateOptionsResult.Fail(message); } - private bool ValidateConvertUrlsToAscii(string value, out string message) - { - var validValues = new[] { "try", "true", "false" }; - return ValidateStringIsOneOfValidValues($"{Constants.Configuration.ConfigRequestHandler}:{nameof(RequestHandlerSettings.ConvertUrlsToAscii)}", value, validValues, out message); - } + return ValidateOptionsResult.Success; + } + + private bool ValidateConvertUrlsToAscii(string value, out string message) + { + var validValues = new[] { "try", "true", "false" }; + return ValidateStringIsOneOfValidValues( + $"{Constants.Configuration.ConfigRequestHandler}:{nameof(RequestHandlerSettings.ConvertUrlsToAscii)}", value, validValues, out message); } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs index 3c073ac100..e262de76e7 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/UnattendedSettingsValidator.cs @@ -1,44 +1,44 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Validator for configuration representated as . +/// +public class UnattendedSettingsValidator + : IValidateOptions { - /// - /// Validator for configuration representated as . - /// - public class UnattendedSettingsValidator - : IValidateOptions + /// + public ValidateOptionsResult Validate(string name, UnattendedSettings options) { - /// - public ValidateOptionsResult Validate(string name, UnattendedSettings options) + if (options.InstallUnattended) { - if (options.InstallUnattended) + var setValues = 0; + if (!string.IsNullOrEmpty(options.UnattendedUserName)) { - int setValues = 0; - if (!string.IsNullOrEmpty(options.UnattendedUserName)) - { - setValues++; - } - - if (!string.IsNullOrEmpty(options.UnattendedUserEmail)) - { - setValues++; - } - - if (!string.IsNullOrEmpty(options.UnattendedUserPassword)) - { - setValues++; - } - - if (0 < setValues && setValues < 3) - { - return ValidateOptionsResult.Fail($"Configuration entry {Constants.Configuration.ConfigUnattended} contains invalid values.\nIf any of the {nameof(options.UnattendedUserName)}, {nameof(options.UnattendedUserEmail)}, {nameof(options.UnattendedUserPassword)} are set, all of them are required."); - } + setValues++; } - return ValidateOptionsResult.Success; + if (!string.IsNullOrEmpty(options.UnattendedUserEmail)) + { + setValues++; + } + + if (!string.IsNullOrEmpty(options.UnattendedUserPassword)) + { + setValues++; + } + + if (setValues > 0 && setValues < 3) + { + return ValidateOptionsResult.Fail( + $"Configuration entry {Constants.Configuration.ConfigUnattended} contains invalid values.\nIf any of the {nameof(options.UnattendedUserName)}, {nameof(options.UnattendedUserEmail)}, {nameof(options.UnattendedUserPassword)} are set, all of them are required."); + } } + + return ValidateOptionsResult.Success; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs b/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs index 970146a27e..ff858943ac 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/ValidatableEntryBase.cs @@ -1,21 +1,19 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Configuration.Models.Validation +namespace Umbraco.Cms.Core.Configuration.Models.Validation; + +/// +/// Provides a base class for configuration models that can be validated based on data annotations. +/// +public abstract class ValidatableEntryBase { - /// - /// Provides a base class for configuration models that can be validated based on data annotations. - /// - public abstract class ValidatableEntryBase + internal virtual bool IsValid() { - internal virtual bool IsValid() - { - var ctx = new ValidationContext(this); - var results = new List(); - return Validator.TryValidateObject(this, ctx, results, true); - } + var ctx = new ValidationContext(this); + var results = new List(); + return Validator.TryValidateObject(this, ctx, results, true); } } diff --git a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs index deb7c64a9f..c4dff7a542 100644 --- a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs @@ -4,81 +4,81 @@ using System.ComponentModel; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for web routing settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigWebRouting)] +public class WebRoutingSettings { + internal const bool StaticTryMatchingEndpointsForAllPages = false; + internal const bool StaticTrySkipIisCustomErrors = false; + internal const bool StaticInternalRedirectPreservesTemplate = false; + internal const bool StaticDisableAlternativeTemplates = false; + internal const bool StaticValidateAlternativeTemplates = false; + internal const bool StaticDisableFindContentByIdPath = false; + internal const bool StaticDisableRedirectUrlTracking = false; + internal const string StaticUrlProviderMode = "Auto"; + /// - /// Typed configuration options for web routing settings. + /// Gets or sets a value indicating whether to check if any routed endpoints match a front-end request before + /// the Umbraco dynamic router tries to map the request to an Umbraco content item. /// - [UmbracoOptions(Constants.Configuration.ConfigWebRouting)] - public class WebRoutingSettings - { + /// + /// This should not be necessary if the Umbraco catch-all/dynamic route is registered last like it's supposed to be. In + /// that case + /// ASP.NET Core will automatically handle this in all cases. This is more of a backward compatible option since this + /// is what v7/v8 used + /// to do. + /// + [DefaultValue(StaticTryMatchingEndpointsForAllPages)] + public bool TryMatchingEndpointsForAllPages { get; set; } = StaticTryMatchingEndpointsForAllPages; - internal const bool StaticTryMatchingEndpointsForAllPages = false; - internal const bool StaticTrySkipIisCustomErrors = false; - internal const bool StaticInternalRedirectPreservesTemplate = false; - internal const bool StaticDisableAlternativeTemplates = false; - internal const bool StaticValidateAlternativeTemplates = false; - internal const bool StaticDisableFindContentByIdPath = false; - internal const bool StaticDisableRedirectUrlTracking = false; - internal const string StaticUrlProviderMode = "Auto"; + /// + /// Gets or sets a value indicating whether IIS custom errors should be skipped. + /// + [DefaultValue(StaticTrySkipIisCustomErrors)] + public bool TrySkipIisCustomErrors { get; set; } = StaticTrySkipIisCustomErrors; - /// - /// Gets or sets a value indicating whether to check if any routed endpoints match a front-end request before - /// the Umbraco dynamic router tries to map the request to an Umbraco content item. - /// - /// - /// This should not be necessary if the Umbraco catch-all/dynamic route is registered last like it's supposed to be. In that case - /// ASP.NET Core will automatically handle this in all cases. This is more of a backward compatible option since this is what v7/v8 used - /// to do. - /// - [DefaultValue(StaticTryMatchingEndpointsForAllPages)] - public bool TryMatchingEndpointsForAllPages { get; set; } = StaticTryMatchingEndpointsForAllPages; + /// + /// Gets or sets a value indicating whether an internal redirect should preserve the template. + /// + [DefaultValue(StaticInternalRedirectPreservesTemplate)] + public bool InternalRedirectPreservesTemplate { get; set; } = StaticInternalRedirectPreservesTemplate; - /// - /// Gets or sets a value indicating whether IIS custom errors should be skipped. - /// - [DefaultValue(StaticTrySkipIisCustomErrors)] - public bool TrySkipIisCustomErrors { get; set; } = StaticTrySkipIisCustomErrors; + /// + /// Gets or sets a value indicating whether the use of alternative templates are disabled. + /// + [DefaultValue(StaticDisableAlternativeTemplates)] + public bool DisableAlternativeTemplates { get; set; } = StaticDisableAlternativeTemplates; - /// - /// Gets or sets a value indicating whether an internal redirect should preserve the template. - /// - [DefaultValue(StaticInternalRedirectPreservesTemplate)] - public bool InternalRedirectPreservesTemplate { get; set; } = StaticInternalRedirectPreservesTemplate; + /// + /// Gets or sets a value indicating whether the use of alternative templates should be validated. + /// + [DefaultValue(StaticValidateAlternativeTemplates)] + public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates; - /// - /// Gets or sets a value indicating whether the use of alternative templates are disabled. - /// - [DefaultValue(StaticDisableAlternativeTemplates)] - public bool DisableAlternativeTemplates { get; set; } = StaticDisableAlternativeTemplates; + /// + /// Gets or sets a value indicating whether find content ID by path is disabled. + /// + [DefaultValue(StaticDisableFindContentByIdPath)] + public bool DisableFindContentByIdPath { get; set; } = StaticDisableFindContentByIdPath; - /// - /// Gets or sets a value indicating whether the use of alternative templates should be validated. - /// - [DefaultValue(StaticValidateAlternativeTemplates)] - public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates; + /// + /// Gets or sets a value indicating whether redirect URL tracking is disabled. + /// + [DefaultValue(StaticDisableRedirectUrlTracking)] + public bool DisableRedirectUrlTracking { get; set; } = StaticDisableRedirectUrlTracking; - /// - /// Gets or sets a value indicating whether find content ID by path is disabled. - /// - [DefaultValue(StaticDisableFindContentByIdPath)] - public bool DisableFindContentByIdPath { get; set; } = StaticDisableFindContentByIdPath; + /// + /// Gets or sets a value for the URL provider mode (). + /// + [DefaultValue(StaticUrlProviderMode)] + public UrlMode UrlProviderMode { get; set; } = Enum.Parse(StaticUrlProviderMode); - /// - /// Gets or sets a value indicating whether redirect URL tracking is disabled. - /// - [DefaultValue(StaticDisableRedirectUrlTracking)] - public bool DisableRedirectUrlTracking { get; set; } = StaticDisableRedirectUrlTracking; - - /// - /// Gets or sets a value for the URL provider mode (). - /// - [DefaultValue(StaticUrlProviderMode)] - public UrlMode UrlProviderMode { get; set; } = Enum.Parse(StaticUrlProviderMode); - - /// - /// Gets or sets a value for the Umbraco application URL. - /// - public string UmbracoApplicationUrl { get; set; } = null!; - } + /// + /// Gets or sets a value for the Umbraco application URL. + /// + public string UmbracoApplicationUrl { get; set; } = null!; } diff --git a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs index 1b1ebc6af5..bcd659c734 100644 --- a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs +++ b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs @@ -1,57 +1,61 @@ -using System.IO; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ModelsBuilderConfigExtensions { - public static class ModelsBuilderConfigExtensions + private static string? _modelsDirectoryAbsolute; + + public static string ModelsDirectoryAbsolute( + this ModelsBuilderSettings modelsBuilderConfig, + IHostingEnvironment hostingEnvironment) { - private static string? _modelsDirectoryAbsolute = null; - - public static string ModelsDirectoryAbsolute(this ModelsBuilderSettings modelsBuilderConfig, IHostingEnvironment hostingEnvironment) + if (_modelsDirectoryAbsolute is null) { - if (_modelsDirectoryAbsolute is null) - { - var modelsDirectory = modelsBuilderConfig.ModelsDirectory; - var root = hostingEnvironment.MapPathContentRoot("~/"); + var modelsDirectory = modelsBuilderConfig.ModelsDirectory; + var root = hostingEnvironment.MapPathContentRoot("~/"); - _modelsDirectoryAbsolute = GetModelsDirectory(root, modelsDirectory, - modelsBuilderConfig.AcceptUnsafeModelsDirectory); - } - - return _modelsDirectoryAbsolute; + _modelsDirectoryAbsolute = GetModelsDirectory(root, modelsDirectory, modelsBuilderConfig.AcceptUnsafeModelsDirectory); } - // internal for tests - internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) + return _modelsDirectoryAbsolute; + } + + // internal for tests + internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) + { + // making sure it is safe, ie under the website root, + // unless AcceptUnsafeModelsDirectory and then everything is OK. + if (!Path.IsPathRooted(root)) { - // making sure it is safe, ie under the website root, - // unless AcceptUnsafeModelsDirectory and then everything is OK. + throw new ConfigurationException($"Root is not rooted \"{root}\"."); + } - if (!Path.IsPathRooted(root)) - throw new ConfigurationException($"Root is not rooted \"{root}\"."); + if (config.StartsWith("~/")) + { + var dir = Path.Combine(root, config.TrimStart("~/")); - if (config.StartsWith("~/")) + // sanitize - GetFullPath will take care of any relative + // segments in path, eg '../../foo.tmp' - it may throw a SecurityException + // if the combined path reaches illegal parts of the filesystem + dir = Path.GetFullPath(dir); + root = Path.GetFullPath(root); + + if (!dir.StartsWith(root) && !acceptUnsafe) { - var dir = Path.Combine(root, config.TrimStart("~/")); - - // sanitize - GetFullPath will take care of any relative - // segments in path, eg '../../foo.tmp' - it may throw a SecurityException - // if the combined path reaches illegal parts of the filesystem - dir = Path.GetFullPath(dir); - root = Path.GetFullPath(root); - - if (!dir.StartsWith(root) && !acceptUnsafe) - throw new ConfigurationException($"Invalid models directory \"{config}\"."); - - return dir; + throw new ConfigurationException($"Invalid models directory \"{config}\"."); } - if (acceptUnsafe) - return Path.GetFullPath(config); - - throw new ConfigurationException($"Invalid models directory \"{config}\"."); + return dir; } + + if (acceptUnsafe) + { + return Path.GetFullPath(config); + } + + throw new ConfigurationException($"Invalid models directory \"{config}\"."); } } diff --git a/src/Umbraco.Core/Configuration/ModelsMode.cs b/src/Umbraco.Core/Configuration/ModelsMode.cs index 064e035892..9e76710e2b 100644 --- a/src/Umbraco.Core/Configuration/ModelsMode.cs +++ b/src/Umbraco.Core/Configuration/ModelsMode.cs @@ -1,39 +1,42 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Defines the models generation modes. +/// +public enum ModelsMode { /// - /// Defines the models generation modes. + /// Do not generate strongly typed models. /// - public enum ModelsMode - { - /// - /// Do not generate strongly typed models. - /// - /// - /// This means that only IPublishedContent instances will be used. - /// - Nothing = 0, + /// + /// This means that only IPublishedContent instances will be used. + /// + Nothing = 0, - /// - /// Generate models in memory. - /// When: a content type change occurs. - /// - /// The app does not restart. Models are available in views exclusively. - InMemoryAuto, + /// + /// Generate models in memory. + /// When: a content type change occurs. + /// + /// The app does not restart. Models are available in views exclusively. + InMemoryAuto, - /// - /// Generate models as *.cs files. - /// When: generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does not restart. - /// Models are not compiled and thus are not available to the project. - SourceCodeManual, + /// + /// Generate models as *.cs files. + /// When: generation is triggered. + /// + /// + /// Generation can be triggered from the dashboard. The app does not restart. + /// Models are not compiled and thus are not available to the project. + /// + SourceCodeManual, - /// - /// Generate models as *.cs files. - /// When: a content type change occurs, or generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does not restart. - /// Models are not compiled and thus are not available to the project. - SourceCodeAuto - } + /// + /// Generate models as *.cs files. + /// When: a content type change occurs, or generation is triggered. + /// + /// + /// Generation can be triggered from the dashboard. The app does not restart. + /// Models are not compiled and thus are not available to the project. + /// + SourceCodeAuto, } diff --git a/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs b/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs index f27d54b55d..52256a29f0 100644 --- a/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs +++ b/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs @@ -1,28 +1,27 @@ using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extensions for the enumeration. +/// +public static class ModelsModeExtensions { /// - /// Provides extensions for the enumeration. + /// Gets a value indicating whether the mode is *Auto. /// - public static class ModelsModeExtensions - { - /// - /// Gets a value indicating whether the mode is *Auto. - /// - public static bool IsAuto(this ModelsMode modelsMode) - => modelsMode == ModelsMode.InMemoryAuto || modelsMode == ModelsMode.SourceCodeAuto; + public static bool IsAuto(this ModelsMode modelsMode) + => modelsMode == ModelsMode.InMemoryAuto || modelsMode == ModelsMode.SourceCodeAuto; - /// - /// Gets a value indicating whether the mode is *Auto but not InMemory. - /// - public static bool IsAutoNotInMemory(this ModelsMode modelsMode) - => modelsMode == ModelsMode.SourceCodeAuto; + /// + /// Gets a value indicating whether the mode is *Auto but not InMemory. + /// + public static bool IsAutoNotInMemory(this ModelsMode modelsMode) + => modelsMode == ModelsMode.SourceCodeAuto; - /// - /// Gets a value indicating whether the mode supports explicit manual generation. - /// - public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) - => modelsMode == ModelsMode.SourceCodeManual || modelsMode == ModelsMode.SourceCodeAuto; - } + /// + /// Gets a value indicating whether the mode supports explicit manual generation. + /// + public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) + => modelsMode == ModelsMode.SourceCodeManual || modelsMode == ModelsMode.SourceCodeAuto; } diff --git a/src/Umbraco.Core/Configuration/PasswordConfiguration.cs b/src/Umbraco.Core/Configuration/PasswordConfiguration.cs index 506821df6d..4c74720860 100644 --- a/src/Umbraco.Core/Configuration/PasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/PasswordConfiguration.cs @@ -1,37 +1,34 @@ -using System; +namespace Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Configuration +public abstract class PasswordConfiguration : IPasswordConfiguration { - public abstract class PasswordConfiguration : IPasswordConfiguration + protected PasswordConfiguration(IPasswordConfiguration configSettings) { - protected PasswordConfiguration(IPasswordConfiguration configSettings) + if (configSettings == null) { - if (configSettings == null) - { - throw new ArgumentNullException(nameof(configSettings)); - } - - RequiredLength = configSettings.RequiredLength; - RequireNonLetterOrDigit = configSettings.RequireNonLetterOrDigit; - RequireDigit = configSettings.RequireDigit; - RequireLowercase = configSettings.RequireLowercase; - RequireUppercase = configSettings.RequireUppercase; - HashAlgorithmType = configSettings.HashAlgorithmType; - MaxFailedAccessAttemptsBeforeLockout = configSettings.MaxFailedAccessAttemptsBeforeLockout; + throw new ArgumentNullException(nameof(configSettings)); } - public int RequiredLength { get; } - - public bool RequireNonLetterOrDigit { get; } - - public bool RequireDigit { get; } - - public bool RequireLowercase { get; } - - public bool RequireUppercase { get; } - - public string HashAlgorithmType { get; } - - public int MaxFailedAccessAttemptsBeforeLockout { get; } + RequiredLength = configSettings.RequiredLength; + RequireNonLetterOrDigit = configSettings.RequireNonLetterOrDigit; + RequireDigit = configSettings.RequireDigit; + RequireLowercase = configSettings.RequireLowercase; + RequireUppercase = configSettings.RequireUppercase; + HashAlgorithmType = configSettings.HashAlgorithmType; + MaxFailedAccessAttemptsBeforeLockout = configSettings.MaxFailedAccessAttemptsBeforeLockout; } + + public int RequiredLength { get; } + + public bool RequireNonLetterOrDigit { get; } + + public bool RequireDigit { get; } + + public bool RequireLowercase { get; } + + public bool RequireUppercase { get; } + + public string HashAlgorithmType { get; } + + public int MaxFailedAccessAttemptsBeforeLockout { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs index b8049fe650..8740b81cb5 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs @@ -1,41 +1,38 @@ -using System.Collections.Generic; -using Umbraco.Cms.Core.Configuration.Models; +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +public class CharacterReplacementEqualityComparer : IEqualityComparer { - public class CharacterReplacementEqualityComparer : IEqualityComparer + public bool Equals(IChar? x, IChar? y) { - public bool Equals(IChar? x, IChar? y) + if (ReferenceEquals(x, y)) { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null) - { - return false; - } - - if (y is null) - { - return false; - } - - if (x.GetType() != y.GetType()) - { - return false; - } - - return x.Char == y.Char && x.Replacement == y.Replacement; + return true; } - public int GetHashCode(IChar obj) + if (x is null) { - unchecked - { - return ((obj.Char != null ? obj.Char.GetHashCode() : 0) * 397) ^ (obj.Replacement != null ? obj.Replacement.GetHashCode() : 0); - } + return false; + } + + if (y is null) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return x.Char == y.Char && x.Replacement == y.Replacement; + } + + public int GetHashCode(IChar obj) + { + unchecked + { + return ((obj.Char != null ? obj.Char.GetHashCode() : 0) * 397) ^ + (obj.Replacement != null ? obj.Replacement.GetHashCode() : 0); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs index 61e840245c..a2ba30b776 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings -{ - public interface IChar - { - string Char { get; } +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; - string Replacement { get; } - } +public interface IChar +{ + string Char { get; } + + string Replacement { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs index c7d91a6d0a..f6431dd77a 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IImagingAutoFillUploadField.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; + +public interface IImagingAutoFillUploadField { - public interface IImagingAutoFillUploadField - { - /// - /// Allow setting internally so we can create a default - /// - string Alias { get; } + /// + /// Allow setting internally so we can create a default + /// + string Alias { get; } - string WidthFieldAlias { get; } + string WidthFieldAlias { get; } - string HeightFieldAlias { get; } + string HeightFieldAlias { get; } - string LengthFieldAlias { get; } + string LengthFieldAlias { get; } - string ExtensionFieldAlias { get; } - } + string ExtensionFieldAlias { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IPasswordConfigurationSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IPasswordConfigurationSection.cs index d79d8940c3..7a309d6fe3 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IPasswordConfigurationSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IPasswordConfigurationSection.cs @@ -1,21 +1,20 @@ -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; + +public interface IPasswordConfigurationSection : IUmbracoConfigurationSection { - public interface IPasswordConfigurationSection : IUmbracoConfigurationSection - { - int RequiredLength { get; } + int RequiredLength { get; } - bool RequireNonLetterOrDigit { get; } + bool RequireNonLetterOrDigit { get; } - bool RequireDigit { get; } + bool RequireDigit { get; } - bool RequireLowercase { get; } + bool RequireLowercase { get; } - bool RequireUppercase { get; } + bool RequireUppercase { get; } - bool UseLegacyEncoding { get; } + bool UseLegacyEncoding { get; } - string HashAlgorithmType { get; } + string HashAlgorithmType { get; } - int MaxFailedAccessAttemptsBeforeLockout { get; } - } + int MaxFailedAccessAttemptsBeforeLockout { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ITypeFinderConfig.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ITypeFinderConfig.cs index 903f21f21a..1dfde6414f 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ITypeFinderConfig.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ITypeFinderConfig.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +public interface ITypeFinderConfig { - public interface ITypeFinderConfig - { - IEnumerable AssembliesAcceptingLoadExceptions { get; } - } + IEnumerable AssembliesAcceptingLoadExceptions { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 4b4ad87801..9664d7cb73 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -1,68 +1,72 @@ -using System; using System.Reflection; using Umbraco.Cms.Core.Semver; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Represents the version of the executing code. +/// +public class UmbracoVersion : IUmbracoVersion { - /// - /// Represents the version of the executing code. - /// - public class UmbracoVersion : IUmbracoVersion + public UmbracoVersion() { - public UmbracoVersion() - { - var umbracoCoreAssembly = typeof(SemVersion).Assembly; + Assembly umbracoCoreAssembly = typeof(SemVersion).Assembly; - // gets the value indicated by the AssemblyVersion attribute - AssemblyVersion = umbracoCoreAssembly.GetName().Version; + // gets the value indicated by the AssemblyVersion attribute + AssemblyVersion = umbracoCoreAssembly.GetName().Version; - // gets the value indicated by the AssemblyFileVersion attribute - AssemblyFileVersion = System.Version.Parse(umbracoCoreAssembly.GetCustomAttribute()?.Version ?? string.Empty); + // gets the value indicated by the AssemblyFileVersion attribute + AssemblyFileVersion = + Version.Parse(umbracoCoreAssembly.GetCustomAttribute()?.Version ?? + string.Empty); - // gets the value indicated by the AssemblyInformationalVersion attribute - // this is the true semantic version of the Umbraco Cms - SemanticVersion = SemVersion.Parse(umbracoCoreAssembly.GetCustomAttribute()?.InformationalVersion ?? string.Empty); + // gets the value indicated by the AssemblyInformationalVersion attribute + // this is the true semantic version of the Umbraco Cms + SemanticVersion = + SemVersion.Parse(umbracoCoreAssembly.GetCustomAttribute() + ?.InformationalVersion ?? string.Empty); - // gets the non-semantic version - Version = SemanticVersion.GetVersion(3); - } - - /// - /// Gets the non-semantic version of the Umbraco code. - /// - public Version Version { get; } - - /// - /// Gets the semantic version comments of the Umbraco code. - /// - public string Comment => SemanticVersion.Prerelease; - - /// - /// Gets the assembly version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// Is the one that the CLR checks for compatibility. Therefore, it changes only on - /// hard-breaking changes (for instance, on new major versions). - /// - public Version? AssemblyVersion { get; } - - /// - /// Gets the assembly file version of the Umbraco code. - /// - /// - /// The assembly version is the value of the . - /// - public Version? AssemblyFileVersion { get; } - - /// - /// Gets the semantic version of the Umbraco code. - /// - /// - /// The semantic version is the value of the . - /// It is the full version of Umbraco, including comments. - /// - public SemVersion SemanticVersion { get; } + // gets the non-semantic version + Version = SemanticVersion.GetVersion(3); } + + /// + /// Gets the non-semantic version of the Umbraco code. + /// + public Version Version { get; } + + /// + /// Gets the semantic version comments of the Umbraco code. + /// + public string Comment => SemanticVersion.Prerelease; + + /// + /// Gets the assembly version of the Umbraco code. + /// + /// + /// The assembly version is the value of the . + /// + /// Is the one that the CLR checks for compatibility. Therefore, it changes only on + /// hard-breaking changes (for instance, on new major versions). + /// + /// + public Version? AssemblyVersion { get; } + + /// + /// Gets the assembly file version of the Umbraco code. + /// + /// + /// The assembly version is the value of the . + /// + public Version? AssemblyFileVersion { get; } + + /// + /// Gets the semantic version of the Umbraco code. + /// + /// + /// The semantic version is the value of the . + /// It is the full version of Umbraco, including comments. + /// + public SemVersion SemanticVersion { get; } } diff --git a/src/Umbraco.Core/Configuration/UserPasswordConfiguration.cs b/src/Umbraco.Core/Configuration/UserPasswordConfiguration.cs index 6c30fbba71..47b950de9c 100644 --- a/src/Umbraco.Core/Configuration/UserPasswordConfiguration.cs +++ b/src/Umbraco.Core/Configuration/UserPasswordConfiguration.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +/// +/// The password configuration for back office users +/// +public class UserPasswordConfiguration : PasswordConfiguration, IUserPasswordConfiguration { - /// - /// The password configuration for back office users - /// - public class UserPasswordConfiguration : PasswordConfiguration, IUserPasswordConfiguration + public UserPasswordConfiguration(IUserPasswordConfiguration configSettings) + : base(configSettings) { - public UserPasswordConfiguration(IUserPasswordConfiguration configSettings) - : base(configSettings) - { - } } } diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index da945731af..dc36715585 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -1,162 +1,161 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the alias identifiers for Umbraco's core application sections. + /// + public static class Applications { /// - /// Defines the alias identifiers for Umbraco's core application sections. + /// Application alias for the content section. /// - public static class Applications - { - /// - /// Application alias for the content section. - /// - public const string Content = "content"; - - /// - /// Application alias for the packages section. - /// - public const string Packages = "packages"; - - /// - /// Application alias for the media section. - /// - public const string Media = "media"; - - /// - /// Application alias for the members section. - /// - public const string Members = "member"; - - /// - /// Application alias for the settings section. - /// - public const string Settings = "settings"; - - /// - /// Application alias for the translation section. - /// - public const string Translation = "translation"; - - /// - /// Application alias for the users section. - /// - public const string Users = "users"; - - /// - /// Application alias for the forms section. - /// - public const string Forms = "forms"; - } + public const string Content = "content"; /// - /// Defines the alias identifiers for Umbraco's core trees. + /// Application alias for the packages section. /// - public static class Trees + public const string Packages = "packages"; + + /// + /// Application alias for the media section. + /// + public const string Media = "media"; + + /// + /// Application alias for the members section. + /// + public const string Members = "member"; + + /// + /// Application alias for the settings section. + /// + public const string Settings = "settings"; + + /// + /// Application alias for the translation section. + /// + public const string Translation = "translation"; + + /// + /// Application alias for the users section. + /// + public const string Users = "users"; + + /// + /// Application alias for the forms section. + /// + public const string Forms = "forms"; + } + + /// + /// Defines the alias identifiers for Umbraco's core trees. + /// + public static class Trees + { + /// + /// alias for the content tree. + /// + public const string Content = "content"; + + /// + /// alias for the content blueprint tree. + /// + public const string ContentBlueprints = "contentBlueprints"; + + /// + /// alias for the member tree. + /// + public const string Members = "member"; + + /// + /// alias for the media tree. + /// + public const string Media = "media"; + + /// + /// alias for the macro tree. + /// + public const string Macros = "macros"; + + /// + /// alias for the datatype tree. + /// + public const string DataTypes = "dataTypes"; + + /// + /// alias for the packages tree + /// + public const string Packages = "packages"; + + /// + /// alias for the dictionary tree. + /// + public const string Dictionary = "dictionary"; + + public const string Stylesheets = "stylesheets"; + + /// + /// alias for the document type tree. + /// + public const string DocumentTypes = "documentTypes"; + + /// + /// alias for the media type tree. + /// + public const string MediaTypes = "mediaTypes"; + + /// + /// alias for the member type tree. + /// + public const string MemberTypes = "memberTypes"; + + /// + /// alias for the member group tree. + /// + public const string MemberGroups = "memberGroups"; + + /// + /// alias for the template tree. + /// + public const string Templates = "templates"; + + public const string RelationTypes = "relationTypes"; + + public const string Languages = "languages"; + + /// + /// alias for the user types tree. + /// + public const string UserTypes = "userTypes"; + + /// + /// alias for the user permissions tree. + /// + public const string UserPermissions = "userPermissions"; + + /// + /// alias for the users tree. + /// + public const string Users = "users"; + + public const string Scripts = "scripts"; + + public const string PartialViews = "partialViews"; + + public const string PartialViewMacros = "partialViewMacros"; + + public const string LogViewer = "logViewer"; + + public static class Groups { - /// - /// alias for the content tree. - /// - public const string Content = "content"; + public const string Settings = "settingsGroup"; - /// - /// alias for the content blueprint tree. - /// - public const string ContentBlueprints = "contentBlueprints"; + public const string Templating = "templatingGroup"; - /// - /// alias for the member tree. - /// - public const string Members = "member"; - - /// - /// alias for the media tree. - /// - public const string Media = "media"; - - /// - /// alias for the macro tree. - /// - public const string Macros = "macros"; - - /// - /// alias for the datatype tree. - /// - public const string DataTypes = "dataTypes"; - - /// - /// alias for the packages tree - /// - public const string Packages = "packages"; - - /// - /// alias for the dictionary tree. - /// - public const string Dictionary = "dictionary"; - - public const string Stylesheets = "stylesheets"; - - /// - /// alias for the document type tree. - /// - public const string DocumentTypes = "documentTypes"; - - /// - /// alias for the media type tree. - /// - public const string MediaTypes = "mediaTypes"; - - /// - /// alias for the member type tree. - /// - public const string MemberTypes = "memberTypes"; - - /// - /// alias for the member group tree. - /// - public const string MemberGroups = "memberGroups"; - - /// - /// alias for the template tree. - /// - public const string Templates = "templates"; - - public const string RelationTypes = "relationTypes"; - - public const string Languages = "languages"; - - /// - /// alias for the user types tree. - /// - public const string UserTypes = "userTypes"; - - /// - /// alias for the user permissions tree. - /// - public const string UserPermissions = "userPermissions"; - - /// - /// alias for the users tree. - /// - public const string Users = "users"; - - public const string Scripts = "scripts"; - - public const string PartialViews = "partialViews"; - - public const string PartialViewMacros = "partialViewMacros"; - - public const string LogViewer = "logViewer"; - - public static class Groups - { - public const string Settings = "settingsGroup"; - - public const string Templating = "templatingGroup"; - - public const string ThirdParty = "thirdPartyGroup"; - } - - // TODO: Fill in the rest! + public const string ThirdParty = "thirdPartyGroup"; } + + // TODO: Fill in the rest! } } diff --git a/src/Umbraco.Core/Constants-Audit.cs b/src/Umbraco.Core/Constants-Audit.cs index 54c51c95ff..f795a25974 100644 --- a/src/Umbraco.Core/Constants-Audit.cs +++ b/src/Umbraco.Core/Constants-Audit.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core; +namespace Umbraco.Cms.Core; public static partial class Constants { diff --git a/src/Umbraco.Core/Constants-CharArrays.cs b/src/Umbraco.Core/Constants-CharArrays.cs index 4be5ecba04..832cac00e6 100644 --- a/src/Umbraco.Core/Constants-CharArrays.cs +++ b/src/Umbraco.Core/Constants-CharArrays.cs @@ -1,138 +1,135 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Char Arrays to avoid allocations + /// + public static class CharArrays { /// - /// Char Arrays to avoid allocations + /// Char array containing only / /// - public static class CharArrays - { - /// - /// Char array containing only / - /// - public static readonly char[] ForwardSlash = new char[] { '/' }; + public static readonly char[] ForwardSlash = { '/' }; - /// - /// Char array containing only \ - /// - public static readonly char[] Backslash = new char[] { '\\' }; + /// + /// Char array containing only \ + /// + public static readonly char[] Backslash = { '\\' }; - /// - /// Char array containing only ' - /// - public static readonly char[] SingleQuote = new char[] { '\'' }; + /// + /// Char array containing only ' + /// + public static readonly char[] SingleQuote = { '\'' }; - /// - /// Char array containing only " - /// - public static readonly char[] DoubleQuote = new char[] { '\"' }; + /// + /// Char array containing only " + /// + public static readonly char[] DoubleQuote = { '\"' }; + /// + /// Char array containing ' " + /// + public static readonly char[] DoubleQuoteSingleQuote = { '\"', '\'' }; - /// - /// Char array containing ' " - /// - public static readonly char[] DoubleQuoteSingleQuote = new char[] { '\"', '\'' }; + /// + /// Char array containing only _ + /// + public static readonly char[] Underscore = { '_' }; - /// - /// Char array containing only _ - /// - public static readonly char[] Underscore = new char[] { '_' }; + /// + /// Char array containing \n \r + /// + public static readonly char[] LineFeedCarriageReturn = { '\n', '\r' }; - /// - /// Char array containing \n \r - /// - public static readonly char[] LineFeedCarriageReturn = new char[] { '\n', '\r' }; + /// + /// Char array containing \n + /// + public static readonly char[] LineFeed = { '\n' }; + /// + /// Char array containing only , + /// + public static readonly char[] Comma = { ',' }; - /// - /// Char array containing \n - /// - public static readonly char[] LineFeed = new char[] { '\n' }; + /// + /// Char array containing only & + /// + public static readonly char[] Ampersand = { '&' }; - /// - /// Char array containing only , - /// - public static readonly char[] Comma = new char[] { ',' }; + /// + /// Char array containing only \0 + /// + public static readonly char[] NullTerminator = { '\0' }; - /// - /// Char array containing only & - /// - public static readonly char[] Ampersand = new char[] { '&' }; + /// + /// Char array containing only . + /// + public static readonly char[] Period = { '.' }; - /// - /// Char array containing only \0 - /// - public static readonly char[] NullTerminator = new char[] { '\0' }; + /// + /// Char array containing only ~ + /// + public static readonly char[] Tilde = { '~' }; - /// - /// Char array containing only . - /// - public static readonly char[] Period = new char[] { '.' }; + /// + /// Char array containing ~ / + /// + public static readonly char[] TildeForwardSlash = { '~', '/' }; - /// - /// Char array containing only ~ - /// - public static readonly char[] Tilde = new char[] { '~' }; - /// - /// Char array containing ~ / - /// - public static readonly char[] TildeForwardSlash = new char[] { '~', '/' }; + /// + /// Char array containing ~ / \ + /// + public static readonly char[] TildeForwardSlashBackSlash = { '~', '/', '\\' }; + /// + /// Char array containing only ? + /// + public static readonly char[] QuestionMark = { '?' }; - /// - /// Char array containing ~ / \ - /// - public static readonly char[] TildeForwardSlashBackSlash = new char[] { '~', '/', '\\' }; + /// + /// Char array containing ? & + /// + public static readonly char[] QuestionMarkAmpersand = { '?', '&' }; - /// - /// Char array containing only ? - /// - public static readonly char[] QuestionMark = new char[] { '?' }; + /// + /// Char array containing XML 1.1 whitespace chars + /// + public static readonly char[] XmlWhitespaceChars = { ' ', '\t', '\r', '\n' }; - /// - /// Char array containing ? & - /// - public static readonly char[] QuestionMarkAmpersand = new char[] { '?', '&' }; + /// + /// Char array containing only the Space char + /// + public static readonly char[] Space = { ' ' }; - /// - /// Char array containing XML 1.1 whitespace chars - /// - public static readonly char[] XmlWhitespaceChars = new char[] { ' ', '\t', '\r', '\n' }; + /// + /// Char array containing only ; + /// + public static readonly char[] Semicolon = { ';' }; - /// - /// Char array containing only the Space char - /// - public static readonly char[] Space = new char[] { ' ' }; + /// + /// Char array containing a comma and a space + /// + public static readonly char[] CommaSpace = { ',', ' ' }; - /// - /// Char array containing only ; - /// - public static readonly char[] Semicolon = new char[] { ';' }; + /// + /// Char array containing _ - + /// + public static readonly char[] UnderscoreDash = { '_', '-' }; - /// - /// Char array containing a comma and a space - /// - public static readonly char[] CommaSpace = new char[] { ',', ' ' }; + /// + /// Char array containing = + /// + public static readonly char[] EqualsChar = { '=' }; - /// - /// Char array containing _ - - /// - public static readonly char[] UnderscoreDash = new char[] { '_', '-' }; + /// + /// Char array containing > + /// + public static readonly char[] GreaterThan = { '>' }; - /// - /// Char array containing = - /// - public static readonly char[] EqualsChar = new char[] { '=' }; - - /// - /// Char array containing > - /// - public static readonly char[] GreaterThan = new char[] { '>' }; - - /// - /// Char array containing | - /// - public static readonly char[] VerticalTab = new char[] { '|' }; - } + /// + /// Char array containing | + /// + public static readonly char[] VerticalTab = { '|' }; } } diff --git a/src/Umbraco.Core/Constants-Composing.cs b/src/Umbraco.Core/Constants-Composing.cs index 747a74b8d8..e55c32d01a 100644 --- a/src/Umbraco.Core/Constants-Composing.cs +++ b/src/Umbraco.Core/Constants-Composing.cs @@ -1,25 +1,19 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines constants. +/// +public static partial class Constants { /// - /// Defines constants. + /// Defines constants for composition. /// - public static partial class Constants + public static class Composing { - /// - /// Defines constants for composition. - /// - public static class Composing + public static readonly string[] UmbracoCoreAssemblyNames = { - public static readonly string[] UmbracoCoreAssemblyNames = new[] - { - "Umbraco.Core", - "Umbraco.Infrastructure", - "Umbraco.PublishedCache.NuCache", - "Umbraco.Examine.Lucene", - "Umbraco.Web.Common", - "Umbraco.Web.BackOffice", - "Umbraco.Web.Website", - }; - } + "Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", "Umbraco.Examine.Lucene", + "Umbraco.Web.Common", "Umbraco.Web.BackOffice", "Umbraco.Web.Website", + }; } } diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index fd63ab3853..3f7f3188a9 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -1,77 +1,80 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Configuration { - public static class Configuration - { - /// - /// Case insensitive prefix for all configurations - /// - /// - /// ":" is used as marker for nested objects in json. E.g. "Umbraco:CMS:" = {"Umbraco":{"CMS":{....}} - /// - public const string ConfigPrefix = "Umbraco:CMS:"; - public const string ConfigContentPrefix = ConfigPrefix + "Content:"; - public const string ConfigContentNotificationsPrefix = ConfigContentPrefix + "Notifications:"; - public const string ConfigCorePrefix = ConfigPrefix + "Core:"; - public const string ConfigCustomErrorsPrefix = ConfigPrefix + "CustomErrors:"; - public const string ConfigGlobalPrefix = ConfigPrefix + "Global:"; - public const string ConfigGlobalId = ConfigGlobalPrefix + "Id"; - public const string ConfigGlobalDistributedLockingMechanism = ConfigGlobalPrefix + "DistributedLockingMechanism"; - public const string ConfigHostingPrefix = ConfigPrefix + "Hosting:"; - public const string ConfigModelsBuilderPrefix = ConfigPrefix + "ModelsBuilder:"; - public const string ConfigSecurityPrefix = ConfigPrefix + "Security:"; - public const string ConfigContentNotificationsEmail = ConfigContentNotificationsPrefix + "Email"; - public const string ConfigContentMacroErrors = ConfigContentPrefix + "MacroErrors"; - public const string ConfigGlobalUseHttps = ConfigGlobalPrefix + "UseHttps"; - public const string ConfigHostingDebug = ConfigHostingPrefix + "Debug"; - public const string ConfigCustomErrorsMode = ConfigCustomErrorsPrefix + "Mode"; - public const string ConfigActiveDirectory = ConfigPrefix + "ActiveDirectory"; - public const string ConfigLegacyPasswordMigration = ConfigPrefix + "LegacyPasswordMigration"; - public const string ConfigContent = ConfigPrefix + "Content"; - public const string ConfigCoreDebug = ConfigCorePrefix + "Debug"; - public const string ConfigExceptionFilter = ConfigPrefix + "ExceptionFilter"; - public const string ConfigGlobal = ConfigPrefix + "Global"; - public const string ConfigUnattended = ConfigPrefix + "Unattended"; - public const string ConfigHealthChecks = ConfigPrefix + "HealthChecks"; - public const string ConfigHosting = ConfigPrefix + "Hosting"; - public const string ConfigImaging = ConfigPrefix + "Imaging"; - public const string ConfigExamine = ConfigPrefix + "Examine"; - public const string ConfigKeepAlive = ConfigPrefix + "KeepAlive"; - public const string ConfigLogging = ConfigPrefix + "Logging"; - public const string ConfigMemberPassword = ConfigPrefix + "Security:MemberPassword"; - public const string ConfigModelsBuilder = ConfigPrefix + "ModelsBuilder"; - public const string ConfigNuCache = ConfigPrefix + "NuCache"; - public const string ConfigPlugins = ConfigPrefix + "Plugins"; - public const string ConfigRequestHandler = ConfigPrefix + "RequestHandler"; - public const string ConfigRuntime = ConfigPrefix + "Runtime"; - public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; - public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; - public const string ConfigSecurity = ConfigPrefix + "Security"; - public const string ConfigBasicAuth = ConfigPrefix + "BasicAuth"; - public const string ConfigTours = ConfigPrefix + "Tours"; - public const string ConfigTypeFinder = ConfigPrefix + "TypeFinder"; - public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; - public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; - public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; - public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; - public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; - public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; - public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; + /// + /// Case insensitive prefix for all configurations + /// + /// + /// ":" is used as marker for nested objects in json. E.g. "Umbraco:CMS:" = {"Umbraco":{"CMS":{....}} + /// + public const string ConfigPrefix = "Umbraco:CMS:"; + + public const string ConfigContentPrefix = ConfigPrefix + "Content:"; + public const string ConfigContentNotificationsPrefix = ConfigContentPrefix + "Notifications:"; + public const string ConfigCorePrefix = ConfigPrefix + "Core:"; + public const string ConfigCustomErrorsPrefix = ConfigPrefix + "CustomErrors:"; + public const string ConfigGlobalPrefix = ConfigPrefix + "Global:"; + public const string ConfigGlobalId = ConfigGlobalPrefix + "Id"; + + public const string ConfigGlobalDistributedLockingMechanism = + ConfigGlobalPrefix + "DistributedLockingMechanism"; + + public const string ConfigHostingPrefix = ConfigPrefix + "Hosting:"; + public const string ConfigModelsBuilderPrefix = ConfigPrefix + "ModelsBuilder:"; + public const string ConfigSecurityPrefix = ConfigPrefix + "Security:"; + public const string ConfigContentNotificationsEmail = ConfigContentNotificationsPrefix + "Email"; + public const string ConfigContentMacroErrors = ConfigContentPrefix + "MacroErrors"; + public const string ConfigGlobalUseHttps = ConfigGlobalPrefix + "UseHttps"; + public const string ConfigHostingDebug = ConfigHostingPrefix + "Debug"; + public const string ConfigCustomErrorsMode = ConfigCustomErrorsPrefix + "Mode"; + public const string ConfigActiveDirectory = ConfigPrefix + "ActiveDirectory"; + public const string ConfigLegacyPasswordMigration = ConfigPrefix + "LegacyPasswordMigration"; + public const string ConfigContent = ConfigPrefix + "Content"; + public const string ConfigCoreDebug = ConfigCorePrefix + "Debug"; + public const string ConfigExceptionFilter = ConfigPrefix + "ExceptionFilter"; + public const string ConfigGlobal = ConfigPrefix + "Global"; + public const string ConfigUnattended = ConfigPrefix + "Unattended"; + public const string ConfigHealthChecks = ConfigPrefix + "HealthChecks"; + public const string ConfigHosting = ConfigPrefix + "Hosting"; + public const string ConfigImaging = ConfigPrefix + "Imaging"; + public const string ConfigExamine = ConfigPrefix + "Examine"; + public const string ConfigKeepAlive = ConfigPrefix + "KeepAlive"; + public const string ConfigLogging = ConfigPrefix + "Logging"; + public const string ConfigMemberPassword = ConfigPrefix + "Security:MemberPassword"; + public const string ConfigModelsBuilder = ConfigPrefix + "ModelsBuilder"; + public const string ConfigNuCache = ConfigPrefix + "NuCache"; + public const string ConfigPlugins = ConfigPrefix + "Plugins"; + public const string ConfigRequestHandler = ConfigPrefix + "RequestHandler"; + public const string ConfigRuntime = ConfigPrefix + "Runtime"; + public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; + public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; + public const string ConfigSecurity = ConfigPrefix + "Security"; + public const string ConfigBasicAuth = ConfigPrefix + "BasicAuth"; + public const string ConfigTours = ConfigPrefix + "Tours"; + public const string ConfigTypeFinder = ConfigPrefix + "TypeFinder"; + public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; + public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; + public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; + public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; + public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; + public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; + public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; - public static class NamedOptions + public static class NamedOptions + { + public static class InstallDefaultData { - public static class InstallDefaultData - { - public const string Languages = "Languages"; + public const string Languages = "Languages"; - public const string DataTypes = "DataTypes"; + public const string DataTypes = "DataTypes"; - public const string MediaTypes = "MediaTypes"; + public const string MediaTypes = "MediaTypes"; - public const string MemberTypes = "MemberTypes"; - } + public const string MemberTypes = "MemberTypes"; } } } diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index cb34901e6c..7b221e1435 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -1,343 +1,352 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public static partial class Constants { - public static partial class Constants + /// + /// Defines the identifiers for property-type alias conventions that are used within the Umbraco core. + /// + public static class Conventions { - /// - /// Defines the identifiers for property-type alias conventions that are used within the Umbraco core. - /// - public static class Conventions + public static class Migrations { - public static class Migrations - { - public const string UmbracoUpgradePlanName = "Umbraco.Core"; - public const string KeyValuePrefix = "Umbraco.Core.Upgrader.State+"; - public const string UmbracoUpgradePlanKey = KeyValuePrefix + UmbracoUpgradePlanName; - } + public const string UmbracoUpgradePlanName = "Umbraco.Core"; + public const string KeyValuePrefix = "Umbraco.Core.Upgrader.State+"; + public const string UmbracoUpgradePlanKey = KeyValuePrefix + UmbracoUpgradePlanName; + } - public static class PermissionCategories - { - public const string ContentCategory = "content"; - public const string AdministrationCategory = "administration"; - public const string StructureCategory = "structure"; - public const string OtherCategory = "other"; - } + public static class PermissionCategories + { + public const string ContentCategory = "content"; + public const string AdministrationCategory = "administration"; + public const string StructureCategory = "structure"; + public const string OtherCategory = "other"; + } - public static class PublicAccess - { - public const string MemberUsernameRuleType = "MemberUsername"; - public const string MemberRoleRuleType = "MemberRole"; - } + public static class PublicAccess + { + public const string MemberUsernameRuleType = "MemberUsername"; + public const string MemberRoleRuleType = "MemberRole"; + } + public static class DataTypes + { + public const string ListViewPrefix = "List View - "; + } - public static class DataTypes - { - public const string ListViewPrefix = "List View - "; - } + /// + /// Constants for Umbraco Content property aliases. + /// + public static class Content + { + /// + /// Property alias for the Content's Url (internal) redirect. + /// + public const string InternalRedirectId = "umbracoInternalRedirectId"; /// - /// Constants for Umbraco Content property aliases. + /// Property alias for the Content's navigational hide, (not actually used in core code). /// - public static class Content - { - /// - /// Property alias for the Content's Url (internal) redirect. - /// - public const string InternalRedirectId = "umbracoInternalRedirectId"; - - /// - /// Property alias for the Content's navigational hide, (not actually used in core code). - /// - public const string NaviHide = "umbracoNaviHide"; - - /// - /// Property alias for the Content's Url redirect. - /// - public const string Redirect = "umbracoRedirect"; - - /// - /// Property alias for the Content's Url alias. - /// - public const string UrlAlias = "umbracoUrlAlias"; - - /// - /// Property alias for the Content's Url name. - /// - public const string UrlName = "umbracoUrlName"; - } + public const string NaviHide = "umbracoNaviHide"; /// - /// Constants for Umbraco Media property aliases. + /// Property alias for the Content's Url redirect. /// - public static class Media - { - /// - /// Property alias for the Media's file name. - /// - public const string File = "umbracoFile"; - - /// - /// Property alias for the Media's width. - /// - public const string Width = "umbracoWidth"; - - /// - /// Property alias for the Media's height. - /// - public const string Height = "umbracoHeight"; - - /// - /// Property alias for the Media's file size (in bytes). - /// - public const string Bytes = "umbracoBytes"; - - /// - /// Property alias for the Media's file extension. - /// - public const string Extension = "umbracoExtension"; - - /// - /// The default height/width of an image file if the size can't be determined from the metadata - /// - public const int DefaultSize = 200; - } + public const string Redirect = "umbracoRedirect"; /// - /// Defines the alias identifiers for Umbraco media types. + /// Property alias for the Content's Url alias. /// - public static class MediaTypes - { - /// - /// MediaType alias for a file. - /// - public const string File = "File"; - - /// - /// MediaType alias for a folder. - /// - public const string Folder = "Folder"; - - /// - /// MediaType alias for an image. - /// - public const string Image = "Image"; - - /// - /// MediaType name for a video. - /// - public const string Video = "Video"; - - /// - /// MediaType name for an audio. - /// - public const string Audio = "Audio"; - - /// - /// MediaType name for an article. - /// - public const string Article = "Article"; - - /// - /// MediaType name for vector graphics. - /// - public const string VectorGraphics = "VectorGraphics"; - - /// - /// MediaType alias for a video. - /// - public const string VideoAlias = "umbracoMediaVideo"; - - /// - /// MediaType alias for an audio. - /// - public const string AudioAlias = "umbracoMediaAudio"; - - /// - /// MediaType alias for an article. - /// - public const string ArticleAlias = "umbracoMediaArticle"; - - /// - /// MediaType alias for vector graphics. - /// - public const string VectorGraphicsAlias = "umbracoMediaVectorGraphics"; - - /// - /// MediaType alias indicating allowing auto-selection. - /// - public const string AutoSelect = "umbracoAutoSelect"; - } + public const string UrlAlias = "umbracoUrlAlias"; /// - /// Constants for Umbraco Member property aliases. + /// Property alias for the Content's Url name. /// - public static class Member - { - /// - /// if a role starts with __umbracoRole we won't show it as it's an internal role used for public access - /// - public static readonly string InternalRolePrefix = "__umbracoRole"; + public const string UrlName = "umbracoUrlName"; + } - /// - /// Property alias for the Comments on a Member - /// - public const string Comments = "umbracoMemberComments"; - - public const string CommentsLabel = "Comments"; - - /// - /// Property alias for the Approved boolean of a Member - /// - [Obsolete("IsApproved is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] - public const string IsApproved = "umbracoMemberApproved"; - [Obsolete("Use the stateApproved translation in the user area instead, scheduled for removal in V11")] - public const string IsApprovedLabel = "Is Approved"; - - /// - /// Property alias for the Locked out boolean of a Member - /// - [Obsolete("IsLockedOut is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] - public const string IsLockedOut = "umbracoMemberLockedOut"; - [Obsolete("Use the stateLockedOut translation in the user area instead, scheduled for removal in V11")] - public const string IsLockedOutLabel = "Is Locked Out"; - - /// - /// Property alias for the last date the Member logged in - /// - [Obsolete("LastLoginDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] - public const string LastLoginDate = "umbracoMemberLastLogin"; - [Obsolete("Use the lastLogin translation in the user area instead, scheduled for removal in V11")] - public const string LastLoginDateLabel = "Last Login Date"; - - /// - /// Property alias for the last date a Member changed its password - /// - [Obsolete("LastPasswordChangeDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] - public const string LastPasswordChangeDate = "umbracoMemberLastPasswordChangeDate"; - [Obsolete("Use the lastPasswordChangeDate translation in the user area instead, scheduled for removal in V11")] - public const string LastPasswordChangeDateLabel = "Last Password Change Date"; - - /// - /// Property alias for the last date a Member was locked out - /// - [Obsolete("LastLockoutDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] - public const string LastLockoutDate = "umbracoMemberLastLockoutDate"; - [Obsolete("Use the lastLockoutDate translation in the user area instead, scheduled for removal in V11")] - public const string LastLockoutDateLabel = "Last Lockout Date"; - - /// - /// Property alias for the number of failed login attempts - /// - [Obsolete("FailedPasswordAttempts is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] - public const string FailedPasswordAttempts = "umbracoMemberFailedPasswordAttempts"; - [Obsolete("Use the failedPasswordAttempts translation in the user area instead, scheduled for removal in V11")] - public const string FailedPasswordAttemptsLabel = "Failed Password Attempts"; - - /// - /// The standard properties group alias for membership properties. - /// - public const string StandardPropertiesGroupAlias = "membership"; - - /// - /// The standard properties group name for membership properties. - /// - public const string StandardPropertiesGroupName = "Membership"; - } + /// + /// Constants for Umbraco Media property aliases. + /// + public static class Media + { + /// + /// Property alias for the Media's file name. + /// + public const string File = "umbracoFile"; /// - /// Defines the alias identifiers for Umbraco member types. + /// Property alias for the Media's width. /// - public static class MemberTypes - { - /// - /// MemberType alias for default member type. - /// - public const string DefaultAlias = "Member"; - - public const string SystemDefaultProtectType = "_umbracoSystemDefaultProtectType"; - - public const string AllMembersListId = "all-members"; - } + public const string Width = "umbracoWidth"; /// - /// Constants for Umbraco URLs/Querystrings. + /// Property alias for the Media's height. /// - public static class Url - { - /// - /// Querystring parameter name used for Umbraco's alternative template functionality. - /// - public const string AltTemplate = "altTemplate"; - } + public const string Height = "umbracoHeight"; /// - /// Defines the alias identifiers for built-in Umbraco relation types. + /// Property alias for the Media's file size (in bytes). /// - public static class RelationTypes - { - /// - /// Name for default relation type "Related Media". - /// - public const string RelatedMediaName = "Related Media"; + public const string Bytes = "umbracoBytes"; - /// - /// Alias for default relation type "Related Media" - /// - public const string RelatedMediaAlias = "umbMedia"; + /// + /// Property alias for the Media's file extension. + /// + public const string Extension = "umbracoExtension"; - /// - /// Name for default relation type "Related Document". - /// - public const string RelatedDocumentName = "Related Document"; + /// + /// The default height/width of an image file if the size can't be determined from the metadata + /// + public const int DefaultSize = 200; + } - /// - /// Alias for default relation type "Related Document" - /// - public const string RelatedDocumentAlias = "umbDocument"; + /// + /// Defines the alias identifiers for Umbraco media types. + /// + public static class MediaTypes + { + /// + /// MediaType alias for a file. + /// + public const string File = "File"; - /// - /// Name for default relation type "Relate Document On Copy". - /// - public const string RelateDocumentOnCopyName = "Relate Document On Copy"; + /// + /// MediaType alias for a folder. + /// + public const string Folder = "Folder"; - /// - /// Alias for default relation type "Relate Document On Copy". - /// - public const string RelateDocumentOnCopyAlias = "relateDocumentOnCopy"; + /// + /// MediaType alias for an image. + /// + public const string Image = "Image"; - /// - /// Name for default relation type "Relate Parent Document On Delete". - /// - public const string RelateParentDocumentOnDeleteName = "Relate Parent Document On Delete"; + /// + /// MediaType name for a video. + /// + public const string Video = "Video"; - /// - /// Alias for default relation type "Relate Parent Document On Delete". - /// - public const string RelateParentDocumentOnDeleteAlias = "relateParentDocumentOnDelete"; + /// + /// MediaType name for an audio. + /// + public const string Audio = "Audio"; - /// - /// Name for default relation type "Relate Parent Media Folder On Delete". - /// - public const string RelateParentMediaFolderOnDeleteName = "Relate Parent Media Folder On Delete"; + /// + /// MediaType name for an article. + /// + public const string Article = "Article"; - /// - /// Alias for default relation type "Relate Parent Media Folder On Delete". - /// - public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; + /// + /// MediaType name for vector graphics. + /// + public const string VectorGraphics = "VectorGraphics"; - /// - /// Returns the types of relations that are automatically tracked - /// - /// - /// Developers should not manually use these relation types since they will all be cleared whenever an entity - /// (content, media or member) is saved since they are auto-populated based on property values. - /// - public static string[] AutomaticRelationTypes { get; } = new[] { RelatedMediaAlias, RelatedDocumentAlias }; + /// + /// MediaType alias for a video. + /// + public const string VideoAlias = "umbracoMediaVideo"; - //TODO: return a list of built in types so we can use that to prevent deletion in the uI - } + /// + /// MediaType alias for an audio. + /// + public const string AudioAlias = "umbracoMediaAudio"; + /// + /// MediaType alias for an article. + /// + public const string ArticleAlias = "umbracoMediaArticle"; + + /// + /// MediaType alias for vector graphics. + /// + public const string VectorGraphicsAlias = "umbracoMediaVectorGraphics"; + + /// + /// MediaType alias indicating allowing auto-selection. + /// + public const string AutoSelect = "umbracoAutoSelect"; + } + + /// + /// Constants for Umbraco Member property aliases. + /// + public static class Member + { + /// + /// Property alias for the Comments on a Member + /// + public const string Comments = "umbracoMemberComments"; + + public const string CommentsLabel = "Comments"; + + /// + /// Property alias for the Approved boolean of a Member + /// + [Obsolete( + "IsApproved is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] + public const string IsApproved = "umbracoMemberApproved"; + + [Obsolete("Use the stateApproved translation in the user area instead, scheduled for removal in V11")] + public const string IsApprovedLabel = "Is Approved"; + + /// + /// Property alias for the Locked out boolean of a Member + /// + [Obsolete( + "IsLockedOut is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] + public const string IsLockedOut = "umbracoMemberLockedOut"; + + [Obsolete("Use the stateLockedOut translation in the user area instead, scheduled for removal in V11")] + public const string IsLockedOutLabel = "Is Locked Out"; + + /// + /// Property alias for the last date the Member logged in + /// + [Obsolete( + "LastLoginDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] + public const string LastLoginDate = "umbracoMemberLastLogin"; + + [Obsolete("Use the lastLogin translation in the user area instead, scheduled for removal in V11")] + public const string LastLoginDateLabel = "Last Login Date"; + + /// + /// Property alias for the last date a Member changed its password + /// + [Obsolete( + "LastPasswordChangeDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] + public const string LastPasswordChangeDate = "umbracoMemberLastPasswordChangeDate"; + + [Obsolete( + "Use the lastPasswordChangeDate translation in the user area instead, scheduled for removal in V11")] + public const string LastPasswordChangeDateLabel = "Last Password Change Date"; + + /// + /// Property alias for the last date a Member was locked out + /// + [Obsolete( + "LastLockoutDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] + public const string LastLockoutDate = "umbracoMemberLastLockoutDate"; + + [Obsolete("Use the lastLockoutDate translation in the user area instead, scheduled for removal in V11")] + public const string LastLockoutDateLabel = "Last Lockout Date"; + + /// + /// Property alias for the number of failed login attempts + /// + [Obsolete( + "FailedPasswordAttempts is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")] + public const string FailedPasswordAttempts = "umbracoMemberFailedPasswordAttempts"; + + [Obsolete( + "Use the failedPasswordAttempts translation in the user area instead, scheduled for removal in V11")] + public const string FailedPasswordAttemptsLabel = "Failed Password Attempts"; + + /// + /// The standard properties group alias for membership properties. + /// + public const string StandardPropertiesGroupAlias = "membership"; + + /// + /// The standard properties group name for membership properties. + /// + public const string StandardPropertiesGroupName = "Membership"; + + /// + /// if a role starts with __umbracoRole we won't show it as it's an internal role used for public access + /// + public static readonly string InternalRolePrefix = "__umbracoRole"; + } + + /// + /// Defines the alias identifiers for Umbraco member types. + /// + public static class MemberTypes + { + /// + /// MemberType alias for default member type. + /// + public const string DefaultAlias = "Member"; + + public const string SystemDefaultProtectType = "_umbracoSystemDefaultProtectType"; + + public const string AllMembersListId = "all-members"; + } + + /// + /// Constants for Umbraco URLs/Querystrings. + /// + public static class Url + { + /// + /// Querystring parameter name used for Umbraco's alternative template functionality. + /// + public const string AltTemplate = "altTemplate"; + } + + /// + /// Defines the alias identifiers for built-in Umbraco relation types. + /// + public static class RelationTypes + { + /// + /// Name for default relation type "Related Media". + /// + public const string RelatedMediaName = "Related Media"; + + /// + /// Alias for default relation type "Related Media" + /// + public const string RelatedMediaAlias = "umbMedia"; + + /// + /// Name for default relation type "Related Document". + /// + public const string RelatedDocumentName = "Related Document"; + + /// + /// Alias for default relation type "Related Document" + /// + public const string RelatedDocumentAlias = "umbDocument"; + + /// + /// Name for default relation type "Relate Document On Copy". + /// + public const string RelateDocumentOnCopyName = "Relate Document On Copy"; + + /// + /// Alias for default relation type "Relate Document On Copy". + /// + public const string RelateDocumentOnCopyAlias = "relateDocumentOnCopy"; + + /// + /// Name for default relation type "Relate Parent Document On Delete". + /// + public const string RelateParentDocumentOnDeleteName = "Relate Parent Document On Delete"; + + /// + /// Alias for default relation type "Relate Parent Document On Delete". + /// + public const string RelateParentDocumentOnDeleteAlias = "relateParentDocumentOnDelete"; + + /// + /// Name for default relation type "Relate Parent Media Folder On Delete". + /// + public const string RelateParentMediaFolderOnDeleteName = "Relate Parent Media Folder On Delete"; + + /// + /// Alias for default relation type "Relate Parent Media Folder On Delete". + /// + public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; + + /// + /// Returns the types of relations that are automatically tracked + /// + /// + /// Developers should not manually use these relation types since they will all be cleared whenever an entity + /// (content, media or member) is saved since they are auto-populated based on property values. + /// + public static string[] AutomaticRelationTypes { get; } = { RelatedMediaAlias, RelatedDocumentAlias }; + + // TODO: return a list of built in types so we can use that to prevent deletion in the uI } } } diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index ba8827cd26..a3e2dbc4c5 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -1,461 +1,428 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public static partial class Constants { - public static partial class Constants + public static class DataTypes { - public static class DataTypes + // NOTE: unfortunately due to backwards compat we can't move/rename these, with the addition of the GUID + // constants, it would make more sense to have these suffixed with "ID" or in a Subclass called "INT", for + // now all we can do is make a subclass called Guids to put the GUID IDs. + public const int LabelString = System.DefaultLabelDataTypeId; + public const int LabelInt = -91; + public const int LabelBigint = -93; + public const int LabelDateTime = -94; + public const int LabelTime = -98; + public const int LabelDecimal = -99; + + public const int Textarea = -89; + public const int Textbox = -88; + public const int RichtextEditor = -87; + public const int Boolean = -49; + public const int DateTime = -36; + public const int DropDownSingle = -39; + public const int DropDownMultiple = -42; + public const int Upload = -90; + public const int UploadVideo = -100; + public const int UploadAudio = -101; + public const int UploadArticle = -102; + public const int UploadVectorGraphics = -103; + + public const int DefaultContentListView = -95; + public const int DefaultMediaListView = -96; + public const int DefaultMembersListView = -97; + + public const int ImageCropper = 1043; + public const int Tags = 1041; + + public static class ReservedPreValueKeys { - //NOTE: unfortunately due to backwards compat we can't move/rename these, with the addition of the GUID - //constants, it would make more sense to have these suffixed with "ID" or in a Subclass called "INT", for - //now all we can do is make a subclass called Guids to put the GUID IDs. + public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; + } - public const int LabelString = System.DefaultLabelDataTypeId; - public const int LabelInt = -91; - public const int LabelBigint = -93; - public const int LabelDateTime = -94; - public const int LabelTime = -98; - public const int LabelDecimal = -99; - - public const int Textarea = -89; - public const int Textbox = -88; - public const int RichtextEditor = -87; - public const int Boolean = -49; - public const int DateTime = -36; - public const int DropDownSingle = -39; - public const int DropDownMultiple = -42; - public const int Upload = -90; - public const int UploadVideo = -100; - public const int UploadAudio = -101; - public const int UploadArticle = -102; - public const int UploadVectorGraphics = -103; - - public const int DefaultContentListView = -95; - public const int DefaultMediaListView = -96; - public const int DefaultMembersListView = -97; - - public const int ImageCropper = 1043; - public const int Tags = 1041; - - public static class ReservedPreValueKeys - { - public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; - } + /// + /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. + /// + public static class Guids + { + /// + /// Guid for Content Picker as string + /// + public const string ContentPicker = "FD1E0DA5-5606-4862-B679-5D0CF3A52A59"; /// - /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. + /// Guid for Member Picker as string /// - public static class Guids - { - - /// - /// Guid for Content Picker as string - /// - public const string ContentPicker = "FD1E0DA5-5606-4862-B679-5D0CF3A52A59"; - - /// - /// Guid for Content Picker - /// - public static readonly Guid ContentPickerGuid = new Guid(ContentPicker); - - - /// - /// Guid for Member Picker as string - /// - public const string MemberPicker = "1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"; - - /// - /// Guid for Member Picker - /// - public static readonly Guid MemberPickerGuid = new Guid(MemberPicker); - - - /// - /// Guid for Media Picker as string - /// - public const string MediaPicker = "135D60E0-64D9-49ED-AB08-893C9BA44AE5"; - - /// - /// Guid for Media Picker - /// - public static readonly Guid MediaPickerGuid = new Guid(MediaPicker); - - - /// - /// Guid for Multiple Media Picker as string - /// - public const string MultipleMediaPicker = "9DBBCBBB-2327-434A-B355-AF1B84E5010A"; - - /// - /// Guid for Multiple Media Picker - /// - public static readonly Guid MultipleMediaPickerGuid = new Guid(MultipleMediaPicker); - - - /// - /// Guid for Media Picker v3 as string - /// - public const string MediaPicker3 = "4309A3EA-0D78-4329-A06C-C80B036AF19A"; - - /// - /// Guid for Media Picker v3 - /// - public static readonly Guid MediaPicker3Guid = new Guid(MediaPicker3); - - /// - /// Guid for Media Picker v3 multiple as string - /// - public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; - - /// - /// Guid for Media Picker v3 multiple - /// - public static readonly Guid MediaPicker3MultipleGuid = new Guid(MediaPicker3Multiple); - - - /// - /// Guid for Media Picker v3 single-image as string - /// - public const string MediaPicker3SingleImage = "AD9F0CF2-BDA2-45D5-9EA1-A63CFC873FD3"; - - /// - /// Guid for Media Picker v3 single-image - /// - public static readonly Guid MediaPicker3SingleImageGuid = new Guid(MediaPicker3SingleImage); - - - /// - /// Guid for Media Picker v3 multi-image as string - /// - public const string MediaPicker3MultipleImages = "0E63D883-B62B-4799-88C3-157F82E83ECC"; - - /// - /// Guid for Media Picker v3 multi-image - /// - public static readonly Guid MediaPicker3MultipleImagesGuid = new Guid(MediaPicker3MultipleImages); - - - /// - /// Guid for Related Links as string - /// - public const string RelatedLinks = "B4E3535A-1753-47E2-8568-602CF8CFEE6F"; - - /// - /// Guid for Related Links - /// - public static readonly Guid RelatedLinksGuid = new Guid(RelatedLinks); - - - /// - /// Guid for Member as string - /// - public const string Member = "d59be02f-1df9-4228-aa1e-01917d806cda"; - - /// - /// Guid for Member - /// - public static readonly Guid MemberGuid = new Guid(Member); - - - /// - /// Guid for Image Cropper as string - /// - public const string ImageCropper = "1df9f033-e6d4-451f-b8d2-e0cbc50a836f"; - - /// - /// Guid for Image Cropper - /// - public static readonly Guid ImageCropperGuid = new Guid(ImageCropper); - - - /// - /// Guid for Tags as string - /// - public const string Tags = "b6b73142-b9c1-4bf8-a16d-e1c23320b549"; - - /// - /// Guid for Tags - /// - public static readonly Guid TagsGuid = new Guid(Tags); - - - /// - /// Guid for List View - Content as string - /// - public const string ListViewContent = "C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"; - - /// - /// Guid for List View - Content - /// - public static readonly Guid ListViewContentGuid = new Guid(ListViewContent); - - - /// - /// Guid for List View - Media as string - /// - public const string ListViewMedia = "3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"; - - /// - /// Guid for List View - Media - /// - public static readonly Guid ListViewMediaGuid = new Guid(ListViewMedia); - - - /// - /// Guid for List View - Members as string - /// - public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D"; - - /// - /// Guid for List View - Members - /// - public static readonly Guid ListViewMembersGuid = new Guid(ListViewMembers); - - /// - /// Guid for Date Picker with time as string - /// - public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a"; - - /// - /// Guid for Date Picker with time - /// - public static readonly Guid DatePickerWithTimeGuid = new Guid(DatePickerWithTime); - - - /// - /// Guid for Approved Color as string - /// - public const string ApprovedColor = "0225af17-b302-49cb-9176-b9f35cab9c17"; - - /// - /// Guid for Approved Color - /// - public static readonly Guid ApprovedColorGuid = new Guid(ApprovedColor); - - - /// - /// Guid for Dropdown multiple as string - /// - public const string DropdownMultiple = "f38f0ac7-1d27-439c-9f3f-089cd8825a53"; - - /// - /// Guid for Dropdown multiple - /// - public static readonly Guid DropdownMultipleGuid = new Guid(DropdownMultiple); - - - /// - /// Guid for Radiobox as string - /// - public const string Radiobox = "bb5f57c9-ce2b-4bb9-b697-4caca783a805"; - - /// - /// Guid for Radiobox - /// - public static readonly Guid RadioboxGuid = new Guid(Radiobox); - - - /// - /// Guid for Date Picker as string - /// - public const string DatePicker = "5046194e-4237-453c-a547-15db3a07c4e1"; - - /// - /// Guid for Date Picker - /// - public static readonly Guid DatePickerGuid = new Guid(DatePicker); - - - /// - /// Guid for Dropdown as string - /// - public const string Dropdown = "0b6a45e7-44ba-430d-9da5-4e46060b9e03"; - - /// - /// Guid for Dropdown - /// - public static readonly Guid DropdownGuid = new Guid(Dropdown); - - - /// - /// Guid for Checkbox list as string - /// - public const string CheckboxList = "fbaf13a8-4036-41f2-93a3-974f678c312a"; - - /// - /// Guid for Checkbox list - /// - public static readonly Guid CheckboxListGuid = new Guid(CheckboxList); - - - /// - /// Guid for Checkbox as string - /// - public const string Checkbox = "92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"; - - /// - /// Guid for Checkbox - /// - public static readonly Guid CheckboxGuid = new Guid(Checkbox); - - - /// - /// Guid for Numeric as string - /// - public const string Numeric = "2e6d3631-066e-44b8-aec4-96f09099b2b5"; - - /// - /// Guid for Dropdown - /// - public static readonly Guid NumericGuid = new Guid(Numeric); - - - /// - /// Guid for Richtext editor as string - /// - public const string RichtextEditor = "ca90c950-0aff-4e72-b976-a30b1ac57dad"; - - /// - /// Guid for Richtext editor - /// - public static readonly Guid RichtextEditorGuid = new Guid(RichtextEditor); - - - /// - /// Guid for Textstring as string - /// - public const string Textstring = "0cc0eba1-9960-42c9-bf9b-60e150b429ae"; - - /// - /// Guid for Textstring - /// - public static readonly Guid TextstringGuid = new Guid(Textstring); - - - /// - /// Guid for Textarea as string - /// - public const string Textarea = "c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"; - - /// - /// Guid for Dropdown - /// - public static readonly Guid TextareaGuid = new Guid(Textarea); - - - /// - /// Guid for Upload as string - /// - public const string Upload = "84c6b441-31df-4ffe-b67e-67d5bc3ae65a"; - - /// - /// Guid for Upload - /// - public static readonly Guid UploadGuid = new Guid(Upload); - - /// - /// Guid for UploadVideo as string - /// - public const string UploadVideo = "70575fe7-9812-4396-bbe1-c81a76db71b5"; - - /// - /// Guid for UploadVideo - /// - public static readonly Guid UploadVideoGuid = new Guid(UploadVideo); - - /// - /// Guid for UploadAudio as string - /// - public const string UploadAudio = "8f430dd6-4e96-447e-9dc0-cb552c8cd1f3"; - - /// - /// Guid for UploadAudio - /// - public static readonly Guid UploadAudioGuid = new Guid(UploadAudio); - - /// - /// Guid for UploadArticle as string - /// - public const string UploadArticle = "bc1e266c-dac4-4164-bf08-8a1ec6a7143d"; - - /// - /// Guid for UploadArticle - /// - public static readonly Guid UploadArticleGuid = new Guid(UploadArticle); - - /// - /// Guid for UploadVectorGraphics as string - /// - public const string UploadVectorGraphics = "215cb418-2153-4429-9aef-8c0f0041191b"; - - /// - /// Guid for UploadVectorGraphics - /// - public static readonly Guid UploadVectorGraphicsGuid = new Guid(UploadVectorGraphics); - - - /// - /// Guid for Label as string - /// - public const string LabelString = "f0bc4bfb-b499-40d6-ba86-058885a5178c"; - - /// - /// Guid for Label string - /// - public static readonly Guid LabelStringGuid = new Guid(LabelString); - - /// - /// Guid for Label as int - /// - public const string LabelInt = "8e7f995c-bd81-4627-9932-c40e568ec788"; - - /// - /// Guid for Label int - /// - public static readonly Guid LabelIntGuid = new Guid(LabelInt); - - /// - /// Guid for Label as big int - /// - public const string LabelBigInt = "930861bf-e262-4ead-a704-f99453565708"; - - /// - /// Guid for Label big int - /// - public static readonly Guid LabelBigIntGuid = new Guid(LabelBigInt); - - /// - /// Guid for Label as date time - /// - public const string LabelDateTime = "0e9794eb-f9b5-4f20-a788-93acd233a7e4"; - - /// - /// Guid for Label date time - /// - public static readonly Guid LabelDateTimeGuid = new Guid(LabelDateTime); - - /// - /// Guid for Label as time - /// - public const string LabelTime = "a97cec69-9b71-4c30-8b12-ec398860d7e8"; - - /// - /// Guid for Label time - /// - public static readonly Guid LabelTimeGuid = new Guid(LabelTime); - - /// - /// Guid for Label as decimal - /// - public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64"; - - /// - /// Guid for Label decimal - /// - public static readonly Guid LabelDecimalGuid = new Guid(LabelDecimal); - - - } + public const string MemberPicker = "1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"; + + /// + /// Guid for Media Picker as string + /// + public const string MediaPicker = "135D60E0-64D9-49ED-AB08-893C9BA44AE5"; + + /// + /// Guid for Multiple Media Picker as string + /// + public const string MultipleMediaPicker = "9DBBCBBB-2327-434A-B355-AF1B84E5010A"; + + /// + /// Guid for Media Picker v3 as string + /// + public const string MediaPicker3 = "4309A3EA-0D78-4329-A06C-C80B036AF19A"; + + /// + /// Guid for Media Picker v3 multiple as string + /// + public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; + + /// + /// Guid for Media Picker v3 single-image as string + /// + public const string MediaPicker3SingleImage = "AD9F0CF2-BDA2-45D5-9EA1-A63CFC873FD3"; + + /// + /// Guid for Media Picker v3 multi-image as string + /// + public const string MediaPicker3MultipleImages = "0E63D883-B62B-4799-88C3-157F82E83ECC"; + + /// + /// Guid for Related Links as string + /// + public const string RelatedLinks = "B4E3535A-1753-47E2-8568-602CF8CFEE6F"; + + /// + /// Guid for Member as string + /// + public const string Member = "d59be02f-1df9-4228-aa1e-01917d806cda"; + + /// + /// Guid for Image Cropper as string + /// + public const string ImageCropper = "1df9f033-e6d4-451f-b8d2-e0cbc50a836f"; + + /// + /// Guid for Tags as string + /// + public const string Tags = "b6b73142-b9c1-4bf8-a16d-e1c23320b549"; + + /// + /// Guid for List View - Content as string + /// + public const string ListViewContent = "C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"; + + /// + /// Guid for List View - Media as string + /// + public const string ListViewMedia = "3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"; + + /// + /// Guid for List View - Members as string + /// + public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D"; + + /// + /// Guid for Date Picker with time as string + /// + public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a"; + + /// + /// Guid for Approved Color as string + /// + public const string ApprovedColor = "0225af17-b302-49cb-9176-b9f35cab9c17"; + + /// + /// Guid for Dropdown multiple as string + /// + public const string DropdownMultiple = "f38f0ac7-1d27-439c-9f3f-089cd8825a53"; + + /// + /// Guid for Radiobox as string + /// + public const string Radiobox = "bb5f57c9-ce2b-4bb9-b697-4caca783a805"; + + /// + /// Guid for Date Picker as string + /// + public const string DatePicker = "5046194e-4237-453c-a547-15db3a07c4e1"; + + /// + /// Guid for Dropdown as string + /// + public const string Dropdown = "0b6a45e7-44ba-430d-9da5-4e46060b9e03"; + + /// + /// Guid for Checkbox list as string + /// + public const string CheckboxList = "fbaf13a8-4036-41f2-93a3-974f678c312a"; + + /// + /// Guid for Checkbox as string + /// + public const string Checkbox = "92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"; + + /// + /// Guid for Numeric as string + /// + public const string Numeric = "2e6d3631-066e-44b8-aec4-96f09099b2b5"; + + /// + /// Guid for Richtext editor as string + /// + public const string RichtextEditor = "ca90c950-0aff-4e72-b976-a30b1ac57dad"; + + /// + /// Guid for Textstring as string + /// + public const string Textstring = "0cc0eba1-9960-42c9-bf9b-60e150b429ae"; + + /// + /// Guid for Textarea as string + /// + public const string Textarea = "c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"; + + /// + /// Guid for Upload as string + /// + public const string Upload = "84c6b441-31df-4ffe-b67e-67d5bc3ae65a"; + + /// + /// Guid for UploadVideo as string + /// + public const string UploadVideo = "70575fe7-9812-4396-bbe1-c81a76db71b5"; + + /// + /// Guid for UploadAudio as string + /// + public const string UploadAudio = "8f430dd6-4e96-447e-9dc0-cb552c8cd1f3"; + + /// + /// Guid for UploadArticle as string + /// + public const string UploadArticle = "bc1e266c-dac4-4164-bf08-8a1ec6a7143d"; + + /// + /// Guid for UploadVectorGraphics as string + /// + public const string UploadVectorGraphics = "215cb418-2153-4429-9aef-8c0f0041191b"; + + /// + /// Guid for Label as string + /// + public const string LabelString = "f0bc4bfb-b499-40d6-ba86-058885a5178c"; + + /// + /// Guid for Label as int + /// + public const string LabelInt = "8e7f995c-bd81-4627-9932-c40e568ec788"; + + /// + /// Guid for Label as big int + /// + public const string LabelBigInt = "930861bf-e262-4ead-a704-f99453565708"; + + /// + /// Guid for Label as date time + /// + public const string LabelDateTime = "0e9794eb-f9b5-4f20-a788-93acd233a7e4"; + + /// + /// Guid for Label as time + /// + public const string LabelTime = "a97cec69-9b71-4c30-8b12-ec398860d7e8"; + + /// + /// Guid for Label as decimal + /// + public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64"; + + /// + /// Guid for Content Picker + /// + public static readonly Guid ContentPickerGuid = new(ContentPicker); + + /// + /// Guid for Member Picker + /// + public static readonly Guid MemberPickerGuid = new(MemberPicker); + + /// + /// Guid for Media Picker + /// + public static readonly Guid MediaPickerGuid = new(MediaPicker); + + /// + /// Guid for Multiple Media Picker + /// + public static readonly Guid MultipleMediaPickerGuid = new(MultipleMediaPicker); + + /// + /// Guid for Media Picker v3 + /// + public static readonly Guid MediaPicker3Guid = new(MediaPicker3); + + /// + /// Guid for Media Picker v3 multiple + /// + public static readonly Guid MediaPicker3MultipleGuid = new(MediaPicker3Multiple); + + /// + /// Guid for Media Picker v3 single-image + /// + public static readonly Guid MediaPicker3SingleImageGuid = new(MediaPicker3SingleImage); + + /// + /// Guid for Media Picker v3 multi-image + /// + public static readonly Guid MediaPicker3MultipleImagesGuid = new(MediaPicker3MultipleImages); + + /// + /// Guid for Related Links + /// + public static readonly Guid RelatedLinksGuid = new(RelatedLinks); + + /// + /// Guid for Member + /// + public static readonly Guid MemberGuid = new(Member); + + /// + /// Guid for Image Cropper + /// + public static readonly Guid ImageCropperGuid = new(ImageCropper); + + /// + /// Guid for Tags + /// + public static readonly Guid TagsGuid = new(Tags); + + /// + /// Guid for List View - Content + /// + public static readonly Guid ListViewContentGuid = new(ListViewContent); + + /// + /// Guid for List View - Media + /// + public static readonly Guid ListViewMediaGuid = new(ListViewMedia); + + /// + /// Guid for List View - Members + /// + public static readonly Guid ListViewMembersGuid = new(ListViewMembers); + + /// + /// Guid for Date Picker with time + /// + public static readonly Guid DatePickerWithTimeGuid = new(DatePickerWithTime); + + /// + /// Guid for Approved Color + /// + public static readonly Guid ApprovedColorGuid = new(ApprovedColor); + + /// + /// Guid for Dropdown multiple + /// + public static readonly Guid DropdownMultipleGuid = new(DropdownMultiple); + + /// + /// Guid for Radiobox + /// + public static readonly Guid RadioboxGuid = new(Radiobox); + + /// + /// Guid for Date Picker + /// + public static readonly Guid DatePickerGuid = new(DatePicker); + + /// + /// Guid for Dropdown + /// + public static readonly Guid DropdownGuid = new(Dropdown); + + /// + /// Guid for Checkbox list + /// + public static readonly Guid CheckboxListGuid = new(CheckboxList); + + /// + /// Guid for Checkbox + /// + public static readonly Guid CheckboxGuid = new(Checkbox); + + /// + /// Guid for Dropdown + /// + public static readonly Guid NumericGuid = new(Numeric); + + /// + /// Guid for Richtext editor + /// + public static readonly Guid RichtextEditorGuid = new(RichtextEditor); + + /// + /// Guid for Textstring + /// + public static readonly Guid TextstringGuid = new(Textstring); + + /// + /// Guid for Dropdown + /// + public static readonly Guid TextareaGuid = new(Textarea); + + /// + /// Guid for Upload + /// + public static readonly Guid UploadGuid = new(Upload); + + /// + /// Guid for UploadVideo + /// + public static readonly Guid UploadVideoGuid = new(UploadVideo); + + /// + /// Guid for UploadAudio + /// + public static readonly Guid UploadAudioGuid = new(UploadAudio); + + /// + /// Guid for UploadArticle + /// + public static readonly Guid UploadArticleGuid = new(UploadArticle); + + /// + /// Guid for UploadVectorGraphics + /// + public static readonly Guid UploadVectorGraphicsGuid = new(UploadVectorGraphics); + + /// + /// Guid for Label string + /// + public static readonly Guid LabelStringGuid = new(LabelString); + + /// + /// Guid for Label int + /// + public static readonly Guid LabelIntGuid = new(LabelInt); + + /// + /// Guid for Label big int + /// + public static readonly Guid LabelBigIntGuid = new(LabelBigInt); + + /// + /// Guid for Label date time + /// + public static readonly Guid LabelDateTimeGuid = new(LabelDateTime); + + /// + /// Guid for Label time + /// + public static readonly Guid LabelTimeGuid = new(LabelTime); + + /// + /// Guid for Label decimal + /// + public static readonly Guid LabelDecimalGuid = new(LabelDecimal); } } } diff --git a/src/Umbraco.Core/Constants-DeploySelector.cs b/src/Umbraco.Core/Constants-DeploySelector.cs index 30daacf42b..0f552e8a82 100644 --- a/src/Umbraco.Core/Constants-DeploySelector.cs +++ b/src/Umbraco.Core/Constants-DeploySelector.cs @@ -1,17 +1,16 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Contains the valid selector values. + /// + public static class DeploySelector { - /// - /// Contains the valid selector values. - /// - public static class DeploySelector - { - public const string This = "this"; - public const string ThisAndChildren = "this-and-children"; - public const string ThisAndDescendants = "this-and-descendants"; - public const string ChildrenOfThis = "children"; - public const string DescendantsOfThis = "descendants"; - } + public const string This = "this"; + public const string ThisAndChildren = "this-and-children"; + public const string ThisAndDescendants = "this-and-descendants"; + public const string ChildrenOfThis = "children"; + public const string DescendantsOfThis = "descendants"; } } diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs index 5a8ea401cb..2980a59457 100644 --- a/src/Umbraco.Core/Constants-HealthChecks.cs +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -1,56 +1,58 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines constants. +/// +public static partial class Constants { /// - /// Defines constants. + /// Defines constants for ModelsBuilder. /// - public static partial class Constants + public static class HealthChecks { - /// - /// Defines constants for ModelsBuilder. - /// - public static class HealthChecks + public static class DocumentationLinks { + public const string SmtpCheck = "https://umbra.co/healthchecks-smtp"; - public static class DocumentationLinks + public static class LiveEnvironment { - public const string SmtpCheck = "https://umbra.co/healthchecks-smtp"; + public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; + } - public static class LiveEnvironment + public static class Configuration + { + public const string MacroErrorsCheck = "https://umbra.co/healthchecks-macro-errors"; + + public const string TrySkipIisCustomErrorsCheck = + "https://umbra.co/healthchecks-skip-iis-custom-errors"; + + public const string NotificationEmailCheck = "https://umbra.co/healthchecks-notification-email"; + } + + public static class FolderAndFilePermissionsCheck + { + public const string FileWriting = "https://umbra.co/healthchecks-file-writing"; + public const string FolderCreation = "https://umbra.co/healthchecks-folder-creation"; + public const string FileWritingForPackages = "https://umbra.co/healthchecks-file-writing-for-packages"; + public const string MediaFolderCreation = "https://umbra.co/healthchecks-media-folder-creation"; + } + + public static class Security + { + public const string UmbracoApplicationUrlCheck = + "https://umbra.co/healthchecks-umbraco-application-url"; + + public const string ClickJackingCheck = "https://umbra.co/healthchecks-click-jacking"; + public const string HstsCheck = "https://umbra.co/healthchecks-hsts"; + public const string NoSniffCheck = "https://umbra.co/healthchecks-no-sniff"; + public const string XssProtectionCheck = "https://umbra.co/healthchecks-xss-protection"; + public const string ExcessiveHeadersCheck = "https://umbra.co/healthchecks-excessive-headers"; + + public static class HttpsCheck { - - public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; - } - - public static class Configuration - { - public const string MacroErrorsCheck = "https://umbra.co/healthchecks-macro-errors"; - public const string TrySkipIisCustomErrorsCheck = "https://umbra.co/healthchecks-skip-iis-custom-errors"; - public const string NotificationEmailCheck = "https://umbra.co/healthchecks-notification-email"; - } - - public static class FolderAndFilePermissionsCheck - { - public const string FileWriting = "https://umbra.co/healthchecks-file-writing"; - public const string FolderCreation = "https://umbra.co/healthchecks-folder-creation"; - public const string FileWritingForPackages = "https://umbra.co/healthchecks-file-writing-for-packages"; - public const string MediaFolderCreation = "https://umbra.co/healthchecks-media-folder-creation"; - } - - public static class Security - { - public const string UmbracoApplicationUrlCheck = "https://umbra.co/healthchecks-umbraco-application-url"; - public const string ClickJackingCheck = "https://umbra.co/healthchecks-click-jacking"; - public const string HstsCheck = "https://umbra.co/healthchecks-hsts"; - public const string NoSniffCheck = "https://umbra.co/healthchecks-no-sniff"; - public const string XssProtectionCheck = "https://umbra.co/healthchecks-xss-protection"; - public const string ExcessiveHeadersCheck = "https://umbra.co/healthchecks-excessive-headers"; - - public static class HttpsCheck - { - public const string CheckIfCurrentSchemeIsHttps = "https://umbra.co/healthchecks-https-request"; - public const string CheckHttpsConfigurationSetting = "https://umbra.co/healthchecks-https-config"; - public const string CheckForValidCertificate = "https://umbra.co/healthchecks-valid-certificate"; - } + public const string CheckIfCurrentSchemeIsHttps = "https://umbra.co/healthchecks-https-request"; + public const string CheckHttpsConfigurationSetting = "https://umbra.co/healthchecks-https-config"; + public const string CheckForValidCertificate = "https://umbra.co/healthchecks-valid-certificate"; } } } diff --git a/src/Umbraco.Core/Constants-HttpClients.cs b/src/Umbraco.Core/Constants-HttpClients.cs index 474ec49a50..677f442085 100644 --- a/src/Umbraco.Core/Constants-HttpClients.cs +++ b/src/Umbraco.Core/Constants-HttpClients.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines constants. +/// +public static partial class Constants { /// - /// Defines constants. + /// Defines constants for named http clients. /// - public static partial class Constants + public static class HttpClients { /// - /// Defines constants for named http clients. + /// Name for http client which ignores certificate errors. /// - public static class HttpClients - { - /// - /// Name for http client which ignores certificate errors. - /// - public const string IgnoreCertificateErrors = "Umbraco:HttpClients:IgnoreCertificateErrors"; - } + public const string IgnoreCertificateErrors = "Umbraco:HttpClients:IgnoreCertificateErrors"; } } diff --git a/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs b/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs index 7be1fbd140..a89bfc2553 100644 --- a/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs +++ b/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class HttpContext { - public static class HttpContext + /// + /// Defines keys for items stored in HttpContext.Items + /// + public static class Items { /// - /// Defines keys for items stored in HttpContext.Items + /// Key for current requests body deserialized as JObject. /// - public static class Items - { - /// - /// Key for current requests body deserialized as JObject. - /// - public const string RequestBodyAsJObject = "RequestBodyAsJObject"; - } + public const string RequestBodyAsJObject = "RequestBodyAsJObject"; } } } diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 39980f116a..40ab52aaa5 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -1,143 +1,142 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Icons { - public static class Icons - { - /// - /// System default icon - /// - public const string DefaultIcon = Content; + /// + /// System default icon + /// + public const string DefaultIcon = Content; - /// - /// System blueprint icon - /// - public const string Blueprint = "icon-blueprint"; + /// + /// System blueprint icon + /// + public const string Blueprint = "icon-blueprint"; - /// - /// System content icon - /// - public const string Content = "icon-document"; + /// + /// System content icon + /// + public const string Content = "icon-document"; - /// - /// System content type icon - /// - public const string ContentType = "icon-item-arrangement"; + /// + /// System content type icon + /// + public const string ContentType = "icon-item-arrangement"; - /// - /// System data type icon - /// - public const string DataType = "icon-autofill"; + /// + /// System data type icon + /// + public const string DataType = "icon-autofill"; - /// - /// System dictionary icon - /// - public const string Dictionary = "icon-book-alt"; + /// + /// System dictionary icon + /// + public const string Dictionary = "icon-book-alt"; - /// - /// System generic folder icon - /// - public const string Folder = "icon-folder"; + /// + /// System generic folder icon + /// + public const string Folder = "icon-folder"; - /// - /// System language icon - /// - public const string Language = "icon-globe"; + /// + /// System language icon + /// + public const string Language = "icon-globe"; - /// - /// System logviewer icon - /// - public const string LogViewer = "icon-box-alt"; + /// + /// System logviewer icon + /// + public const string LogViewer = "icon-box-alt"; - /// - /// System list view icon - /// - public const string ListView = "icon-thumbnail-list"; + /// + /// System list view icon + /// + public const string ListView = "icon-thumbnail-list"; - /// - /// System macro icon - /// - public const string Macro = "icon-settings-alt"; + /// + /// System macro icon + /// + public const string Macro = "icon-settings-alt"; - /// - /// System media file icon - /// - public const string MediaFile = "icon-document"; + /// + /// System media file icon + /// + public const string MediaFile = "icon-document"; - /// - /// System media video icon - /// - public const string MediaVideo = "icon-video"; + /// + /// System media video icon + /// + public const string MediaVideo = "icon-video"; - /// - /// System media audio icon - /// - public const string MediaAudio = "icon-sound-waves"; + /// + /// System media audio icon + /// + public const string MediaAudio = "icon-sound-waves"; - /// - /// System media article icon - /// - public const string MediaArticle = "icon-article"; + /// + /// System media article icon + /// + public const string MediaArticle = "icon-article"; - /// - /// System media vector icon - /// - public const string MediaVectorGraphics = "icon-picture"; + /// + /// System media vector icon + /// + public const string MediaVectorGraphics = "icon-picture"; - /// - /// System media folder icon - /// - public const string MediaFolder = "icon-folder"; + /// + /// System media folder icon + /// + public const string MediaFolder = "icon-folder"; - /// - /// System media image icon - /// - public const string MediaImage = "icon-picture"; + /// + /// System media image icon + /// + public const string MediaImage = "icon-picture"; - /// - /// System media type icon - /// - public const string MediaType = "icon-thumbnails"; + /// + /// System media type icon + /// + public const string MediaType = "icon-thumbnails"; - /// - /// System member icon - /// - public const string Member = "icon-user"; + /// + /// System member icon + /// + public const string Member = "icon-user"; - /// - /// System member group icon - /// - public const string MemberGroup = "icon-users-alt"; + /// + /// System member group icon + /// + public const string MemberGroup = "icon-users-alt"; - /// - /// System member type icon - /// - public const string MemberType = "icon-users"; + /// + /// System member type icon + /// + public const string MemberType = "icon-users"; - /// - /// System packages icon - /// - public const string Packages = "icon-box"; + /// + /// System packages icon + /// + public const string Packages = "icon-box"; - /// - /// System property editor icon - /// - public const string PropertyEditor = "icon-autofill"; + /// + /// System property editor icon + /// + public const string PropertyEditor = "icon-autofill"; - /// - /// System member icon - /// - public const string Template = "icon-layout"; + /// + /// System member icon + /// + public const string Template = "icon-layout"; - /// - /// System user icon - /// - public const string User = "icon-user"; + /// + /// System user icon + /// + public const string User = "icon-user"; - /// - /// System user group icon - /// - public const string UserGroup = "icon-users"; - } + /// + /// System user group icon + /// + public const string UserGroup = "icon-users"; } } diff --git a/src/Umbraco.Core/Constants-Indexes.cs b/src/Umbraco.Core/Constants-Indexes.cs index fcf2e7ed14..9c5d9ca48e 100644 --- a/src/Umbraco.Core/Constants-Indexes.cs +++ b/src/Umbraco.Core/Constants-Indexes.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class UmbracoIndexes { - public static class UmbracoIndexes - { - public const string InternalIndexName = "InternalIndex"; - public const string ExternalIndexName = "ExternalIndex"; - public const string MembersIndexName = "MembersIndex"; - } + public const string InternalIndexName = "InternalIndex"; + public const string ExternalIndexName = "ExternalIndex"; + public const string MembersIndexName = "MembersIndex"; } } diff --git a/src/Umbraco.Core/Constants-ModelsBuilder.cs b/src/Umbraco.Core/Constants-ModelsBuilder.cs index 289c0355a8..63b852a600 100644 --- a/src/Umbraco.Core/Constants-ModelsBuilder.cs +++ b/src/Umbraco.Core/Constants-ModelsBuilder.cs @@ -1,16 +1,15 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines constants. +/// +public static partial class Constants { /// - /// Defines constants. + /// Defines constants for ModelsBuilder. /// - public static partial class Constants + public static class ModelsBuilder { - /// - /// Defines constants for ModelsBuilder. - /// - public static class ModelsBuilder - { - public const string DefaultModelsNamespace = "Umbraco.Cms.Web.Common.PublishedModels"; - } + public const string DefaultModelsNamespace = "Umbraco.Cms.Web.Common.PublishedModels"; } } diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 0a9847b848..049a536690 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -1,125 +1,123 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public static partial class Constants { - public static partial class Constants + /// + /// Defines the Umbraco object type unique identifiers. + /// + public static class ObjectTypes { + public static readonly Guid SystemRoot = new(Strings.SystemRoot); + + public static readonly Guid ContentRecycleBin = new(Strings.ContentRecycleBin); + + public static readonly Guid MediaRecycleBin = new(Strings.MediaRecycleBin); + + public static readonly Guid DataTypeContainer = new(Strings.DataTypeContainer); + + public static readonly Guid DocumentTypeContainer = new(Strings.DocumentTypeContainer); + + public static readonly Guid MediaTypeContainer = new(Strings.MediaTypeContainer); + + public static readonly Guid DataType = new(Strings.DataType); + + public static readonly Guid Document = new(Strings.Document); + + public static readonly Guid DocumentBlueprint = new(Strings.DocumentBlueprint); + + public static readonly Guid DocumentType = new(Strings.DocumentType); + + public static readonly Guid Media = new(Strings.Media); + + public static readonly Guid MediaType = new(Strings.MediaType); + + public static readonly Guid Member = new(Strings.Member); + + public static readonly Guid MemberGroup = new(Strings.MemberGroup); + + public static readonly Guid MemberType = new(Strings.MemberType); + + public static readonly Guid TemplateType = new(Strings.Template); + + public static readonly Guid LockObject = new(Strings.LockObject); + + public static readonly Guid RelationType = new(Strings.RelationType); + + public static readonly Guid FormsForm = new(Strings.FormsForm); + + public static readonly Guid FormsPreValue = new(Strings.FormsPreValue); + + public static readonly Guid FormsDataSource = new(Strings.FormsDataSource); + + public static readonly Guid Language = new(Strings.Language); + + public static readonly Guid IdReservation = new(Strings.IdReservation); + + public static readonly Guid Template = new(Strings.Template); + + public static readonly Guid ContentItem = new(Strings.ContentItem); + /// - /// Defines the Umbraco object type unique identifiers. + /// Defines the Umbraco object type unique identifiers as string. /// - public static class ObjectTypes + /// + /// Should be used only when it's not possible to use the corresponding + /// readonly Guid value, e.g. in attributes (where only consts can be used). + /// + public static class Strings { - /// - /// Defines the Umbraco object type unique identifiers as string. - /// - /// Should be used only when it's not possible to use the corresponding - /// readonly Guid value, e.g. in attributes (where only consts can be used). - public static class Strings - { - // ReSharper disable MemberHidesStaticFromOuterClass + // ReSharper disable MemberHidesStaticFromOuterClass + public const string DataTypeContainer = "521231E3-8B37-469C-9F9D-51AFC91FEB7B"; - public const string DataTypeContainer = "521231E3-8B37-469C-9F9D-51AFC91FEB7B"; + public const string DocumentTypeContainer = "2F7A2769-6B0B-4468-90DD-AF42D64F7F16"; - public const string DocumentTypeContainer = "2F7A2769-6B0B-4468-90DD-AF42D64F7F16"; + public const string MediaTypeContainer = "42AEF799-B288-4744-9B10-BE144B73CDC4"; - public const string MediaTypeContainer = "42AEF799-B288-4744-9B10-BE144B73CDC4"; + public const string ContentItem = "10E2B09F-C28B-476D-B77A-AA686435E44A"; - public const string ContentItem = "10E2B09F-C28B-476D-B77A-AA686435E44A"; + public const string ContentItemType = "7A333C54-6F43-40A4-86A2-18688DC7E532"; - public const string ContentItemType = "7A333C54-6F43-40A4-86A2-18688DC7E532"; + public const string ContentRecycleBin = "01BB7FF2-24DC-4C0C-95A2-C24EF72BBAC8"; - public const string ContentRecycleBin = "01BB7FF2-24DC-4C0C-95A2-C24EF72BBAC8"; + public const string DataType = "30A2A501-1978-4DDB-A57B-F7EFED43BA3C"; - public const string DataType = "30A2A501-1978-4DDB-A57B-F7EFED43BA3C"; + public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; - public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; + public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA"; - public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA"; + public const string DocumentType = "A2CB7800-F571-4787-9638-BC48539A0EFB"; - public const string DocumentType = "A2CB7800-F571-4787-9638-BC48539A0EFB"; + public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C"; - public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C"; + public const string MediaRecycleBin = "CF3D8E34-1C1C-41e9-AE56-878B57B32113"; - public const string MediaRecycleBin = "CF3D8E34-1C1C-41e9-AE56-878B57B32113"; + public const string MediaType = "4EA4382B-2F5A-4C2B-9587-AE9B3CF3602E"; - public const string MediaType = "4EA4382B-2F5A-4C2B-9587-AE9B3CF3602E"; + public const string Member = "39EB0F98-B348-42A1-8662-E7EB18487560"; - public const string Member = "39EB0F98-B348-42A1-8662-E7EB18487560"; + public const string MemberGroup = "366E63B9-880F-4E13-A61C-98069B029728"; - public const string MemberGroup = "366E63B9-880F-4E13-A61C-98069B029728"; + public const string MemberType = "9B5416FB-E72F-45A9-A07B-5A9A2709CE43"; - public const string MemberType = "9B5416FB-E72F-45A9-A07B-5A9A2709CE43"; + public const string SystemRoot = "EA7D8624-4CFE-4578-A871-24AA946BF34D"; - public const string SystemRoot = "EA7D8624-4CFE-4578-A871-24AA946BF34D"; + public const string Template = "6FBDE604-4178-42CE-A10B-8A2600A2F07D"; - public const string Template = "6FBDE604-4178-42CE-A10B-8A2600A2F07D"; + public const string LockObject = "87A9F1FF-B1E4-4A25-BABB-465A4A47EC41"; - public const string LockObject = "87A9F1FF-B1E4-4A25-BABB-465A4A47EC41"; + public const string RelationType = "B1988FAD-8675-4F47-915A-B3A602BC5D8D"; - public const string RelationType = "B1988FAD-8675-4F47-915A-B3A602BC5D8D"; + public const string FormsForm = "F5A9F787-6593-46F0-B8FF-BFD9BCA9F6BB"; - public const string FormsForm = "F5A9F787-6593-46F0-B8FF-BFD9BCA9F6BB"; + public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; - public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; + public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; - public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; + public const string Language = "6B05D05B-EC78-49BE-A4E4-79E274F07A77"; - public const string Language = "6B05D05B-EC78-49BE-A4E4-79E274F07A77"; + public const string IdReservation = "92849B1E-3904-4713-9356-F646F87C25F4"; - public const string IdReservation = "92849B1E-3904-4713-9356-F646F87C25F4"; - - // ReSharper restore MemberHidesStaticFromOuterClass - } - - public static readonly Guid SystemRoot = new Guid(Strings.SystemRoot); - - public static readonly Guid ContentRecycleBin = new Guid(Strings.ContentRecycleBin); - - public static readonly Guid MediaRecycleBin = new Guid(Strings.MediaRecycleBin); - - public static readonly Guid DataTypeContainer = new Guid(Strings.DataTypeContainer); - - public static readonly Guid DocumentTypeContainer = new Guid(Strings.DocumentTypeContainer); - - public static readonly Guid MediaTypeContainer = new Guid(Strings.MediaTypeContainer); - - public static readonly Guid DataType = new Guid(Strings.DataType); - - public static readonly Guid Document = new Guid(Strings.Document); - - public static readonly Guid DocumentBlueprint = new Guid(Strings.DocumentBlueprint); - - public static readonly Guid DocumentType = new Guid(Strings.DocumentType); - - public static readonly Guid Media = new Guid(Strings.Media); - - public static readonly Guid MediaType = new Guid(Strings.MediaType); - - public static readonly Guid Member = new Guid(Strings.Member); - - public static readonly Guid MemberGroup = new Guid(Strings.MemberGroup); - - public static readonly Guid MemberType = new Guid(Strings.MemberType); - - public static readonly Guid TemplateType = new Guid(Strings.Template); - - public static readonly Guid LockObject = new Guid(Strings.LockObject); - - public static readonly Guid RelationType = new Guid(Strings.RelationType); - - public static readonly Guid FormsForm = new Guid(Strings.FormsForm); - - public static readonly Guid FormsPreValue = new Guid(Strings.FormsPreValue); - - public static readonly Guid FormsDataSource = new Guid(Strings.FormsDataSource); - - public static readonly Guid Language = new Guid(Strings.Language); - - public static readonly Guid IdReservation = new Guid(Strings.IdReservation); - - public static readonly Guid Template = new Guid(Strings.Template); - - public static readonly Guid ContentItem = new Guid(Strings.ContentItem); + // ReSharper restore MemberHidesStaticFromOuterClass } } } diff --git a/src/Umbraco.Core/Constants-PackageRepository.cs b/src/Umbraco.Core/Constants-PackageRepository.cs index 96ef39b7c1..96746adb49 100644 --- a/src/Umbraco.Core/Constants-PackageRepository.cs +++ b/src/Umbraco.Core/Constants-PackageRepository.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the constants used for the Umbraco package repository + /// + public static class PackageRepository { - /// - /// Defines the constants used for the Umbraco package repository - /// - public static class PackageRepository - { - public const string RestApiBaseUrl = "https://our.umbraco.com/webapi/packages/v1"; - public const string DefaultRepositoryName = "Umbraco package Repository"; - public const string DefaultRepositoryId = "65194810-1f85-11dd-bd0b-0800200c9a66"; - } + public const string RestApiBaseUrl = "https://our.umbraco.com/webapi/packages/v1"; + public const string DefaultRepositoryName = "Umbraco package Repository"; + public const string DefaultRepositoryId = "65194810-1f85-11dd-bd0b-0800200c9a66"; } } diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index b34351d902..2bb53b3299 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -1,241 +1,239 @@ using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines property editors constants. + /// + public static class PropertyEditors { /// - /// Defines property editors constants. + /// Used to prefix generic properties that are internal content properties /// - public static class PropertyEditors + public const string InternalGenericPropertiesPrefix = "_umb_"; + + public static class Legacy { - /// - /// Used to prefix generic properties that are internal content properties - /// - public const string InternalGenericPropertiesPrefix = "_umb_"; - - public static class Legacy - { - public static class Aliases - { - public const string Textbox = "Umbraco.Textbox"; - public const string Date = "Umbraco.Date"; - public const string ContentPicker2 = "Umbraco.ContentPicker2"; - public const string MediaPicker2 = "Umbraco.MediaPicker2"; - public const string MemberPicker2 = "Umbraco.MemberPicker2"; - public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2"; - public const string TextboxMultiple = "Umbraco.TextboxMultiple"; - public const string RelatedLinks2 = "Umbraco.RelatedLinks2"; - public const string RelatedLinks = "Umbraco.RelatedLinks"; - - } - } - - /// - /// Defines Umbraco built-in property editor aliases. - /// public static class Aliases { - /// - /// Block List. - /// - public const string BlockList = "Umbraco.BlockList"; - - /// - /// CheckBox List. - /// - public const string CheckBoxList = "Umbraco.CheckBoxList"; - - /// - /// Color Picker. - /// - public const string ColorPicker = "Umbraco.ColorPicker"; - - /// - /// Eye Dropper Color Picker. - /// - public const string ColorPickerEyeDropper = "Umbraco.ColorPicker.EyeDropper"; - - /// - /// Content Picker. - /// - public const string ContentPicker = "Umbraco.ContentPicker"; - - /// - /// DateTime. - /// - public const string DateTime = "Umbraco.DateTime"; - - /// - /// DropDown List. - /// - public const string DropDownListFlexible = "Umbraco.DropDown.Flexible"; - - /// - /// Grid. - /// - public const string Grid = "Umbraco.Grid"; - - /// - /// Image Cropper. - /// - public const string ImageCropper = "Umbraco.ImageCropper"; - - /// - /// Integer. - /// - public const string Integer = "Umbraco.Integer"; - - /// - /// Decimal. - /// - public const string Decimal = "Umbraco.Decimal"; - - /// - /// ListView. - /// - public const string ListView = "Umbraco.ListView"; - - /// - /// Media Picker. - /// - public const string MediaPicker = "Umbraco.MediaPicker"; - - /// - /// Media Picker v.3. - /// - public const string MediaPicker3 = "Umbraco.MediaPicker3"; - - /// - /// Multiple Media Picker. - /// - public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker"; - - /// - /// Member Picker. - /// - public const string MemberPicker = "Umbraco.MemberPicker"; - - /// - /// Member Group Picker. - /// - public const string MemberGroupPicker = "Umbraco.MemberGroupPicker"; - - /// - /// MultiNode Tree Picker. - /// - public const string MultiNodeTreePicker = "Umbraco.MultiNodeTreePicker"; - - /// - /// Multiple TextString. - /// - public const string MultipleTextstring = "Umbraco.MultipleTextstring"; - - /// - /// Label. - /// - public const string Label = "Umbraco.Label"; - - /// - /// Picker Relations. - /// - public const string PickerRelations = "Umbraco.PickerRelations"; - - /// - /// RadioButton list. - /// - public const string RadioButtonList = "Umbraco.RadioButtonList"; - - /// - /// Slider. - /// - public const string Slider = "Umbraco.Slider"; - - /// - /// Tags. - /// - public const string Tags = "Umbraco.Tags"; - - /// - /// Textbox. - /// - public const string TextBox = "Umbraco.TextBox"; - - /// - /// Textbox Multiple. - /// - public const string TextArea = "Umbraco.TextArea"; - - /// - /// TinyMCE - /// - public const string TinyMce = "Umbraco.TinyMCE"; - - /// - /// Boolean. - /// - public const string Boolean = "Umbraco.TrueFalse"; - - /// - /// Markdown Editor. - /// - public const string MarkdownEditor = "Umbraco.MarkdownEditor"; - - /// - /// User Picker. - /// - public const string UserPicker = "Umbraco.UserPicker"; - - /// - /// Upload Field. - /// - public const string UploadField = "Umbraco.UploadField"; - - /// - /// Email Address. - /// - public const string EmailAddress = "Umbraco.EmailAddress"; - - /// - /// Nested Content. - /// - public const string NestedContent = "Umbraco.NestedContent"; - - /// - /// Alias for the multi URL picker editor. - /// - public const string MultiUrlPicker = "Umbraco.MultiUrlPicker"; + public const string Textbox = "Umbraco.Textbox"; + public const string Date = "Umbraco.Date"; + public const string ContentPicker2 = "Umbraco.ContentPicker2"; + public const string MediaPicker2 = "Umbraco.MediaPicker2"; + public const string MemberPicker2 = "Umbraco.MemberPicker2"; + public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2"; + public const string TextboxMultiple = "Umbraco.TextboxMultiple"; + public const string RelatedLinks2 = "Umbraco.RelatedLinks2"; + public const string RelatedLinks = "Umbraco.RelatedLinks"; } + } + + /// + /// Defines Umbraco built-in property editor aliases. + /// + public static class Aliases + { + /// + /// Block List. + /// + public const string BlockList = "Umbraco.BlockList"; /// - /// Defines Umbraco build-in datatype configuration keys. + /// CheckBox List. /// - public static class ConfigurationKeys - { - /// - /// The value type of property data (i.e., string, integer, etc) - /// - /// Must be a valid value. - public const string DataValueType = "umbracoDataValueType"; - } + public const string CheckBoxList = "Umbraco.CheckBoxList"; /// - /// Defines Umbraco's built-in property editor groups. + /// Color Picker. /// - public static class Groups - { - public const string Common = "Common"; + public const string ColorPicker = "Umbraco.ColorPicker"; - public const string Lists = "Lists"; + /// + /// Eye Dropper Color Picker. + /// + public const string ColorPickerEyeDropper = "Umbraco.ColorPicker.EyeDropper"; - public const string Media = "Media"; + /// + /// Content Picker. + /// + public const string ContentPicker = "Umbraco.ContentPicker"; - public const string People = "People"; + /// + /// DateTime. + /// + public const string DateTime = "Umbraco.DateTime"; - public const string Pickers = "Pickers"; + /// + /// DropDown List. + /// + public const string DropDownListFlexible = "Umbraco.DropDown.Flexible"; - public const string RichContent = "Rich Content"; - } + /// + /// Grid. + /// + public const string Grid = "Umbraco.Grid"; + + /// + /// Image Cropper. + /// + public const string ImageCropper = "Umbraco.ImageCropper"; + + /// + /// Integer. + /// + public const string Integer = "Umbraco.Integer"; + + /// + /// Decimal. + /// + public const string Decimal = "Umbraco.Decimal"; + + /// + /// ListView. + /// + public const string ListView = "Umbraco.ListView"; + + /// + /// Media Picker. + /// + public const string MediaPicker = "Umbraco.MediaPicker"; + + /// + /// Media Picker v.3. + /// + public const string MediaPicker3 = "Umbraco.MediaPicker3"; + + /// + /// Multiple Media Picker. + /// + public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker"; + + /// + /// Member Picker. + /// + public const string MemberPicker = "Umbraco.MemberPicker"; + + /// + /// Member Group Picker. + /// + public const string MemberGroupPicker = "Umbraco.MemberGroupPicker"; + + /// + /// MultiNode Tree Picker. + /// + public const string MultiNodeTreePicker = "Umbraco.MultiNodeTreePicker"; + + /// + /// Multiple TextString. + /// + public const string MultipleTextstring = "Umbraco.MultipleTextstring"; + + /// + /// Label. + /// + public const string Label = "Umbraco.Label"; + + /// + /// Picker Relations. + /// + public const string PickerRelations = "Umbraco.PickerRelations"; + + /// + /// RadioButton list. + /// + public const string RadioButtonList = "Umbraco.RadioButtonList"; + + /// + /// Slider. + /// + public const string Slider = "Umbraco.Slider"; + + /// + /// Tags. + /// + public const string Tags = "Umbraco.Tags"; + + /// + /// Textbox. + /// + public const string TextBox = "Umbraco.TextBox"; + + /// + /// Textbox Multiple. + /// + public const string TextArea = "Umbraco.TextArea"; + + /// + /// TinyMCE + /// + public const string TinyMce = "Umbraco.TinyMCE"; + + /// + /// Boolean. + /// + public const string Boolean = "Umbraco.TrueFalse"; + + /// + /// Markdown Editor. + /// + public const string MarkdownEditor = "Umbraco.MarkdownEditor"; + + /// + /// User Picker. + /// + public const string UserPicker = "Umbraco.UserPicker"; + + /// + /// Upload Field. + /// + public const string UploadField = "Umbraco.UploadField"; + + /// + /// Email Address. + /// + public const string EmailAddress = "Umbraco.EmailAddress"; + + /// + /// Nested Content. + /// + public const string NestedContent = "Umbraco.NestedContent"; + + /// + /// Alias for the multi URL picker editor. + /// + public const string MultiUrlPicker = "Umbraco.MultiUrlPicker"; + } + + /// + /// Defines Umbraco build-in datatype configuration keys. + /// + public static class ConfigurationKeys + { + /// + /// The value type of property data (i.e., string, integer, etc) + /// + /// Must be a valid value. + public const string DataValueType = "umbracoDataValueType"; + } + + /// + /// Defines Umbraco's built-in property editor groups. + /// + public static class Groups + { + public const string Common = "Common"; + + public const string Lists = "Lists"; + + public const string Media = "Media"; + + public const string People = "People"; + + public const string Pickers = "Pickers"; + + public const string RichContent = "Rich Content"; } } } diff --git a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs index 46b41ea233..a713b279b1 100644 --- a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs +++ b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs @@ -1,46 +1,45 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the identifiers for property-type groups conventions that are used within the Umbraco core. + /// + public static class PropertyTypeGroups { /// - /// Defines the identifiers for property-type groups conventions that are used within the Umbraco core. + /// Guid for an Image PropertyTypeGroup object. /// - public static class PropertyTypeGroups - { - /// - /// Guid for an Image PropertyTypeGroup object. - /// - public const string Image = "79ED4D07-254A-42CF-8FA9-EBE1C116A596"; + public const string Image = "79ED4D07-254A-42CF-8FA9-EBE1C116A596"; - /// - /// Guid for a File PropertyTypeGroup object. - /// - public const string File = "50899F9C-023A-4466-B623-ABA9049885FE"; + /// + /// Guid for a File PropertyTypeGroup object. + /// + public const string File = "50899F9C-023A-4466-B623-ABA9049885FE"; - /// - /// Guid for a Video PropertyTypeGroup object. - /// - public const string Video = "2F0A61B6-CF92-4FF4-B437-751AB35EB254"; + /// + /// Guid for a Video PropertyTypeGroup object. + /// + public const string Video = "2F0A61B6-CF92-4FF4-B437-751AB35EB254"; - /// - /// Guid for an Audio PropertyTypeGroup object. - /// - public const string Audio = "335FB495-0A87-4E82-B902-30EB367B767C"; + /// + /// Guid for an Audio PropertyTypeGroup object. + /// + public const string Audio = "335FB495-0A87-4E82-B902-30EB367B767C"; - /// - /// Guid for an Article PropertyTypeGroup object. - /// - public const string Article = "9AF3BD65-F687-4453-9518-5F180D1898EC"; + /// + /// Guid for an Article PropertyTypeGroup object. + /// + public const string Article = "9AF3BD65-F687-4453-9518-5F180D1898EC"; - /// - /// Guid for a VectorGraphics PropertyTypeGroup object. - /// - public const string VectorGraphics = "F199B4D7-9E84-439F-8531-F87D9AF37711"; + /// + /// Guid for a VectorGraphics PropertyTypeGroup object. + /// + public const string VectorGraphics = "F199B4D7-9E84-439F-8531-F87D9AF37711"; - /// - /// Guid for a Membership PropertyTypeGroup object. - /// - public const string Membership = "0756729D-D665-46E3-B84A-37ACEAA614F8"; - } + /// + /// Guid for a Membership PropertyTypeGroup object. + /// + public const string Membership = "0756729D-D665-46E3-B84A-37ACEAA614F8"; } } diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 68601a78b0..26e26804ae 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -1,73 +1,82 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Security { - public static class Security - { - /// - /// Gets the identifier of the 'super' user. - /// - public const int SuperUserId = -1; + /// + /// Gets the identifier of the 'super' user. + /// + public const int SuperUserId = -1; - public const string SuperUserIdAsString = "-1"; + public const string SuperUserIdAsString = "-1"; - /// - /// The id for the 'unknown' user. - /// - /// - /// This is a user row that exists in the DB only for referential integrity but the user is never returned from any of the services - /// - public const int UnknownUserId = 0; + /// + /// The id for the 'unknown' user. + /// + /// + /// This is a user row that exists in the DB only for referential integrity but the user is never returned from any of + /// the services + /// + public const int UnknownUserId = 0; - /// - /// The name of the 'unknown' user. - /// - public const string UnknownUserName = "SYSTEM"; + /// + /// The name of the 'unknown' user. + /// + public const string UnknownUserName = "SYSTEM"; - public const string AdminGroupAlias = "admin"; - public const string EditorGroupAlias = "editor"; - public const string SensitiveDataGroupAlias = "sensitiveData"; - public const string TranslatorGroupAlias = "translator"; - public const string WriterGroupAlias = "writer"; + public const string AdminGroupAlias = "admin"; + public const string EditorGroupAlias = "editor"; + public const string SensitiveDataGroupAlias = "sensitiveData"; + public const string TranslatorGroupAlias = "translator"; + public const string WriterGroupAlias = "writer"; - public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; - public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; - public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; - public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; - public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; - public const string BackOfficeTwoFactorRememberMeAuthenticationType = "UmbracoTwoFactorRememberMeCookie"; + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; + public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; + public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; + public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; + public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; + public const string BackOfficeTwoFactorRememberMeAuthenticationType = "UmbracoTwoFactorRememberMeCookie"; - public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__"; + public const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__"; - public const string DefaultMemberTypeAlias = "Member"; + public const string DefaultMemberTypeAlias = "Member"; + /// + /// The prefix used for external identity providers for their authentication type + /// + /// + /// By default we don't want to interfere with front-end external providers and their default setup, for back office + /// the + /// providers need to be setup differently and each auth type for the back office will be prefixed with this value + /// + public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; - /// - /// The prefix used for external identity providers for their authentication type - /// - /// - /// By default we don't want to interfere with front-end external providers and their default setup, for back office the - /// providers need to be setup differently and each auth type for the back office will be prefixed with this value - /// - public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; - public const string MemberExternalAuthenticationTypePrefix = "UmbracoMembers."; + public const string MemberExternalAuthenticationTypePrefix = "UmbracoMembers."; - public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; - public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; - public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; - public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; - public const string TicketExpiresClaimType = "http://umbraco.org/2020/06/identity/claims/backoffice/ticketexpires"; + public const string StartContentNodeIdClaimType = + "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; - /// - /// The claim type for the ASP.NET Identity security stamp - /// - public const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp"; + public const string StartMediaNodeIdClaimType = + "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; - public const string AspNetCoreV3PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V3"; - public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2"; - public const string AspNetUmbraco8PasswordHashAlgorithmName = "HMACSHA256"; - public const string AspNetUmbraco4PasswordHashAlgorithmName = "HMACSHA1"; - public const string UnknownPasswordConfigJson = "{\"hashAlgorithm\":\"Unknown\"}"; - } + public const string AllowedApplicationsClaimType = + "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; + + public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + + public const string TicketExpiresClaimType = + "http://umbraco.org/2020/06/identity/claims/backoffice/ticketexpires"; + + /// + /// The claim type for the ASP.NET Identity security stamp + /// + public const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp"; + + public const string AspNetCoreV3PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V3"; + public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2"; + public const string AspNetUmbraco8PasswordHashAlgorithmName = "HMACSHA256"; + public const string AspNetUmbraco4PasswordHashAlgorithmName = "HMACSHA1"; + public const string UnknownPasswordConfigJson = "{\"hashAlgorithm\":\"Unknown\"}"; } } diff --git a/src/Umbraco.Core/Constants-Sql.cs b/src/Umbraco.Core/Constants-Sql.cs index b57861c92a..f893680465 100644 --- a/src/Umbraco.Core/Constants-Sql.cs +++ b/src/Umbraco.Core/Constants-Sql.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class Sql { - public static class Sql - { - /// - /// The maximum amount of parameters that can be used in a query. - /// - /// - /// The actual limit is 2100 - /// (https://docs.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server), - /// but we want to ensure there's room for additional parameters if this value is used to create groups/batches. - /// - public const int MaxParameterCount = 2000; - } + /// + /// The maximum amount of parameters that can be used in a query. + /// + /// + /// The actual limit is 2100 + /// (https://docs.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server), + /// but we want to ensure there's room for additional parameters if this value is used to create groups/batches. + /// + public const int MaxParameterCount = 2000; } } diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index a2fe501ab3..549dae5bd6 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -1,43 +1,53 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class SqlTemplates { - public static class SqlTemplates + public static class VersionableRepository { - 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"; - } - public static class RelationRepository - { - public const string DeleteByParentAll = "Umbraco.Core.RelationRepository.DeleteByParent"; - public const string DeleteByParentIn = "Umbraco.Core.RelationRepository.DeleteByParentIn"; - } + 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"; + } - public static class DataTypeRepository - { - public const string EnsureUniqueNodeName = "Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName"; - } + public static class RelationRepository + { + public const string DeleteByParentAll = "Umbraco.Core.RelationRepository.DeleteByParent"; + public const string DeleteByParentIn = "Umbraco.Core.RelationRepository.DeleteByParentIn"; + } - public static class NuCacheDatabaseDataSource - { - public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; - public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; - public const string SourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin"; - public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; - public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; - public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect"; - public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount"; - public const string ObjectTypeNotTrashedFilter = "Umbraco.Web.PublishedCache.NuCache.DataSource.ObjectTypeNotTrashedFilter"; - public const string OrderByLevelIdSortOrder = "Umbraco.Web.PublishedCache.NuCache.DataSource.OrderByLevelIdSortOrder"; + public static class DataTypeRepository + { + public const string EnsureUniqueNodeName = "Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName"; + } - } + public static class NuCacheDatabaseDataSource + { + public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; + public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; + + public const string SourcesSelectUmbracoNodeJoin = + "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin"; + + public const string ContentSourcesSelect = + "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; + + public const string ContentSourcesCount = + "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; + + public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect"; + public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount"; + + public const string ObjectTypeNotTrashedFilter = + "Umbraco.Web.PublishedCache.NuCache.DataSource.ObjectTypeNotTrashedFilter"; + + public const string OrderByLevelIdSortOrder = + "Umbraco.Web.PublishedCache.NuCache.DataSource.OrderByLevelIdSortOrder"; } } } diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 0ad9852671..43de01995b 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -1,70 +1,68 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the identifiers for Umbraco system nodes. + /// + public static class System { /// - /// Defines the identifiers for Umbraco system nodes. + /// The integer identifier for global system root node. /// - public static class System - { - /// - /// The integer identifier for global system root node. - /// - public const int Root = -1; + public const int Root = -1; - /// - /// The string identifier for global system root node. - /// - /// Use this instead of re-creating the string everywhere. - public const string RootString = "-1"; + /// + /// The string identifier for global system root node. + /// + /// Use this instead of re-creating the string everywhere. + public const string RootString = "-1"; - /// - /// The integer identifier for content's recycle bin. - /// - public const int RecycleBinContent = -20; + /// + /// The integer identifier for content's recycle bin. + /// + public const int RecycleBinContent = -20; - /// - /// The string identifier for content's recycle bin. - /// - /// Use this instead of re-creating the string everywhere. - public const string RecycleBinContentString = "-20"; + /// + /// The string identifier for content's recycle bin. + /// + /// Use this instead of re-creating the string everywhere. + public const string RecycleBinContentString = "-20"; - /// - /// The string path prefix of the content's recycle bin. - /// - /// - /// Everything that is in the content recycle bin, has a path that starts with the prefix. - /// Use this instead of re-creating the string everywhere. - /// - public const string RecycleBinContentPathPrefix = "-1,-20,"; + /// + /// The string path prefix of the content's recycle bin. + /// + /// + /// Everything that is in the content recycle bin, has a path that starts with the prefix. + /// Use this instead of re-creating the string everywhere. + /// + public const string RecycleBinContentPathPrefix = "-1,-20,"; - /// - /// The integer identifier for media's recycle bin. - /// - public const int RecycleBinMedia = -21; + /// + /// The integer identifier for media's recycle bin. + /// + public const int RecycleBinMedia = -21; - /// - /// The string identifier for media's recycle bin. - /// - /// Use this instead of re-creating the string everywhere. - public const string RecycleBinMediaString = "-21"; + /// + /// The string identifier for media's recycle bin. + /// + /// Use this instead of re-creating the string everywhere. + public const string RecycleBinMediaString = "-21"; - /// - /// The string path prefix of the media's recycle bin. - /// - /// - /// Everything that is in the media recycle bin, has a path that starts with the prefix. - /// Use this instead of re-creating the string everywhere. - /// - public const string RecycleBinMediaPathPrefix = "-1,-21,"; + /// + /// The string path prefix of the media's recycle bin. + /// + /// + /// Everything that is in the media recycle bin, has a path that starts with the prefix. + /// Use this instead of re-creating the string everywhere. + /// + public const string RecycleBinMediaPathPrefix = "-1,-21,"; - public const int DefaultLabelDataTypeId = -92; + public const int DefaultLabelDataTypeId = -92; - public const string UmbracoDefaultDatabaseName = "Umbraco"; + public const string UmbracoDefaultDatabaseName = "Umbraco"; - public const string UmbracoConnectionName = "umbracoDbDSN"; - - public const string DefaultUmbracoPath = "~/umbraco"; - } + public const string UmbracoConnectionName = "umbracoDbDSN"; + public const string DefaultUmbracoPath = "~/umbraco"; } } diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index f70dd199fc..85375390ac 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -1,71 +1,68 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public static partial class Constants { - public static partial class Constants + public static class SystemDirectories { - public static class SystemDirectories - { - /// - /// The aspnet bin folder - /// - public const string Bin = "~/bin"; + /// + /// The aspnet bin folder + /// + public const string Bin = "~/bin"; - // TODO: Shouldn't this exist underneath /Umbraco in the content root? - public const string Config = "~/config"; + // TODO: Shouldn't this exist underneath /Umbraco in the content root? + public const string Config = "~/config"; - /// - /// The Umbraco folder that exists at the content root. - /// - /// - /// This is not the same as the Umbraco web folder which is configurable for serving front-end files. - /// - public const string Umbraco = "~/umbraco"; + /// + /// The Umbraco folder that exists at the content root. + /// + /// + /// This is not the same as the Umbraco web folder which is configurable for serving front-end files. + /// + public const string Umbraco = "~/umbraco"; - /// - /// The Umbraco data folder in the content root. - /// - public const string Data = Umbraco + "/Data"; + /// + /// The Umbraco data folder in the content root. + /// + public const string Data = Umbraco + "/Data"; - /// - /// The Umbraco licenses folder in the content root. - /// - public const string Licenses = Umbraco + "/Licenses"; + /// + /// The Umbraco licenses folder in the content root. + /// + public const string Licenses = Umbraco + "/Licenses"; - /// - /// The Umbraco temp data folder in the content root. - /// - public const string TempData = Data + "/TEMP"; + /// + /// The Umbraco temp data folder in the content root. + /// + public const string TempData = Data + "/TEMP"; - public const string TempFileUploads = TempData + "/FileUploads"; + public const string TempFileUploads = TempData + "/FileUploads"; - public const string TempImageUploads = TempFileUploads + "/rte"; + public const string TempImageUploads = TempFileUploads + "/rte"; - public const string Install = "~/install"; + public const string Install = "~/install"; - public const string AppPlugins = "/App_Plugins"; + public const string AppPlugins = "/App_Plugins"; - [Obsolete("Use PluginIcons instead")] - public static string AppPluginIcons => "/Backoffice/Icons"; + public const string PluginIcons = "/backoffice/icons"; - public const string PluginIcons = "/backoffice/icons"; + public const string MvcViews = "~/Views"; - public const string MvcViews = "~/Views"; + public const string PartialViews = MvcViews + "/Partials/"; - public const string PartialViews = MvcViews + "/Partials/"; + public const string MacroPartials = MvcViews + "/MacroPartials/"; - public const string MacroPartials = MvcViews + "/MacroPartials/"; + public const string Packages = Data + "/packages"; - public const string Packages = Data + "/packages"; + public const string CreatedPackages = Data + "/CreatedPackages"; - public const string CreatedPackages = Data + "/CreatedPackages"; + public const string Preview = Data + "/preview"; - public const string Preview = Data + "/preview"; + /// + /// The default folder where Umbraco log files are stored + /// + public const string LogFiles = Umbraco + "/Logs"; - /// - /// The default folder where Umbraco log files are stored - /// - public const string LogFiles = Umbraco + "/Logs"; - } + [Obsolete("Use PluginIcons instead")] + public static string AppPluginIcons => "/Backoffice/Icons"; } } diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs index 6fc474d9ae..5f3783a774 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -1,32 +1,30 @@ -namespace Umbraco.Cms.Core -{ - public static partial class Constants - { - public static class Telemetry - { +namespace Umbraco.Cms.Core; - public static string RootCount = "RootCount"; - public static string DomainCount = "DomainCount"; - public static string ExamineIndexCount = "ExamineIndexCount"; - public static string LanguageCount = "LanguageCount"; - public static string MacroCount = "MacroCount"; - public static string MediaCount = "MediaCount"; - public static string MemberCount = "MemberCount"; - public static string TemplateCount = "TemplateCount"; - public static string ContentCount = "ContentCount"; - public static string DocumentTypeCount = "DocumentTypeCount"; - public static string Properties = "Properties"; - public static string UserCount = "UserCount"; - public static string UserGroupCount = "UserGroupCount"; - public static string ServerOs = "ServerOs"; - public static string ServerFramework = "ServerFramework"; - public static string OsLanguage = "OsLanguage"; - public static string WebServer = "WebServer"; - public static string ModelsBuilderMode = "ModelBuilderMode"; - public static string CustomUmbracoPath = "CustomUmbracoPath"; - public static string AspEnvironment = "AspEnvironment"; - public static string IsDebug = "IsDebug"; - public static string DatabaseProvider = "DatabaseProvider"; - } +public static partial class Constants +{ + public static class Telemetry + { + public static string RootCount = "RootCount"; + public static string DomainCount = "DomainCount"; + public static string ExamineIndexCount = "ExamineIndexCount"; + public static string LanguageCount = "LanguageCount"; + public static string MacroCount = "MacroCount"; + public static string MediaCount = "MediaCount"; + public static string MemberCount = "MemberCount"; + public static string TemplateCount = "TemplateCount"; + public static string ContentCount = "ContentCount"; + public static string DocumentTypeCount = "DocumentTypeCount"; + public static string Properties = "Properties"; + public static string UserCount = "UserCount"; + public static string UserGroupCount = "UserGroupCount"; + public static string ServerOs = "ServerOs"; + public static string ServerFramework = "ServerFramework"; + public static string OsLanguage = "OsLanguage"; + public static string WebServer = "WebServer"; + public static string ModelsBuilderMode = "ModelBuilderMode"; + public static string CustomUmbracoPath = "CustomUmbracoPath"; + public static string AspEnvironment = "AspEnvironment"; + public static string IsDebug = "IsDebug"; + public static string DatabaseProvider = "DatabaseProvider"; } } diff --git a/src/Umbraco.Core/Constants-UdiEntityType.cs b/src/Umbraco.Core/Constants-UdiEntityType.cs index 01e9ca213d..f65c290516 100644 --- a/src/Umbraco.Core/Constants-UdiEntityType.cs +++ b/src/Umbraco.Core/Constants-UdiEntityType.cs @@ -1,74 +1,66 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines well-known entity types. + /// + /// + /// Well-known entity types are those that Deploy already knows about, + /// but entity types are strings and so can be extended beyond what is defined here. + /// + public static class UdiEntityType { - /// - /// Defines well-known entity types. - /// - /// Well-known entity types are those that Deploy already knows about, - /// but entity types are strings and so can be extended beyond what is defined here. - public static class UdiEntityType - { - // note: const fields in this class MUST be consistent with what GetTypes returns - // this is validated by UdiTests.ValidateUdiEntityType - // also, this is used exclusively in Udi static ctor, only once, so there is no - // need to keep it around in a field nor to make it readonly + // note: const fields in this class MUST be consistent with what GetTypes returns + // this is validated by UdiTests.ValidateUdiEntityType + // also, this is used exclusively in Udi static ctor, only once, so there is no + // need to keep it around in a field nor to make it readonly + public const string Unknown = "unknown"; + // guid entity types + public const string AnyGuid = "any-guid"; // that one is for tests - public const string Unknown = "unknown"; + public const string Element = "element"; + public const string Document = "document"; - // guid entity types + public const string DocumentBlueprint = "document-blueprint"; - public const string AnyGuid = "any-guid"; // that one is for tests + public const string Media = "media"; + public const string Member = "member"; - public const string Element = "element"; - public const string Document = "document"; + public const string DictionaryItem = "dictionary-item"; + public const string Macro = "macro"; + public const string Template = "template"; - public const string DocumentBlueprint = "document-blueprint"; + public const string DocumentType = "document-type"; + public const string DocumentTypeContainer = "document-type-container"; - public const string Media = "media"; - public const string Member = "member"; + // TODO: What is this? This alias is only used for the blue print tree to render the blueprint's document type, it's not a real udi type + public const string DocumentTypeBluePrints = "document-type-blueprints"; + public const string MediaType = "media-type"; + public const string MediaTypeContainer = "media-type-container"; + public const string DataType = "data-type"; + public const string DataTypeContainer = "data-type-container"; + public const string MemberType = "member-type"; + public const string MemberGroup = "member-group"; - public const string DictionaryItem = "dictionary-item"; - public const string Macro = "macro"; - public const string Template = "template"; + public const string RelationType = "relation-type"; - public const string DocumentType = "document-type"; - public const string DocumentTypeContainer = "document-type-container"; + // forms + public const string FormsForm = "forms-form"; + public const string FormsPreValue = "forms-prevalue"; + public const string FormsDataSource = "forms-datasource"; - // TODO: What is this? This alias is only used for the blue print tree to render the blueprint's document type, it's not a real udi type - public const string DocumentTypeBluePrints = "document-type-blueprints"; - public const string MediaType = "media-type"; - public const string MediaTypeContainer = "media-type-container"; - public const string DataType = "data-type"; - public const string DataTypeContainer = "data-type-container"; - public const string MemberType = "member-type"; - public const string MemberGroup = "member-group"; + // string entity types + public const string AnyString = "any-string"; // that one is for tests - public const string RelationType = "relation-type"; - - // forms - - public const string FormsForm = "forms-form"; - public const string FormsPreValue = "forms-prevalue"; - public const string FormsDataSource = "forms-datasource"; - - // string entity types - - public const string AnyString = "any-string"; // that one is for tests - - public const string Language = "language"; - public const string MacroScript = "macroscript"; - public const string MediaFile = "media-file"; - public const string TemplateFile = "template-file"; - public const string Script = "script"; - public const string Stylesheet = "stylesheet"; - public const string PartialView = "partial-view"; - public const string PartialViewMacro = "partial-view-macro"; - - - - - } + public const string Language = "language"; + public const string MacroScript = "macroscript"; + public const string MediaFile = "media-file"; + public const string TemplateFile = "template-file"; + public const string Script = "script"; + public const string Stylesheet = "stylesheet"; + public const string PartialView = "partial-view"; + public const string PartialViewMacro = "partial-view-macro"; } } diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index f6a8c00970..bfbe4e56d5 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -1,73 +1,75 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + /// + /// Defines the identifiers for Umbraco system nodes. + /// + public static class Web { /// - /// Defines the identifiers for Umbraco system nodes. + /// The preview cookie name /// - public static class Web + public const string PreviewCookieName = "UMB_PREVIEW"; + + /// + /// Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. + /// + public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT"; + + public const string InstallerCookieName = "umb_installId"; + + /// + /// The cookie name that is used to store the validation value + /// + public const string CsrfValidationCookieName = "UMB-XSRF-V"; + + /// + /// The cookie name that is set for angular to use to pass in to the header value for "X-UMB-XSRF-TOKEN" + /// + public const string AngularCookieName = "UMB-XSRF-TOKEN"; + + /// + /// The header name that angular uses to pass in the token to validate the cookie + /// + public const string AngularHeadername = "X-UMB-XSRF-TOKEN"; + + /// + /// The route name of the page shown when Umbraco has no published content. + /// + public const string NoContentRouteName = "umbraco-no-content"; + + /// + /// The default authentication type used for remembering that 2FA is not needed on next login + /// + public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser"; + + public static class Mvc { - /// - /// The preview cookie name - /// - public const string PreviewCookieName = "UMB_PREVIEW"; + public const string InstallArea = "UmbracoInstall"; - /// - /// Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. - /// - public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT"; + public const string + BackOfficePathSegment = "BackOffice"; // The path segment prefix for all back office controllers - public const string InstallerCookieName = "umb_installId"; + public const string BackOfficeArea = "UmbracoBackOffice"; // Used for area routes of non-api controllers + public const string BackOfficeApiArea = "UmbracoApi"; // Same name as v8 so all routing remains the same + public const string BackOfficeTreeArea = "UmbracoTrees"; // Same name as v8 so all routing remains the same + } - /// - /// The cookie name that is used to store the validation value - /// - public const string CsrfValidationCookieName = "UMB-XSRF-V"; + public static class Routing + { + public const string ControllerToken = "controller"; + public const string ActionToken = "action"; + public const string AreaToken = "area"; + } - /// - /// The cookie name that is set for angular to use to pass in to the header value for "X-UMB-XSRF-TOKEN" - /// - public const string AngularCookieName = "UMB-XSRF-TOKEN"; - - /// - /// The header name that angular uses to pass in the token to validate the cookie - /// - public const string AngularHeadername = "X-UMB-XSRF-TOKEN"; - - /// - /// The route name of the page shown when Umbraco has no published content. - /// - public const string NoContentRouteName = "umbraco-no-content"; - - /// - /// The default authentication type used for remembering that 2FA is not needed on next login - /// - public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser"; - - public static class Mvc - { - public const string InstallArea = "UmbracoInstall"; - public const string BackOfficePathSegment = "BackOffice"; // The path segment prefix for all back office controllers - public const string BackOfficeArea = "UmbracoBackOffice"; // Used for area routes of non-api controllers - public const string BackOfficeApiArea = "UmbracoApi"; // Same name as v8 so all routing remains the same - public const string BackOfficeTreeArea = "UmbracoTrees"; // Same name as v8 so all routing remains the same - } - - public static class Routing - { - public const string ControllerToken = "controller"; - public const string ActionToken = "action"; - public const string AreaToken = "area"; - } - - public static class EmailTypes - { - public const string HealthCheck = "HealthCheck"; - public const string Notification = "Notification"; - public const string PasswordReset = "PasswordReset"; - public const string TwoFactorAuth = "2FA"; - public const string UserInvite = "UserInvite"; - } + public static class EmailTypes + { + public const string HealthCheck = "HealthCheck"; + public const string Notification = "Notification"; + public const string PasswordReset = "PasswordReset"; + public const string TwoFactorAuth = "2FA"; + public const string UserInvite = "UserInvite"; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs index e4a5eedf18..09a3e410fd 100644 --- a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs +++ b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.ContentEditing; @@ -8,59 +5,63 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentAppFactoryCollection : BuilderCollectionBase { - public class ContentAppFactoryCollection : BuilderCollectionBase + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly ILogger _logger; + + public ContentAppFactoryCollection( + Func> items, + ILogger logger, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + : base(items) { - private readonly ILogger _logger; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + _logger = logger; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } - public ContentAppFactoryCollection(Func> items, ILogger logger, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) - : base(items) + public IEnumerable GetContentAppsFor(object o, IEnumerable? userGroups = null) + { + IEnumerable roles = GetCurrentUserGroups(); + + var apps = this.Select(x => x.GetContentAppFor(o, roles)).WhereNotNull().OrderBy(x => x.Weight).ToList(); + + var aliases = new HashSet(); + List? dups = null; + + foreach (ContentApp app in apps) { - _logger = logger; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - } - - private IEnumerable GetCurrentUserGroups() - { - var currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - return currentUser == null - ? Enumerable.Empty() - : currentUser.Groups; - - } - - public IEnumerable GetContentAppsFor(object o, IEnumerable? userGroups = null) - { - var roles = GetCurrentUserGroups(); - - var apps = this.Select(x => x.GetContentAppFor(o, roles)).WhereNotNull().OrderBy(x => x.Weight).ToList(); - - var aliases = new HashSet(); - List? dups = null; - - foreach (var app in apps) + if (app.Alias is not null) { - if (app.Alias is not null) + if (aliases.Contains(app.Alias)) { - - if (aliases.Contains(app.Alias)) - (dups ?? (dups = new List())).Add(app.Alias); - else - aliases.Add(app.Alias); + (dups ??= new List()).Add(app.Alias); + } + else + { + aliases.Add(app.Alias); } } - - if (dups != null) - { - // dying is not user-friendly, so let's write to log instead, and wish people read logs... - - //throw new InvalidOperationException($"Duplicate content app aliases found: {string.Join(",", dups)}"); - _logger.LogWarning("Duplicate content app aliases found: {DuplicateAliases}", string.Join(",", dups)); - } - - return apps; } + + if (dups != null) + { + // dying is not user-friendly, so let's write to log instead, and wish people read logs... + + // throw new InvalidOperationException($"Duplicate content app aliases found: {string.Join(",", dups)}"); + _logger.LogWarning("Duplicate content app aliases found: {DuplicateAliases}", string.Join(",", dups)); + } + + return apps; + } + + private IEnumerable GetCurrentUserGroups() + { + IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + return currentUser == null + ? Enumerable.Empty() + : currentUser.Groups; } } diff --git a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs index a80c79a3ef..fe6fdd423a 100644 --- a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs +++ b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; @@ -9,31 +6,31 @@ using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentAppFactoryCollectionBuilder : OrderedCollectionBuilderBase { - public class ContentAppFactoryCollectionBuilder : OrderedCollectionBuilderBase + protected override ContentAppFactoryCollectionBuilder This => this; + + // need to inject dependencies in the collection, so override creation + public override ContentAppFactoryCollection CreateCollection(IServiceProvider factory) { - protected override ContentAppFactoryCollectionBuilder This => this; + // get the logger factory just-in-time - see note below for manifest parser + ILoggerFactory loggerFactory = factory.GetRequiredService(); + IBackOfficeSecurityAccessor backOfficeSecurityAccessor = + factory.GetRequiredService(); + return new ContentAppFactoryCollection(() => CreateItems(factory), loggerFactory.CreateLogger(), backOfficeSecurityAccessor); + } - // need to inject dependencies in the collection, so override creation - public override ContentAppFactoryCollection CreateCollection(IServiceProvider factory) - { - // get the logger factory just-in-time - see note below for manifest parser - var loggerFactory = factory.GetRequiredService(); - var backOfficeSecurityAccessor = factory.GetRequiredService(); - return new ContentAppFactoryCollection( - () => CreateItems(factory), - loggerFactory.CreateLogger(), backOfficeSecurityAccessor); - } - - protected override IEnumerable CreateItems(IServiceProvider factory) - { - // get the manifest parser just-in-time - injecting it in the ctor would mean that - // simply getting the builder in order to configure the collection, would require - // its dependencies too, and that can create cycles or other oddities - var manifestParser = factory.GetRequiredService(); - var ioHelper = factory.GetRequiredService(); - return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.ContentApps.Select(x => new ManifestContentAppFactory(x, ioHelper))); - } + protected override IEnumerable CreateItems(IServiceProvider factory) + { + // get the manifest parser just-in-time - injecting it in the ctor would mean that + // simply getting the builder in order to configure the collection, would require + // its dependencies too, and that can create cycles or other oddities + IManifestParser manifestParser = factory.GetRequiredService(); + IIOHelper ioHelper = factory.GetRequiredService(); + return base.CreateItems(factory) + .Concat(manifestParser.CombinedManifest.ContentApps.Select(x => + new ManifestContentAppFactory(x, ioHelper))); } } diff --git a/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs index 948c563ea9..ac8b3a2061 100644 --- a/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs @@ -1,56 +1,54 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentEditorContentAppFactory : IContentAppFactory { - public class ContentEditorContentAppFactory : IContentAppFactory + // see note on ContentApp + internal const int Weight = -100; + + private ContentApp? _contentApp; + private ContentApp? _mediaApp; + private ContentApp? _memberApp; + + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) { - // see note on ContentApp - internal const int Weight = -100; - - private ContentApp? _contentApp; - private ContentApp? _mediaApp; - private ContentApp? _memberApp; - - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + switch (o) { - switch (o) - { - case IContent content when content.Properties.Count > 0: - return _contentApp ?? (_contentApp = new ContentApp - { - Alias = "umbContent", - Name = "Content", - Icon = Constants.Icons.Content, - View = "views/content/apps/content/content.html", - Weight = Weight - }); + case IContent content when content.Properties.Count > 0: + return _contentApp ??= new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = Constants.Icons.Content, + View = "views/content/apps/content/content.html", + Weight = Weight, + }; - case IMedia media when !media.ContentType.IsContainer || media.Properties.Count > 0: - return _mediaApp ?? (_mediaApp = new ContentApp - { - Alias = "umbContent", - Name = "Content", - Icon = Constants.Icons.Content, - View = "views/media/apps/content/content.html", - Weight = Weight - }); + case IMedia media when !media.ContentType.IsContainer || media.Properties.Count > 0: + return _mediaApp ??= new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = Constants.Icons.Content, + View = "views/media/apps/content/content.html", + Weight = Weight, + }; - case IMember _: - return _memberApp ?? (_memberApp = new ContentApp - { - Alias = "umbContent", - Name = "Content", - Icon = Constants.Icons.Content, - View = "views/member/apps/content/content.html", - Weight = Weight - }); + case IMember _: + return _memberApp ??= new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = Constants.Icons.Content, + View = "views/member/apps/content/content.html", + Weight = Weight, + }; - default: - return null; - } + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentInfoContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentInfoContentAppFactory.cs index 3e068750c4..1e318e380e 100644 --- a/src/Umbraco.Core/ContentApps/ContentInfoContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentInfoContentAppFactory.cs @@ -1,55 +1,53 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentInfoContentAppFactory : IContentAppFactory { - public class ContentInfoContentAppFactory : IContentAppFactory + // see note on ContentApp + private const int Weight = +100; + + private ContentApp? _contentApp; + private ContentApp? _mediaApp; + private ContentApp? _memberApp; + + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) { - // see note on ContentApp - private const int Weight = +100; - - private ContentApp? _contentApp; - private ContentApp? _mediaApp; - private ContentApp? _memberApp; - - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + switch (o) { - switch (o) - { - case IContent _: - return _contentApp ??= new ContentApp - { - Alias = "umbInfo", - Name = "Info", - Icon = "icon-info", - View = "views/content/apps/info/info.html", - Weight = Weight - }; + case IContent _: + return _contentApp ??= new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/content/apps/info/info.html", + Weight = Weight, + }; - case IMedia _: - return _mediaApp ??= new ContentApp - { - Alias = "umbInfo", - Name = "Info", - Icon = "icon-info", - View = "views/media/apps/info/info.html", - Weight = Weight - }; - case IMember _: - return _memberApp ??= new ContentApp - { - Alias = "umbInfo", - Name = "Info", - Icon = "icon-info", - View = "views/member/apps/info/info.html", - Weight = Weight - }; + case IMedia _: + return _mediaApp ??= new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/media/apps/info/info.html", + Weight = Weight, + }; + case IMember _: + return _memberApp ??= new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/member/apps/info/info.html", + Weight = Weight, + }; - default: - return null; - } + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentTypeDesignContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentTypeDesignContentAppFactory.cs index 0fe482e7d4..5e4f6a7a88 100644 --- a/src/Umbraco.Core/ContentApps/ContentTypeDesignContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentTypeDesignContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentTypeDesignContentAppFactory : IContentAppFactory { - public class ContentTypeDesignContentAppFactory : IContentAppFactory + private const int Weight = -200; + + private ContentApp? _contentTypeApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -200; - - private ContentApp? _contentTypeApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IContentType _: - return _contentTypeApp ??= new ContentApp() - { - Alias = "design", - Name = "Design", - Icon = "icon-document-dashed-line", - View = "views/documentTypes/views/design/design.html", - Weight = Weight - }; - default: - return null; - } + case IContentType _: + return _contentTypeApp ??= new ContentApp + { + Alias = "design", + Name = "Design", + Icon = "icon-document-dashed-line", + View = "views/documentTypes/views/design/design.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentTypeListViewContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentTypeListViewContentAppFactory.cs index 6ddf98e132..8aed04050f 100644 --- a/src/Umbraco.Core/ContentApps/ContentTypeListViewContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentTypeListViewContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentTypeListViewContentAppFactory : IContentAppFactory { - public class ContentTypeListViewContentAppFactory : IContentAppFactory + private const int Weight = -180; + + private ContentApp? _contentTypeApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -180; - - private ContentApp? _contentTypeApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IContentType _: - return _contentTypeApp ??= new ContentApp() - { - Alias = "listView", - Name = "List view", - Icon = "icon-list", - View = "views/documentTypes/views/listview/listview.html", - Weight = Weight - }; - default: - return null; - } + case IContentType _: + return _contentTypeApp ??= new ContentApp + { + Alias = "listView", + Name = "List view", + Icon = "icon-list", + View = "views/documentTypes/views/listview/listview.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentTypePermissionsContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentTypePermissionsContentAppFactory.cs index 98b82d24e7..b585a7db4d 100644 --- a/src/Umbraco.Core/ContentApps/ContentTypePermissionsContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentTypePermissionsContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentTypePermissionsContentAppFactory : IContentAppFactory { - public class ContentTypePermissionsContentAppFactory : IContentAppFactory + private const int Weight = -160; + + private ContentApp? _contentTypeApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -160; - - private ContentApp? _contentTypeApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IContentType _: - return _contentTypeApp ??= new ContentApp() - { - Alias = "permissions", - Name = "Permissions", - Icon = "icon-keychain", - View = "views/documentTypes/views/permissions/permissions.html", - Weight = Weight - }; - default: - return null; - } + case IContentType _: + return _contentTypeApp ??= new ContentApp + { + Alias = "permissions", + Name = "Permissions", + Icon = "icon-keychain", + View = "views/documentTypes/views/permissions/permissions.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentTypeTemplatesContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentTypeTemplatesContentAppFactory.cs index 74e57d76c9..712e1e7c1e 100644 --- a/src/Umbraco.Core/ContentApps/ContentTypeTemplatesContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentTypeTemplatesContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ContentTypeTemplatesContentAppFactory : IContentAppFactory { - public class ContentTypeTemplatesContentAppFactory : IContentAppFactory + private const int Weight = -140; + + private ContentApp? _contentTypeApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -140; - - private ContentApp? _contentTypeApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IContentType _: - return _contentTypeApp ??= new ContentApp() - { - Alias = "templates", - Name = "Templates", - Icon = "icon-layout", - View = "views/documentTypes/views/templates/templates.html", - Weight = Weight - }; - default: - return null; - } + case IContentType _: + return _contentTypeApp ??= new ContentApp + { + Alias = "templates", + Name = "Templates", + Icon = "icon-layout", + View = "views/documentTypes/views/templates/templates.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs index ae8a957df7..21bfcfcef0 100644 --- a/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +internal class DictionaryContentAppFactory : IContentAppFactory { - internal class DictionaryContentAppFactory : IContentAppFactory + private const int Weight = -100; + + private ContentApp? _dictionaryApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - private const int Weight = -100; - - private ContentApp? _dictionaryApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IDictionaryItem _: - return _dictionaryApp ??= new ContentApp - { - Alias = "dictionaryContent", - Name = "Content", - Icon = "icon-document", - View = "views/dictionary/views/content/content.html", - Weight = Weight - }; - default: - return null; - } + case IDictionaryItem _: + return _dictionaryApp ??= new ContentApp + { + Alias = "dictionaryContent", + Name = "Content", + Icon = "icon-document", + View = "views/dictionary/views/content/content.html", + Weight = Weight, + }; + default: + return null; } } } diff --git a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs index d33c50499f..466c9d7a3b 100644 --- a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; @@ -7,129 +5,157 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +public class ListViewContentAppFactory : IContentAppFactory { - public class ListViewContentAppFactory : IContentAppFactory + // see note on ContentApp + private const int Weight = -666; + + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditors; + + public ListViewContentAppFactory(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors) { - // see note on ContentApp - private const int Weight = -666; + _dataTypeService = dataTypeService; + _propertyEditors = propertyEditors; + } - private readonly IDataTypeService _dataTypeService; - private readonly PropertyEditorCollection _propertyEditors; - - public ListViewContentAppFactory(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors) + public static ContentApp CreateContentApp( + IDataTypeService dataTypeService, + PropertyEditorCollection propertyEditors, + string entityType, + string contentTypeAlias, + int defaultListViewDataType) + { + if (dataTypeService == null) { - _dataTypeService = dataTypeService; - _propertyEditors = propertyEditors; + throw new ArgumentNullException(nameof(dataTypeService)); } - public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + if (propertyEditors == null) { - string contentTypeAlias, entityType; - int dtdId; - - switch (o) - { - case IContent content when !content.ContentType.IsContainer: - return null; - case IContent content: - contentTypeAlias = content.ContentType.Alias; - entityType = "content"; - dtdId = Constants.DataTypes.DefaultContentListView; - break; - case IMedia media when !media.ContentType.IsContainer && media.ContentType.Alias != Constants.Conventions.MediaTypes.Folder: - return null; - case IMedia media: - contentTypeAlias = media.ContentType.Alias; - entityType = "media"; - dtdId = Constants.DataTypes.DefaultMediaListView; - break; - default: - return null; - } - - return CreateContentApp(_dataTypeService, _propertyEditors, entityType, contentTypeAlias, dtdId); + throw new ArgumentNullException(nameof(propertyEditors)); } - public static ContentApp CreateContentApp(IDataTypeService dataTypeService, - PropertyEditorCollection propertyEditors, - string entityType, string contentTypeAlias, - int defaultListViewDataType) + if (string.IsNullOrWhiteSpace(entityType)) { - if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService)); - if (propertyEditors == null) throw new ArgumentNullException(nameof(propertyEditors)); - if (string.IsNullOrWhiteSpace(entityType)) throw new ArgumentException("message", nameof(entityType)); - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("message", nameof(contentTypeAlias)); - if (defaultListViewDataType == default) throw new ArgumentException("defaultListViewDataType", nameof(defaultListViewDataType)); - - var contentApp = new ContentApp - { - Alias = "umbListView", - Name = "Child items", - Icon = "icon-list", - View = "views/content/apps/listview/listview.html", - Weight = Weight - }; - - var customDtdName = Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias; - - //first try to get the custom one if there is one - var dt = dataTypeService.GetDataType(customDtdName) - ?? dataTypeService.GetDataType(defaultListViewDataType); - - if (dt == null) - { - throw new InvalidOperationException("No list view data type was found for this document type, ensure that the default list view data types exists and/or that your custom list view data type exists"); - } - - var editor = propertyEditors[dt.EditorAlias]; - if (editor == null) - { - throw new NullReferenceException("The property editor with alias " + dt.EditorAlias + " does not exist"); - } - - var listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditor(dt.Configuration); - //add the entity type to the config - listViewConfig["entityType"] = entityType; - - //Override Tab Label if tabName is provided - if (listViewConfig.ContainsKey("tabName")) - { - var configTabName = listViewConfig["tabName"]; - if (configTabName != null && String.IsNullOrWhiteSpace(configTabName.ToString()) == false) - contentApp.Name = configTabName.ToString(); - } - - //Override Icon if icon is provided - if (listViewConfig.ContainsKey("icon")) - { - var configIcon = listViewConfig["icon"]; - if (configIcon != null && String.IsNullOrWhiteSpace(configIcon.ToString()) == false) - contentApp.Icon = configIcon.ToString(); - } - - // if the list view is configured to show umbContent first, update the list view content app weight accordingly - if(listViewConfig.ContainsKey("showContentFirst") && - listViewConfig["showContentFirst"]?.ToString().TryConvertTo().Result == true) - { - contentApp.Weight = ContentEditorContentAppFactory.Weight + 1; - } - - //This is the view model used for the list view app - contentApp.ViewModel = new List - { - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}containerView", - Label = "", - Value = null, - View = editor.GetValueEditor().View, - HideLabel = true, - Config = listViewConfig - } - }; - - return contentApp; + throw new ArgumentException("message", nameof(entityType)); } + + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + { + throw new ArgumentException("message", nameof(contentTypeAlias)); + } + + if (defaultListViewDataType == default) + { + throw new ArgumentException("defaultListViewDataType", nameof(defaultListViewDataType)); + } + + var contentApp = new ContentApp + { + Alias = "umbListView", + Name = "Child items", + Icon = "icon-list", + View = "views/content/apps/listview/listview.html", + Weight = Weight, + }; + + var customDtdName = Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias; + + // first try to get the custom one if there is one + IDataType? dt = dataTypeService.GetDataType(customDtdName) + ?? dataTypeService.GetDataType(defaultListViewDataType); + + if (dt == null) + { + throw new InvalidOperationException( + "No list view data type was found for this document type, ensure that the default list view data types exists and/or that your custom list view data type exists"); + } + + IDataEditor? editor = propertyEditors[dt.EditorAlias]; + if (editor == null) + { + throw new NullReferenceException("The property editor with alias " + dt.EditorAlias + " does not exist"); + } + + IDictionary listViewConfig = + editor.GetConfigurationEditor().ToConfigurationEditor(dt.Configuration); + + // add the entity type to the config + listViewConfig["entityType"] = entityType; + + // Override Tab Label if tabName is provided + if (listViewConfig.ContainsKey("tabName")) + { + var configTabName = listViewConfig["tabName"]; + if (string.IsNullOrWhiteSpace(configTabName.ToString()) == false) + { + contentApp.Name = configTabName.ToString(); + } + } + + // Override Icon if icon is provided + if (listViewConfig.ContainsKey("icon")) + { + var configIcon = listViewConfig["icon"]; + if (string.IsNullOrWhiteSpace(configIcon.ToString()) == false) + { + contentApp.Icon = configIcon.ToString(); + } + } + + // if the list view is configured to show umbContent first, update the list view content app weight accordingly + if (listViewConfig.ContainsKey("showContentFirst") && + listViewConfig["showContentFirst"]?.ToString().TryConvertTo().Result == true) + { + contentApp.Weight = ContentEditorContentAppFactory.Weight + 1; + } + + // This is the view model used for the list view app + contentApp.ViewModel = new List + { + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}containerView", + Label = string.Empty, + Value = null, + View = editor.GetValueEditor().View, + HideLabel = true, + Config = listViewConfig, + }, + }; + + return contentApp; + } + + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + { + string contentTypeAlias, entityType; + int dtdId; + + switch (o) + { + case IContent content when !content.ContentType.IsContainer: + return null; + case IContent content: + contentTypeAlias = content.ContentType.Alias; + entityType = "content"; + dtdId = Constants.DataTypes.DefaultContentListView; + break; + case IMedia media when !media.ContentType.IsContainer && + media.ContentType.Alias != Constants.Conventions.MediaTypes.Folder: + return null; + case IMedia media: + contentTypeAlias = media.ContentType.Alias; + entityType = "media"; + dtdId = Constants.DataTypes.DefaultMediaListView; + break; + default: + return null; + } + + return CreateContentApp(_dataTypeService, _propertyEditors, entityType, contentTypeAlias, dtdId); } } diff --git a/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs b/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs index ae5e783bbc..5ba19cabb0 100644 --- a/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/MemberEditorContentAppFactory.cs @@ -1,34 +1,32 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.ContentApps +namespace Umbraco.Cms.Core.ContentApps; + +internal class MemberEditorContentAppFactory : IContentAppFactory { - internal class MemberEditorContentAppFactory : IContentAppFactory + // see note on ContentApp + internal const int Weight = +50; + + private ContentApp? _memberApp; + + public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) { - // see note on ContentApp - internal const int Weight = +50; - - private ContentApp? _memberApp; - - public ContentApp? GetContentAppFor(object source, IEnumerable userGroups) + switch (source) { - switch (source) - { - case IMember _: - return _memberApp ??= new ContentApp - { - Alias = "umbMembership", - Name = "Member", - Icon = "icon-user", - View = "views/member/apps/membership/membership.html", - Weight = Weight - }; + case IMember _: + return _memberApp ??= new ContentApp + { + Alias = "umbMembership", + Name = "Member", + Icon = "icon-user", + View = "views/member/apps/membership/membership.html", + Weight = Weight, + }; - default: - return null; - } + default: + return null; } } } diff --git a/src/Umbraco.Core/ConventionsHelper.cs b/src/Umbraco.Core/ConventionsHelper.cs index 2f9203ef92..7d79338142 100644 --- a/src/Umbraco.Core/ConventionsHelper.cs +++ b/src/Umbraco.Core/ConventionsHelper.cs @@ -1,26 +1,22 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class ConventionsHelper { - public static class ConventionsHelper - { - public static Dictionary GetStandardPropertyTypeStubs(IShortStringHelper shortStringHelper) => - new Dictionary + public static Dictionary GetStandardPropertyTypeStubs(IShortStringHelper shortStringHelper) => + new() + { { - { - Constants.Conventions.Member.Comments, - new PropertyType( - shortStringHelper, - Constants.PropertyEditors.Aliases.TextArea, - ValueStorageType.Ntext, - true, - Constants.Conventions.Member.Comments) - { - Name = Constants.Conventions.Member.CommentsLabel, - } - }, - }; - } + Constants.Conventions.Member.Comments, + new PropertyType( + shortStringHelper, + Constants.PropertyEditors.Aliases.TextArea, + ValueStorageType.Ntext, + true, + Constants.Conventions.Member.Comments) + { Name = Constants.Conventions.Member.CommentsLabel } + }, + }; } diff --git a/src/Umbraco.Core/CustomBooleanTypeConverter.cs b/src/Umbraco.Core/CustomBooleanTypeConverter.cs index 253f070b40..bacfec7ef9 100644 --- a/src/Umbraco.Core/CustomBooleanTypeConverter.cs +++ b/src/Umbraco.Core/CustomBooleanTypeConverter.cs @@ -1,34 +1,48 @@ -using System; using System.ComponentModel; +using System.Globalization; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Allows for converting string representations of 0 and 1 to boolean +/// +public class CustomBooleanTypeConverter : BooleanConverter { - /// - /// Allows for converting string representations of 0 and 1 to boolean - /// - public class CustomBooleanTypeConverter : BooleanConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + if (sourceType == typeof(string)) { - if (sourceType == typeof(string)) + return true; + } + + return base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is string str) + { + if (str == null || str.Length == 0 || str == "0") + { + return false; + } + + if (str == "1") { return true; } - return base.CanConvertFrom(context, sourceType); - } - public override object? ConvertFrom(ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) - { - if (value is string) + if (str.Equals("Yes", StringComparison.OrdinalIgnoreCase)) { - var str = (string)value; - if (str == null || str.Length == 0 || str == "0") return false; - if (str == "1") return true; - if (str.Equals("Yes", StringComparison.OrdinalIgnoreCase)) return true; - if (str.Equals("No", StringComparison.OrdinalIgnoreCase)) return false; + return true; } - return base.ConvertFrom(context, culture, value); + if (str.Equals("No", StringComparison.OrdinalIgnoreCase)) + { + return false; + } } + + return base.ConvertFrom(context, culture, value); } } diff --git a/src/Umbraco.Core/Dashboards/AccessRule.cs b/src/Umbraco.Core/Dashboards/AccessRule.cs index 070659518e..eb7383f601 100644 --- a/src/Umbraco.Core/Dashboards/AccessRule.cs +++ b/src/Umbraco.Core/Dashboards/AccessRule.cs @@ -1,13 +1,13 @@ -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Implements . +/// +public class AccessRule : IAccessRule { - /// - /// Implements . - /// - public class AccessRule : IAccessRule - { - /// - public AccessRuleType Type { get; set; } = AccessRuleType.Unknown; - /// - public string? Value { get; set; } - } + /// + public AccessRuleType Type { get; set; } = AccessRuleType.Unknown; + + /// + public string? Value { get; set; } } diff --git a/src/Umbraco.Core/Dashboards/AccessRuleType.cs b/src/Umbraco.Core/Dashboards/AccessRuleType.cs index 103d944de8..63d92fc38a 100644 --- a/src/Umbraco.Core/Dashboards/AccessRuleType.cs +++ b/src/Umbraco.Core/Dashboards/AccessRuleType.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Defines dashboard access rules type. +/// +public enum AccessRuleType { /// - /// Defines dashboard access rules type. + /// Unknown (default value). /// - public enum AccessRuleType - { - /// - /// Unknown (default value). - /// - Unknown = 0, + Unknown = 0, - /// - /// Grant access to the dashboard if user belongs to the specified user group. - /// - Grant, + /// + /// Grant access to the dashboard if user belongs to the specified user group. + /// + Grant, - /// - /// Deny access to the dashboard if user belongs to the specified user group. - /// - Deny, + /// + /// Deny access to the dashboard if user belongs to the specified user group. + /// + Deny, - /// - /// Grant access to the dashboard if user has access to the specified section. - /// - GrantBySection - } + /// + /// Grant access to the dashboard if user has access to the specified section. + /// + GrantBySection, } diff --git a/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs b/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs index 1be6e045d0..07688832f6 100644 --- a/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs +++ b/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs @@ -1,15 +1,12 @@ -using System; +namespace Umbraco.Cms.Core.Dashboards; -namespace Umbraco.Cms.Core.Dashboards +public class AnalyticsDashboard : IDashboard { - public class AnalyticsDashboard : IDashboard - { - public string Alias => "settingsAnalytics"; + public string Alias => "settingsAnalytics"; - public string[] Sections => new [] { "settings" }; + public string[] Sections => new[] { "settings" }; - public string View => "views/dashboard/settings/analytics.html"; + public string View => "views/dashboard/settings/analytics.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/ContentDashboard.cs b/src/Umbraco.Core/Dashboards/ContentDashboard.cs index 135fe4304d..ff3a0031b3 100644 --- a/src/Umbraco.Core/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ContentDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class ContentDashboard : IDashboard { - [Weight(10)] - public class ContentDashboard : IDashboard - { - public string Alias => "contentIntro"; + public string Alias => "contentIntro"; - public string[] Sections => new[] { "content" }; + public string[] Sections => new[] { "content" }; - public string View => "views/dashboard/default/startupdashboardintro.html"; + public string View => "views/dashboard/default/startupdashboardintro.html"; - public IAccessRule[] AccessRules { get; } = Array.Empty(); - } + public IAccessRule[] AccessRules { get; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/DashboardCollection.cs b/src/Umbraco.Core/Dashboards/DashboardCollection.cs index e5c8378139..ebcf79fc7f 100644 --- a/src/Umbraco.Core/Dashboards/DashboardCollection.cs +++ b/src/Umbraco.Core/Dashboards/DashboardCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +public class DashboardCollection : BuilderCollectionBase { - public class DashboardCollection : BuilderCollectionBase + public DashboardCollection(Func> items) + : base(items) { - public DashboardCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs index 348e81e383..50867c90f4 100644 --- a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs +++ b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs @@ -1,46 +1,42 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +public class DashboardCollectionBuilder : WeightedCollectionBuilderBase { - public class DashboardCollectionBuilder : WeightedCollectionBuilderBase + protected override DashboardCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) { - protected override DashboardCollectionBuilder This => this; + // get the manifest parser just-in-time - injecting it in the ctor would mean that + // simply getting the builder in order to configure the collection, would require + // its dependencies too, and that can create cycles or other oddities + IManifestParser manifestParser = factory.GetRequiredService(); - protected override IEnumerable CreateItems(IServiceProvider factory) + IEnumerable dashboardSections = + Merge(base.CreateItems(factory), manifestParser.CombinedManifest.Dashboards); + + return dashboardSections; + } + + private IEnumerable Merge( + IEnumerable dashboardsFromCode, + IReadOnlyList dashboardFromManifest) => + dashboardsFromCode.Concat(dashboardFromManifest) + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(GetWeight); + + private int GetWeight(IDashboard dashboard) + { + switch (dashboard) { - // get the manifest parser just-in-time - injecting it in the ctor would mean that - // simply getting the builder in order to configure the collection, would require - // its dependencies too, and that can create cycles or other oddities - var manifestParser = factory.GetRequiredService(); + case ManifestDashboard manifestDashboardDefinition: + return manifestDashboardDefinition.Weight; - var dashboardSections = Merge(base.CreateItems(factory), manifestParser.CombinedManifest.Dashboards); - - return dashboardSections; - } - - private IEnumerable Merge(IEnumerable dashboardsFromCode, IReadOnlyList dashboardFromManifest) - { - return dashboardsFromCode.Concat(dashboardFromManifest) - .Where(x => !string.IsNullOrEmpty(x.Alias)) - .OrderBy(GetWeight); - } - - private int GetWeight(IDashboard dashboard) - { - switch (dashboard) - { - case ManifestDashboard manifestDashboardDefinition: - return manifestDashboardDefinition.Weight; - - default: - return GetWeight(dashboard.GetType()); - } + default: + return GetWeight(dashboard.GetType()); } } } diff --git a/src/Umbraco.Core/Dashboards/DashboardSlim.cs b/src/Umbraco.Core/Dashboards/DashboardSlim.cs index 9ff2b51baf..a79392c0d0 100644 --- a/src/Umbraco.Core/Dashboards/DashboardSlim.cs +++ b/src/Umbraco.Core/Dashboards/DashboardSlim.cs @@ -1,12 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[DataContract(IsReference = true)] +public class DashboardSlim : IDashboardSlim { - [DataContract(IsReference = true)] - public class DashboardSlim : IDashboardSlim - { - public string? Alias { get; set; } + public string? Alias { get; set; } - public string? View { get; set; } - } + public string? View { get; set; } } diff --git a/src/Umbraco.Core/Dashboards/ExamineDashboard.cs b/src/Umbraco.Core/Dashboards/ExamineDashboard.cs index 5411f1d3ce..ddd048c99e 100644 --- a/src/Umbraco.Core/Dashboards/ExamineDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ExamineDashboard.cs @@ -1,19 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(20)] +public class ExamineDashboard : IDashboard { - [Weight(20)] - public class ExamineDashboard : IDashboard - { - public string Alias => "settingsExamine"; + public string Alias => "settingsExamine"; - public string[] Sections => new [] { "settings" }; - - public string View => "views/dashboard/settings/examinemanagement.html"; - - public IAccessRule[] AccessRules => Array.Empty(); - } + public string[] Sections => new[] { "settings" }; + public string View => "views/dashboard/settings/examinemanagement.html"; + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/FormsDashboard.cs b/src/Umbraco.Core/Dashboards/FormsDashboard.cs index c56ad7c51a..4146553484 100644 --- a/src/Umbraco.Core/Dashboards/FormsDashboard.cs +++ b/src/Umbraco.Core/Dashboards/FormsDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class FormsDashboard : IDashboard { - [Weight(10)] - public class FormsDashboard : IDashboard - { - public string Alias => "formsInstall"; + public string Alias => "formsInstall"; - public string[] Sections => new [] { Constants.Applications.Forms }; + public string[] Sections => new[] { Constants.Applications.Forms }; - public string View => "views/dashboard/forms/formsdashboardintro.html"; + public string View => "views/dashboard/forms/formsdashboardintro.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/HealthCheckDashboard.cs b/src/Umbraco.Core/Dashboards/HealthCheckDashboard.cs index 24b4efaf6d..85c2053450 100644 --- a/src/Umbraco.Core/Dashboards/HealthCheckDashboard.cs +++ b/src/Umbraco.Core/Dashboards/HealthCheckDashboard.cs @@ -1,19 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(50)] +public class HealthCheckDashboard : IDashboard { - [Weight(50)] - public class HealthCheckDashboard : IDashboard - { - public string Alias => "settingsHealthCheck"; + public string Alias => "settingsHealthCheck"; - public string[] Sections => new [] { "settings" }; - - public string View => "views/dashboard/settings/healthcheck.html"; - - public IAccessRule[] AccessRules => Array.Empty(); - } + public string[] Sections => new[] { "settings" }; + public string View => "views/dashboard/settings/healthcheck.html"; + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/IAccessRule.cs b/src/Umbraco.Core/Dashboards/IAccessRule.cs index 9f8c120910..fcd78ebc9b 100644 --- a/src/Umbraco.Core/Dashboards/IAccessRule.cs +++ b/src/Umbraco.Core/Dashboards/IAccessRule.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Represents an access rule. +/// +public interface IAccessRule { /// - /// Represents an access rule. + /// Gets or sets the rule type. /// - public interface IAccessRule - { - /// - /// Gets or sets the rule type. - /// - AccessRuleType Type { get; set; } + AccessRuleType Type { get; set; } - /// - /// Gets or sets the value for the rule. - /// - string? Value { get; set; } - } + /// + /// Gets or sets the value for the rule. + /// + string? Value { get; set; } } diff --git a/src/Umbraco.Core/Dashboards/IDashboard.cs b/src/Umbraco.Core/Dashboards/IDashboard.cs index 41a60cb518..96e29d0539 100644 --- a/src/Umbraco.Core/Dashboards/IDashboard.cs +++ b/src/Umbraco.Core/Dashboards/IDashboard.cs @@ -1,37 +1,43 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Represents a dashboard. +/// +public interface IDashboard : IDashboardSlim { /// - /// Represents a dashboard. + /// Gets the aliases of sections/applications where this dashboard appears. /// - public interface IDashboard : IDashboardSlim - { - /// - /// Gets the aliases of sections/applications where this dashboard appears. - /// - /// - /// This field is *not* needed by the UI and therefore we want to exclude - /// it from serialization, but it is deserialized as part of the manifest, - /// therefore we cannot plainly ignore it. - /// So, it has to remain a data member, plus we use our special - /// JsonDontSerialize attribute (see attribute for more details). - /// - [DataMember(Name = "sections")] - string[] Sections { get; } + /// + /// + /// This field is *not* needed by the UI and therefore we want to exclude + /// it from serialization, but it is deserialized as part of the manifest, + /// therefore we cannot plainly ignore it. + /// + /// + /// So, it has to remain a data member, plus we use our special + /// JsonDontSerialize attribute (see attribute for more details). + /// + /// + [DataMember(Name = "sections")] + string[] Sections { get; } - - /// - /// Gets the access rule determining the visibility of the dashboard. - /// - /// - /// This field is *not* needed by the UI and therefore we want to exclude - /// it from serialization, but it is deserialized as part of the manifest, - /// therefore we cannot plainly ignore it. - /// So, it has to remain a data member, plus we use our special - /// JsonDontSerialize attribute (see attribute for more details). - /// - [DataMember(Name = "access")] - IAccessRule[] AccessRules { get; } - } + /// + /// Gets the access rule determining the visibility of the dashboard. + /// + /// + /// + /// This field is *not* needed by the UI and therefore we want to exclude + /// it from serialization, but it is deserialized as part of the manifest, + /// therefore we cannot plainly ignore it. + /// + /// + /// So, it has to remain a data member, plus we use our special + /// JsonDontSerialize attribute (see attribute for more details). + /// + /// + [DataMember(Name = "access")] + IAccessRule[] AccessRules { get; } } diff --git a/src/Umbraco.Core/Dashboards/IDashboardSlim.cs b/src/Umbraco.Core/Dashboards/IDashboardSlim.cs index 4859f5dc84..c3907b1af4 100644 --- a/src/Umbraco.Core/Dashboards/IDashboardSlim.cs +++ b/src/Umbraco.Core/Dashboards/IDashboardSlim.cs @@ -1,22 +1,21 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +/// +/// Represents a dashboard with only minimal data. +/// +public interface IDashboardSlim { /// - /// Represents a dashboard with only minimal data. + /// Gets the alias of the dashboard. /// - public interface IDashboardSlim - { - /// - /// Gets the alias of the dashboard. - /// - [DataMember(Name = "alias")] - string? Alias { get; } + [DataMember(Name = "alias")] + string? Alias { get; } - /// - /// Gets the view used to render the dashboard. - /// - [DataMember(Name = "view")] - string? View { get; } - } + /// + /// Gets the view used to render the dashboard. + /// + [DataMember(Name = "view")] + string? View { get; } } diff --git a/src/Umbraco.Core/Dashboards/MediaDashboard.cs b/src/Umbraco.Core/Dashboards/MediaDashboard.cs index acbad0bc2a..47e45c4270 100644 --- a/src/Umbraco.Core/Dashboards/MediaDashboard.cs +++ b/src/Umbraco.Core/Dashboards/MediaDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class MediaDashboard : IDashboard { - [Weight(10)] - public class MediaDashboard : IDashboard - { - public string Alias => "mediaFolderBrowser"; + public string Alias => "mediaFolderBrowser"; - public string[] Sections => new [] { "media" }; + public string[] Sections => new[] { "media" }; - public string View => "views/dashboard/media/mediafolderbrowser.html"; + public string View => "views/dashboard/media/mediafolderbrowser.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/MembersDashboard.cs b/src/Umbraco.Core/Dashboards/MembersDashboard.cs index 3023d63b8a..f69d0a1ed0 100644 --- a/src/Umbraco.Core/Dashboards/MembersDashboard.cs +++ b/src/Umbraco.Core/Dashboards/MembersDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class MembersDashboard : IDashboard { - [Weight(10)] - public class MembersDashboard : IDashboard - { - public string Alias => "memberIntro"; + public string Alias => "memberIntro"; - public string[] Sections => new [] { "member" }; + public string[] Sections => new[] { "member" }; - public string View => "views/dashboard/members/membersdashboardvideos.html"; + public string View => "views/dashboard/members/membersdashboardvideos.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs b/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs index 9ba5c9dd0c..640f6daf6e 100644 --- a/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ModelsBuilderDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(40)] +public class ModelsBuilderDashboard : IDashboard { - [Weight(40)] - public class ModelsBuilderDashboard : IDashboard - { - public string Alias => "settingsModelsBuilder"; + public string Alias => "settingsModelsBuilder"; - public string[] Sections => new [] { "settings" }; + public string[] Sections => new[] { "settings" }; - public string View => "views/dashboard/settings/modelsbuildermanagement.html"; + public string View => "views/dashboard/settings/modelsbuildermanagement.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/ProfilerDashboard.cs b/src/Umbraco.Core/Dashboards/ProfilerDashboard.cs index 7a3829209f..b84b1529c3 100644 --- a/src/Umbraco.Core/Dashboards/ProfilerDashboard.cs +++ b/src/Umbraco.Core/Dashboards/ProfilerDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(60)] +public class ProfilerDashboard : IDashboard { - [Weight(60)] - public class ProfilerDashboard : IDashboard - { - public string Alias => "settingsProfiler"; + public string Alias => "settingsProfiler"; - public string[] Sections => new [] { "settings" }; + public string[] Sections => new[] { "settings" }; - public string View => "views/dashboard/settings/profiler.html"; + public string View => "views/dashboard/settings/profiler.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/PublishedStatusDashboard.cs b/src/Umbraco.Core/Dashboards/PublishedStatusDashboard.cs index 5cae4594f7..49709436ab 100644 --- a/src/Umbraco.Core/Dashboards/PublishedStatusDashboard.cs +++ b/src/Umbraco.Core/Dashboards/PublishedStatusDashboard.cs @@ -1,19 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(30)] +public class PublishedStatusDashboard : IDashboard { - [Weight(30)] - public class PublishedStatusDashboard : IDashboard - { - public string Alias => "settingsPublishedStatus"; + public string Alias => "settingsPublishedStatus"; - public string[] Sections => new [] { "settings" }; - - public string View => "views/dashboard/settings/publishedstatus.html"; - - public IAccessRule[] AccessRules => Array.Empty(); - } + public string[] Sections => new[] { "settings" }; + public string View => "views/dashboard/settings/publishedstatus.html"; + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/RedirectUrlDashboard.cs b/src/Umbraco.Core/Dashboards/RedirectUrlDashboard.cs index 15eb883697..25b064154b 100644 --- a/src/Umbraco.Core/Dashboards/RedirectUrlDashboard.cs +++ b/src/Umbraco.Core/Dashboards/RedirectUrlDashboard.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(20)] +public class RedirectUrlDashboard : IDashboard { - [Weight(20)] - public class RedirectUrlDashboard : IDashboard - { - public string Alias => "contentRedirectManager"; + public string Alias => "contentRedirectManager"; - public string[] Sections => new [] { "content" }; + public string[] Sections => new[] { "content" }; - public string View => "views/dashboard/content/redirecturls.html"; + public string View => "views/dashboard/content/redirecturls.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/Dashboards/SettingsDashboards.cs b/src/Umbraco.Core/Dashboards/SettingsDashboards.cs index e5f37fd5a3..b9cb572240 100644 --- a/src/Umbraco.Core/Dashboards/SettingsDashboards.cs +++ b/src/Umbraco.Core/Dashboards/SettingsDashboards.cs @@ -1,17 +1,15 @@ -using System; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Dashboards +namespace Umbraco.Cms.Core.Dashboards; + +[Weight(10)] +public class SettingsDashboard : IDashboard { - [Weight(10)] - public class SettingsDashboard : IDashboard - { - public string Alias => "settingsWelcome"; + public string Alias => "settingsWelcome"; - public string[] Sections => new [] { "settings" }; + public string[] Sections => new[] { "settings" }; - public string View => "views/dashboard/settings/settingsdashboardintro.html"; + public string View => "views/dashboard/settings/settingsdashboardintro.html"; - public IAccessRule[] AccessRules => Array.Empty(); - } + public IAccessRule[] AccessRules => Array.Empty(); } diff --git a/src/Umbraco.Core/DefaultEventMessagesFactory.cs b/src/Umbraco.Core/DefaultEventMessagesFactory.cs index 544299b03a..9648e76fca 100644 --- a/src/Umbraco.Core/DefaultEventMessagesFactory.cs +++ b/src/Umbraco.Core/DefaultEventMessagesFactory.cs @@ -1,29 +1,26 @@ -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public class DefaultEventMessagesFactory : IEventMessagesFactory { - public class DefaultEventMessagesFactory : IEventMessagesFactory + private readonly IEventMessagesAccessor _eventMessagesAccessor; + + public DefaultEventMessagesFactory(IEventMessagesAccessor eventMessagesAccessor) { - private readonly IEventMessagesAccessor _eventMessagesAccessor; - - public DefaultEventMessagesFactory(IEventMessagesAccessor eventMessagesAccessor) - { - if (eventMessagesAccessor == null) throw new ArgumentNullException(nameof(eventMessagesAccessor)); - _eventMessagesAccessor = eventMessagesAccessor; - } - - public EventMessages Get() - { - var eventMessages = _eventMessagesAccessor.EventMessages; - if (eventMessages == null) - _eventMessagesAccessor.EventMessages = eventMessages = new EventMessages(); - return eventMessages; - } - - public EventMessages? GetOrDefault() - { - return _eventMessagesAccessor.EventMessages; - } + _eventMessagesAccessor = eventMessagesAccessor ?? throw new ArgumentNullException(nameof(eventMessagesAccessor)); } + + public EventMessages Get() + { + EventMessages? eventMessages = _eventMessagesAccessor.EventMessages; + if (eventMessages == null) + { + _eventMessagesAccessor.EventMessages = eventMessages = new EventMessages(); + } + + return eventMessages; + } + + public EventMessages? GetOrDefault() => _eventMessagesAccessor.EventMessages; } diff --git a/src/Umbraco.Core/DelegateEqualityComparer.cs b/src/Umbraco.Core/DelegateEqualityComparer.cs index 64d715c838..8a442e8f85 100644 --- a/src/Umbraco.Core/DelegateEqualityComparer.cs +++ b/src/Umbraco.Core/DelegateEqualityComparer.cs @@ -1,60 +1,54 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// A custom equality comparer that excepts a delegate to do the comparison operation +/// +/// +public class DelegateEqualityComparer : IEqualityComparer { - /// - /// A custom equality comparer that excepts a delegate to do the comparison operation - /// - /// - public class DelegateEqualityComparer : IEqualityComparer + private readonly Func _equals; + private readonly Func _getHashcode; + + #region Implementation of IEqualityComparer + + public DelegateEqualityComparer(Func equals, Func getHashcode) { - private readonly Func _equals; - private readonly Func _getHashcode; - - #region Implementation of IEqualityComparer - - public DelegateEqualityComparer(Func equals, Func getHashcode) - { - _getHashcode = getHashcode; - _equals = equals; - } - - public static DelegateEqualityComparer CompareMember(Func memberExpression) where TMember : IEquatable - { - return new DelegateEqualityComparer( - (x, y) => memberExpression.Invoke(x).Equals((TMember)memberExpression.Invoke(y)), - x => - { - var invoked = memberExpression.Invoke(x); - return !ReferenceEquals(invoked, default(TMember)) ? invoked.GetHashCode() : 0; - }); - } - - /// - /// Determines whether the specified objects are equal. - /// - /// - /// true if the specified objects are equal; otherwise, false. - /// - /// The first object of type to compare.The second object of type to compare. - public bool Equals(T? x, T? y) - { - return _equals.Invoke(x, y); - } - - /// - /// Returns a hash code for the specified object. - /// - /// - /// A hash code for the specified object. - /// - /// The for which a hash code is to be returned.The type of is a reference type and is null. - public int GetHashCode(T obj) - { - return _getHashcode.Invoke(obj); - } - - #endregion + _getHashcode = getHashcode; + _equals = equals; } + + public static DelegateEqualityComparer CompareMember(Func memberExpression) + where TMember : IEquatable => + new DelegateEqualityComparer( + (x, y) => memberExpression.Invoke(x).Equals(memberExpression.Invoke(y)), + x => + { + TMember invoked = memberExpression.Invoke(x); + return !ReferenceEquals(invoked, default(TMember)) ? invoked.GetHashCode() : 0; + }); + + /// + /// Determines whether the specified objects are equal. + /// + /// + /// true if the specified objects are equal; otherwise, false. + /// + /// The first object of type to compare. + /// The second object of type to compare. + public bool Equals(T? x, T? y) => _equals.Invoke(x, y); + + /// + /// Returns a hash code for the specified object. + /// + /// + /// A hash code for the specified object. + /// + /// The for which a hash code is to be returned. + /// + /// The type of is a reference type and + /// is null. + /// + public int GetHashCode(T obj) => _getHashcode.Invoke(obj); + + #endregion } diff --git a/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs index d1fabe26db..939315cd86 100644 --- a/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs +++ b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs @@ -1,19 +1,16 @@ -using System; +namespace Umbraco.Cms.Core.DependencyInjection; -namespace Umbraco.Cms.Core.DependencyInjection +/// +/// Provides access to a request scoped service provider when available for cases where +/// IHttpContextAccessor is not available. e.g. No reference to AspNetCore.Http in core. +/// +public interface IScopedServiceProvider { /// - /// Provides access to a request scoped service provider when available for cases where - /// IHttpContextAccessor is not available. e.g. No reference to AspNetCore.Http in core. + /// Gets a request scoped service provider when available. /// - public interface IScopedServiceProvider - { - /// - /// Gets a request scoped service provider when available. - /// - /// - /// Can be null. - /// - IServiceProvider? ServiceProvider { get; } - } + /// + /// Can be null. + /// + IServiceProvider? ServiceProvider { get; } } diff --git a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs index 59f06801ff..2629aceb6f 100644 --- a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -7,33 +6,38 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +public interface IUmbracoBuilder { - public interface IUmbracoBuilder - { - IServiceCollection Services { get; } - IConfiguration Config { get; } - TypeLoader TypeLoader { get; } + IServiceCollection Services { get; } - /// - /// A Logger factory created specifically for the . This is NOT the same - /// instance that will be resolved from DI. Use only if required during configuration. - /// - ILoggerFactory BuilderLoggerFactory { get; } + IConfiguration Config { get; } - /// - /// A hosting environment created specifically for the . This is NOT the same - /// instance that will be resolved from DI. Use only if required during configuration. - /// - /// - /// This may be null. - /// - [Obsolete("This property will be removed in a future version, please find an alternative approach.")] - IHostingEnvironment? BuilderHostingEnvironment { get; } + TypeLoader TypeLoader { get; } - IProfiler Profiler { get; } - AppCaches AppCaches { get; } - TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder; - void Build(); - } + /// + /// A Logger factory created specifically for the . This is NOT the same + /// instance that will be resolved from DI. Use only if required during configuration. + /// + ILoggerFactory BuilderLoggerFactory { get; } + + /// + /// A hosting environment created specifically for the . This is NOT the same + /// instance that will be resolved from DI. Use only if required during configuration. + /// + /// + /// This may be null. + /// + [Obsolete("This property will be removed in a future version, please find an alternative approach.")] + IHostingEnvironment? BuilderHostingEnvironment { get; } + + IProfiler Profiler { get; } + + AppCaches AppCaches { get; } + + TBuilder WithCollectionBuilder() + where TBuilder : ICollectionBuilder; + + void Build(); } diff --git a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs index d0f198557f..3d74261abf 100644 --- a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,51 +1,52 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ServiceCollectionExtensions { - public static class ServiceCollectionExtensions + /// + /// Adds a service of type with an implementation type of + /// to the specified . + /// + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services) + where TService : class + where TImplementing : class, TService => + AddUnique(services, ServiceLifetime.Singleton); + + /// + /// Adds a service of type with an implementation type of + /// to the specified . + /// + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services, + ServiceLifetime lifetime) + where TService : class + where TImplementing : class, TService { - /// - /// Adds a service of type with an implementation type of to the specified . - /// - /// - /// Removes all previous registrations for the type . - /// - public static void AddUnique( - this IServiceCollection services) - where TService : class - where TImplementing : class, TService - { - AddUnique(services, ServiceLifetime.Singleton); - } + services.RemoveAll(); + services.Add(ServiceDescriptor.Describe(typeof(TService), typeof(TImplementing), lifetime)); + } - /// - /// Adds a service of type with an implementation type of to the specified . - /// - /// - /// Removes all previous registrations for the type . - /// - public static void AddUnique( - this IServiceCollection services, - ServiceLifetime lifetime) - where TService : class - where TImplementing : class, TService - { - services.RemoveAll(); - services.Add(ServiceDescriptor.Describe(typeof(TService), typeof(TImplementing), lifetime)); - } - - /// - /// Adds services of types & with a shared implementation type of to the specified . - /// - /// - /// Removes all previous registrations for the types & . - /// - public static void AddMultipleUnique( - this IServiceCollection services) - where TService1 : class + /// + /// Adds services of types & with a shared + /// implementation type of to the specified . + /// + /// + /// Removes all previous registrations for the types & + /// . + /// + public static void AddMultipleUnique( + this IServiceCollection services) + where TService1 : class where TService2 : class where TImplementing : class, TService1, TService2 => services.AddMultipleUnique(ServiceLifetime.Singleton); @@ -59,33 +60,34 @@ namespace Umbraco.Extensions public static void AddMultipleUnique( this IServiceCollection services, ServiceLifetime lifetime) - where TService1 : class - where TService2 : class - where TImplementing : class, TService1, TService2 - { - services.AddUnique(lifetime); - services.AddUnique(factory => (TImplementing)factory.GetRequiredService(), lifetime); - } + where TService1 : class + where TService2 : class + where TImplementing : class, TService1, TService2 + { + services.AddUnique(lifetime); + services.AddUnique(factory => (TImplementing)factory.GetRequiredService(), lifetime); + } - // TODO(V11): Remove this function. - [Obsolete("This method is functionally equivalent to AddSingleton() please use that instead.")] - public static void AddUnique(this IServiceCollection services) - where TImplementing : class - { - services.RemoveAll(); - services.AddSingleton(); - } + // TODO(V11): Remove this function. + [Obsolete("This method is functionally equivalent to AddSingleton() please use that instead.")] + public static void AddUnique(this IServiceCollection services) + where TImplementing : class + { + services.RemoveAll(); + services.AddSingleton(); + } - /// - /// Adds a service of type with an implementation factory method to the specified . - /// - /// - /// Removes all previous registrations for the type . - /// - public static void AddUnique( - this IServiceCollection services, - Func factory) - where TService : class + /// + /// Adds a service of type with an implementation factory method to the specified + /// . + /// + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services, + Func factory) + where TService : class => services.AddUnique(factory, ServiceLifetime.Singleton); /// @@ -98,41 +100,42 @@ namespace Umbraco.Extensions this IServiceCollection services, Func factory, ServiceLifetime lifetime) - where TService : class - { - services.RemoveAll(); - services.Add(ServiceDescriptor.Describe(typeof(TService), factory, lifetime)); - } + where TService : class + { + services.RemoveAll(); + services.Add(ServiceDescriptor.Describe(typeof(TService), factory, lifetime)); + } - /// - /// Adds a singleton service of the type specified by to the specified . - /// - /// - /// Removes all previous registrations for the type specified by . - /// - public static void AddUnique(this IServiceCollection services, Type serviceType, object instance) - { - services.RemoveAll(serviceType); - services.AddSingleton(serviceType, instance); - } + /// + /// Adds a singleton service of the type specified by to the specified + /// . + /// + /// + /// Removes all previous registrations for the type specified by . + /// + public static void AddUnique(this IServiceCollection services, Type serviceType, object instance) + { + services.RemoveAll(serviceType); + services.AddSingleton(serviceType, instance); + } - /// - /// Adds a singleton service of type to the specified . - /// - /// - /// Removes all previous registrations for the type type . - /// - public static void AddUnique(this IServiceCollection services, TService instance) - where TService : class - { - services.RemoveAll(); - services.AddSingleton(instance); - } + /// + /// Adds a singleton service of type to the specified + /// . + /// + /// + /// Removes all previous registrations for the type type . + /// + public static void AddUnique(this IServiceCollection services, TService instance) + where TService : class + { + services.RemoveAll(); + services.AddSingleton(instance); + } - internal static IServiceCollection AddLazySupport(this IServiceCollection services) - { - services.Replace(ServiceDescriptor.Transient(typeof(Lazy<>), typeof(LazyResolve<>))); - return services; - } + internal static IServiceCollection AddLazySupport(this IServiceCollection services) + { + services.Replace(ServiceDescriptor.Transient(typeof(Lazy<>), typeof(LazyResolve<>))); + return services; } } diff --git a/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs b/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs index 9bcc0cf7f8..9c2202e2aa 100644 --- a/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs @@ -1,57 +1,55 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to the class. +/// +public static class ServiceProviderExtensions { /// - /// Provides extension methods to the class. + /// Creates an instance with arguments. /// - public static class ServiceProviderExtensions + /// The type of the instance. + /// The factory. + /// Arguments. + /// An instance of the specified type. + /// + /// Throws an exception if the factory failed to get an instance of the specified type. + /// The arguments are used as dependencies by the factory. + /// + public static T CreateInstance(this IServiceProvider serviceProvider, params object[] args) + where T : class + => (T)serviceProvider.CreateInstance(typeof(T), args); + + /// + /// Creates an instance of a service, with arguments. + /// + /// The + /// The type of the instance. + /// Named arguments. + /// An instance of the specified type. + /// + /// The instance type does not need to be registered into the factory. + /// + /// The arguments are used as dependencies by the factory. Other dependencies + /// are retrieved from the factory. + /// + /// + public static object CreateInstance(this IServiceProvider serviceProvider, Type type, params object[] args) + => ActivatorUtilities.CreateInstance(serviceProvider, type, args); + + [EditorBrowsable(EditorBrowsableState.Never)] + public static PublishedModelFactory CreateDefaultPublishedModelFactory(this IServiceProvider factory) { - /// - /// Creates an instance with arguments. - /// - /// The type of the instance. - /// The factory. - /// Arguments. - /// An instance of the specified type. - /// - /// Throws an exception if the factory failed to get an instance of the specified type. - /// The arguments are used as dependencies by the factory. - /// - public static T CreateInstance(this IServiceProvider serviceProvider, params object[] args) - where T : class - => (T)serviceProvider.CreateInstance(typeof(T), args); - - /// - /// Creates an instance of a service, with arguments. - /// - /// The - /// The type of the instance. - /// Named arguments. - /// An instance of the specified type. - /// - /// The instance type does not need to be registered into the factory. - /// The arguments are used as dependencies by the factory. Other dependencies - /// are retrieved from the factory. - /// - public static object CreateInstance(this IServiceProvider serviceProvider, Type type, params object[] args) - => ActivatorUtilities.CreateInstance(serviceProvider, type, args); - - [EditorBrowsable(EditorBrowsableState.Never)] - public static PublishedModelFactory CreateDefaultPublishedModelFactory(this IServiceProvider factory) - { - TypeLoader typeLoader = factory.GetRequiredService(); - IPublishedValueFallback publishedValueFallback = factory.GetRequiredService(); - IEnumerable types = typeLoader - .GetTypes() // element models - .Concat(typeLoader.GetTypes()); // content models - return new PublishedModelFactory(types, publishedValueFallback); - } + TypeLoader typeLoader = factory.GetRequiredService(); + IPublishedValueFallback publishedValueFallback = factory.GetRequiredService(); + IEnumerable types = typeLoader + .GetTypes() // element models + .Concat(typeLoader.GetTypes()); // content models + return new PublishedModelFactory(types, publishedValueFallback); } } diff --git a/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs index fdc4e3f622..6f8e4a2173 100644 --- a/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs +++ b/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs @@ -1,25 +1,25 @@ -using System; using System.ComponentModel; -namespace Umbraco.Cms.Web.Common.DependencyInjection +namespace Umbraco.Cms.Web.Common.DependencyInjection; + +/// +/// Service locator for internal (umbraco cms) only purposes. Should only be used if no other ways exist. +/// +/// +/// It is created with only two goals in mind +/// 1) Continue to have the same extension methods on IPublishedContent and IPublishedElement as in V8. To make +/// migration easier. +/// 2) To have a tool to avoid breaking changes in minor and patch versions. All methods using this should in theory be +/// obsolete. +/// Keep in mind, every time this is used, the code becomes basically untestable. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class StaticServiceProvider { /// - /// Service locator for internal (umbraco cms) only purposes. Should only be used if no other ways exist. + /// The service locator. /// - /// - /// It is created with only two goals in mind - /// 1) Continue to have the same extension methods on IPublishedContent and IPublishedElement as in V8. To make migration easier. - /// 2) To have a tool to avoid breaking changes in minor and patch versions. All methods using this should in theory be obsolete. - /// - /// Keep in mind, every time this is used, the code becomes basically untestable. - /// [EditorBrowsable(EditorBrowsableState.Never)] - public static class StaticServiceProvider - { - /// - /// The service locator. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static IServiceProvider Instance { get; set; } = null!; // This is set doing startup and will always exists after that - } + public static IServiceProvider Instance { get; set; } = + null!; // This is set doing startup and will always exists after that } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs index cba4a95c8e..bd8114bab9 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs @@ -5,111 +5,110 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Sections; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// Contains extensions methods for used for registering content apps. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Contains extensions methods for used for registering content apps. + /// Register a component. /// - public static partial class UmbracoBuilderExtensions + /// The type of the component. + /// The builder. + public static IUmbracoBuilder AddComponent(this IUmbracoBuilder builder) + where T : class, IComponent { - /// - /// Register a component. - /// - /// The type of the component. - /// The builder. - public static IUmbracoBuilder AddComponent(this IUmbracoBuilder builder) - where T : class, IComponent - { - builder.Components().Append(); - return builder; - } + builder.Components().Append(); + return builder; + } - /// - /// Register a content app. - /// - /// The type of the content app. - /// The builder. - public static IUmbracoBuilder AddContentApp(this IUmbracoBuilder builder) - where T : class, IContentAppFactory - { - builder.ContentApps().Append(); - return builder; - } + /// + /// Register a content app. + /// + /// The type of the content app. + /// The builder. + public static IUmbracoBuilder AddContentApp(this IUmbracoBuilder builder) + where T : class, IContentAppFactory + { + builder.ContentApps().Append(); + return builder; + } - /// - /// Register a content finder. - /// - /// The type of the content finder. - /// The builder. - public static IUmbracoBuilder AddContentFinder(this IUmbracoBuilder builder) - where T : class, IContentFinder - { - builder.ContentFinders().Append(); - return builder; - } + /// + /// Register a content finder. + /// + /// The type of the content finder. + /// The builder. + public static IUmbracoBuilder AddContentFinder(this IUmbracoBuilder builder) + where T : class, IContentFinder + { + builder.ContentFinders().Append(); + return builder; + } - /// - /// Register a dashboard. - /// - /// The type of the dashboard. - /// The builder. - public static IUmbracoBuilder AddDashboard(this IUmbracoBuilder builder) - where T : class, IDashboard - { - builder.Dashboards().Add(); - return builder; - } + /// + /// Register a dashboard. + /// + /// The type of the dashboard. + /// The builder. + public static IUmbracoBuilder AddDashboard(this IUmbracoBuilder builder) + where T : class, IDashboard + { + builder.Dashboards().Add(); + return builder; + } - /// - /// Register a media url provider. - /// - /// The type of the media url provider. - /// The builder. - public static IUmbracoBuilder AddMediaUrlProvider(this IUmbracoBuilder builder) - where T : class, IMediaUrlProvider - { - builder.MediaUrlProviders().Append(); - return builder; - } + /// + /// Register a media url provider. + /// + /// The type of the media url provider. + /// The builder. + public static IUmbracoBuilder AddMediaUrlProvider(this IUmbracoBuilder builder) + where T : class, IMediaUrlProvider + { + builder.MediaUrlProviders().Append(); + return builder; + } - /// - /// Register a embed provider. - /// - /// The type of the embed provider. - /// The builder. - public static IUmbracoBuilder AddEmbedProvider(this IUmbracoBuilder builder) - where T : class, IEmbedProvider - { - builder.EmbedProviders().Append(); - return builder; - } + /// + /// Register a embed provider. + /// + /// The type of the embed provider. + /// The builder. + public static IUmbracoBuilder AddEmbedProvider(this IUmbracoBuilder builder) + where T : class, IEmbedProvider + { + builder.EmbedProviders().Append(); + return builder; + } - [Obsolete("Use AddEmbedProvider instead. This will be removed in Umbraco 10")] - public static IUmbracoBuilder AddOEmbedProvider(this IUmbracoBuilder builder) - where T : class, IEmbedProvider => AddEmbedProvider(builder); + [Obsolete("Use AddEmbedProvider instead. This will be removed in Umbraco 10")] + public static IUmbracoBuilder AddOEmbedProvider(this IUmbracoBuilder builder) + where T : class, IEmbedProvider => AddEmbedProvider(builder); - /// - /// Register a section. - /// - /// The type of the section. - /// The builder. - public static IUmbracoBuilder AddSection(this IUmbracoBuilder builder) - where T : class, ISection - { - builder.Sections().Append(); - return builder; - } + /// + /// Register a section. + /// + /// The type of the section. + /// The builder. + public static IUmbracoBuilder AddSection(this IUmbracoBuilder builder) + where T : class, ISection + { + builder.Sections().Append(); + return builder; + } - /// - /// Register a url provider. - /// - /// The type of the url provider. - /// The Builder. - public static IUmbracoBuilder AddUrlProvider(this IUmbracoBuilder builder) - where T : class, IUrlProvider - { - builder.UrlProviders().Append(); - return builder; - } + /// + /// Register a url provider. + /// + /// The type of the url provider. + /// The Builder. + public static IUmbracoBuilder AddUrlProvider(this IUmbracoBuilder builder) + where T : class, IUrlProvider + { + builder.UrlProviders().Append(); + return builder; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 11acf17a3b..5ad759de68 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -20,291 +20,290 @@ using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.WebAssets; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// Extension methods for +/// +public static partial class UmbracoBuilderExtensions { /// - /// Extension methods for + /// Adds all core collection builders /// - public static partial class UmbracoBuilderExtensions + internal static void AddAllCoreCollectionBuilders(this IUmbracoBuilder builder) { - /// - /// Adds all core collection builders - /// - internal static void AddAllCoreCollectionBuilders(this IUmbracoBuilder builder) - { - builder.CacheRefreshers().Add(() => builder.TypeLoader.GetCacheRefreshers()); - builder.DataEditors().Add(() => builder.TypeLoader.GetDataEditors()); - builder.Actions().Add(() => builder .TypeLoader.GetActions()); + builder.CacheRefreshers().Add(() => builder.TypeLoader.GetCacheRefreshers()); + builder.DataEditors().Add(() => builder.TypeLoader.GetDataEditors()); + builder.Actions().Add(() => builder .TypeLoader.GetActions()); - // register known content apps - builder.ContentApps() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); + // register known content apps + builder.ContentApps() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); - // all built-in finders in the correct order, - // devs can then modify this list on application startup - builder.ContentFinders() - .Append() - .Append() - .Append() - /*.Append() // disabled, this is an odd finder */ - .Append() - .Append(); - builder.EditorValidators().Add(() => builder.TypeLoader.GetTypes()); - builder.HealthChecks().Add(() => builder.TypeLoader.GetTypes()); - builder.HealthCheckNotificationMethods().Add(() => builder.TypeLoader.GetTypes()); - builder.TourFilters(); - builder.UrlProviders() - .Append() - .Append(); - builder.MediaUrlProviders() - .Append(); - // register back office sections in the order we want them rendered - builder.Sections() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - builder.Components(); - // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards - builder.Dashboards() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(builder.TypeLoader.GetTypes()); - builder.PartialViewSnippets(); - builder.PartialViewMacroSnippets(); - builder.DataValueReferenceFactories(); - builder.PropertyValueConverters().Append(builder.TypeLoader.GetTypes()); - builder.UrlSegmentProviders().Append(); - builder.ManifestValueValidators() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(); - builder.ManifestFilters(); - builder.MediaUrlGenerators(); - // register OEmbed providers - no type scanning - all explicit opt-in of adding types, IEmbedProvider is not IDiscoverable - builder.EmbedProviders() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - builder.SearchableTrees().Add(() => builder.TypeLoader.GetTypes()); - builder.BackOfficeAssets(); - } - - /// - /// Gets the actions collection builder. - /// - /// The builder. - public static ActionCollectionBuilder Actions(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the content apps collection builder. - /// - /// The builder. - public static ContentAppFactoryCollectionBuilder ContentApps(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the content finders collection builder. - /// - /// The builder. - public static ContentFinderCollectionBuilder ContentFinders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the editor validators collection builder. - /// - /// The builder. - public static EditorValidatorCollectionBuilder EditorValidators(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the health checks collection builder. - /// - /// The builder. - public static HealthCheckCollectionBuilder HealthChecks(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - public static HealthCheckNotificationMethodCollectionBuilder HealthCheckNotificationMethods(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the TourFilters collection builder. - /// - public static TourFilterCollectionBuilder TourFilters(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the URL providers collection builder. - /// - /// The builder. - public static UrlProviderCollectionBuilder UrlProviders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the media url providers collection builder. - /// - /// The builder. - public static MediaUrlProviderCollectionBuilder MediaUrlProviders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the backoffice sections/applications collection builder. - /// - /// The builder. - public static SectionCollectionBuilder Sections(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the components collection builder. - /// - public static ComponentCollectionBuilder Components(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the backoffice dashboards collection builder. - /// - /// The builder. - public static DashboardCollectionBuilder Dashboards(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the partial view snippets collection builder. - /// - /// The builder. - public static PartialViewSnippetCollectionBuilder? PartialViewSnippets(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the partial view macro snippets collection builder. - /// - /// The builder. - public static PartialViewMacroSnippetCollectionBuilder? PartialViewMacroSnippets(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the cache refreshers collection builder. - /// - /// The builder. - public static CacheRefresherCollectionBuilder CacheRefreshers(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the map definitions collection builder. - /// - /// The builder. - public static MapDefinitionCollectionBuilder MapDefinitions(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the data editor collection builder. - /// - /// The builder. - public static DataEditorCollectionBuilder DataEditors(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the data value reference factory collection builder. - /// - /// The builder. - public static DataValueReferenceFactoryCollectionBuilder DataValueReferenceFactories(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the property value converters collection builder. - /// - /// The builder. - public static PropertyValueConverterCollectionBuilder PropertyValueConverters(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the url segment providers collection builder. - /// - /// The builder. - public static UrlSegmentProviderCollectionBuilder UrlSegmentProviders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the validators collection builder. - /// - /// The builder. - internal static ManifestValueValidatorCollectionBuilder ManifestValueValidators(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the manifest filter collection builder. - /// - /// The builder. - public static ManifestFilterCollectionBuilder ManifestFilters(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the content finders collection builder. - /// - /// The builder. - public static MediaUrlGeneratorCollectionBuilder MediaUrlGenerators(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the backoffice OEmbed Providers collection builder. - /// - /// The builder. - [Obsolete("Use EmbedProviders() instead")] - public static EmbedProvidersCollectionBuilder OEmbedProviders(this IUmbracoBuilder builder) - => EmbedProviders(builder); - - /// - /// Gets the backoffice Embed Providers collection builder. - /// - /// The builder. - public static EmbedProvidersCollectionBuilder EmbedProviders(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the back office searchable tree collection builder - /// - public static SearchableTreeCollectionBuilder SearchableTrees(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - /// - /// Gets the back office custom assets collection builder - /// - public static CustomBackOfficeAssetsCollectionBuilder BackOfficeAssets(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); + // all built-in finders in the correct order, + // devs can then modify this list on application startup + builder.ContentFinders() + .Append() + .Append() + .Append() + /*.Append() // disabled, this is an odd finder */ + .Append() + .Append(); + builder.EditorValidators().Add(() => builder.TypeLoader.GetTypes()); + builder.HealthChecks().Add(() => builder.TypeLoader.GetTypes()); + builder.HealthCheckNotificationMethods().Add(() => builder.TypeLoader.GetTypes()); + builder.TourFilters(); + builder.UrlProviders() + .Append() + .Append(); + builder.MediaUrlProviders() + .Append(); + // register back office sections in the order we want them rendered + builder.Sections() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + builder.Components(); + // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards + builder.Dashboards() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(builder.TypeLoader.GetTypes()); + builder.PartialViewSnippets(); + builder.PartialViewMacroSnippets(); + builder.DataValueReferenceFactories(); + builder.PropertyValueConverters().Append(builder.TypeLoader.GetTypes()); + builder.UrlSegmentProviders().Append(); + builder.ManifestValueValidators() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); + builder.ManifestFilters(); + builder.MediaUrlGenerators(); + // register OEmbed providers - no type scanning - all explicit opt-in of adding types, IEmbedProvider is not IDiscoverable + builder.EmbedProviders() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + builder.SearchableTrees().Add(() => builder.TypeLoader.GetTypes()); + builder.BackOfficeAssets(); } + + /// + /// Gets the actions collection builder. + /// + /// The builder. + public static ActionCollectionBuilder Actions(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the content apps collection builder. + /// + /// The builder. + public static ContentAppFactoryCollectionBuilder ContentApps(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the content finders collection builder. + /// + /// The builder. + public static ContentFinderCollectionBuilder ContentFinders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the editor validators collection builder. + /// + /// The builder. + public static EditorValidatorCollectionBuilder EditorValidators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the health checks collection builder. + /// + /// The builder. + public static HealthCheckCollectionBuilder HealthChecks(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + public static HealthCheckNotificationMethodCollectionBuilder HealthCheckNotificationMethods(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the TourFilters collection builder. + /// + public static TourFilterCollectionBuilder TourFilters(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the URL providers collection builder. + /// + /// The builder. + public static UrlProviderCollectionBuilder UrlProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the media url providers collection builder. + /// + /// The builder. + public static MediaUrlProviderCollectionBuilder MediaUrlProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the backoffice sections/applications collection builder. + /// + /// The builder. + public static SectionCollectionBuilder Sections(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the components collection builder. + /// + public static ComponentCollectionBuilder Components(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the backoffice dashboards collection builder. + /// + /// The builder. + public static DashboardCollectionBuilder Dashboards(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the partial view snippets collection builder. + /// + /// The builder. + public static PartialViewSnippetCollectionBuilder? PartialViewSnippets(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the partial view macro snippets collection builder. + /// + /// The builder. + public static PartialViewMacroSnippetCollectionBuilder? PartialViewMacroSnippets(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the cache refreshers collection builder. + /// + /// The builder. + public static CacheRefresherCollectionBuilder CacheRefreshers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the map definitions collection builder. + /// + /// The builder. + public static MapDefinitionCollectionBuilder MapDefinitions(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the data editor collection builder. + /// + /// The builder. + public static DataEditorCollectionBuilder DataEditors(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the data value reference factory collection builder. + /// + /// The builder. + public static DataValueReferenceFactoryCollectionBuilder DataValueReferenceFactories(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the property value converters collection builder. + /// + /// The builder. + public static PropertyValueConverterCollectionBuilder PropertyValueConverters(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the url segment providers collection builder. + /// + /// The builder. + public static UrlSegmentProviderCollectionBuilder UrlSegmentProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the validators collection builder. + /// + /// The builder. + internal static ManifestValueValidatorCollectionBuilder ManifestValueValidators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the manifest filter collection builder. + /// + /// The builder. + public static ManifestFilterCollectionBuilder ManifestFilters(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the content finders collection builder. + /// + /// The builder. + public static MediaUrlGeneratorCollectionBuilder MediaUrlGenerators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the backoffice OEmbed Providers collection builder. + /// + /// The builder. + [Obsolete("Use EmbedProviders() instead")] + public static EmbedProvidersCollectionBuilder OEmbedProviders(this IUmbracoBuilder builder) + => EmbedProviders(builder); + + /// + /// Gets the backoffice Embed Providers collection builder. + /// + /// The builder. + public static EmbedProvidersCollectionBuilder EmbedProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the back office searchable tree collection builder + /// + public static SearchableTreeCollectionBuilder SearchableTrees(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the back office custom assets collection builder + /// + public static CustomBackOfficeAssetsCollectionBuilder BackOfficeAssets(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Composers.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Composers.cs index 81a1bbac32..e3a659056b 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Composers.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Composers.cs @@ -1,26 +1,24 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// Extension methods for +/// +public static partial class UmbracoBuilderExtensions { /// - /// Extension methods for + /// Adds Umbraco composers for plugins /// - public static partial class UmbracoBuilderExtensions + public static IUmbracoBuilder AddComposers(this IUmbracoBuilder builder) { - /// - /// Adds Umbraco composers for plugins - /// - public static IUmbracoBuilder AddComposers(this IUmbracoBuilder builder) - { - IEnumerable composerTypes = builder.TypeLoader.GetTypes(); - IEnumerable enableDisable = builder.TypeLoader.GetAssemblyAttributes(typeof(EnableComposerAttribute), typeof(DisableComposerAttribute)); + IEnumerable composerTypes = builder.TypeLoader.GetTypes(); + IEnumerable enableDisable = + builder.TypeLoader.GetAssemblyAttributes(typeof(EnableComposerAttribute), typeof(DisableComposerAttribute)); - new ComposerGraph(builder, composerTypes, enableDisable, builder.BuilderLoggerFactory.CreateLogger()).Compose(); + new ComposerGraph(builder, composerTypes, enableDisable, builder.BuilderLoggerFactory.CreateLogger()).Compose(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index bce6a36da9..90e2e49c94 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -16,13 +16,13 @@ public static partial class UmbracoBuilderExtensions private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action>? configure = null) where TOptions : class { - var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); + UmbracoOptionsAttribute? umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); if (umbracoOptionsAttribute is null) { throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute."); } - var optionsBuilder = builder.Services.AddOptions() + OptionsBuilder? optionsBuilder = builder.Services.AddOptions() .Bind( builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs index 441bc836da..844c52a5ab 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs @@ -5,72 +5,80 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// Contains extensions methods for used for registering event handlers. +/// +public static partial class UmbracoBuilderExtensions { + /// + /// Registers a notification handler against the Umbraco service collection. + /// + /// The type of notification. + /// The type of notificiation handler. + /// The Umbraco builder. + /// The . + public static IUmbracoBuilder AddNotificationHandler( + this IUmbracoBuilder builder) + where TNotificationHandler : INotificationHandler + where TNotification : INotification + { + builder.Services.AddNotificationHandler(); + return builder; + } /// - /// Contains extensions methods for used for registering event handlers. + /// Registers a notification async handler against the Umbraco service collection. /// - public static partial class UmbracoBuilderExtensions + /// The type of notification. + /// The type of notification async handler. + /// The Umbraco builder. + /// The . + public static IUmbracoBuilder AddNotificationAsyncHandler( + this IUmbracoBuilder builder) + where TNotificationAsyncHandler : INotificationAsyncHandler + where TNotification : INotification { - /// - /// Registers a notification handler against the Umbraco service collection. - /// - /// The type of notification. - /// The type of notificiation handler. - /// The Umbraco builder. - /// The . - public static IUmbracoBuilder AddNotificationHandler(this IUmbracoBuilder builder) - where TNotificationHandler : INotificationHandler - where TNotification : INotification + builder.Services.AddNotificationAsyncHandler(); + return builder; + } + + internal static IServiceCollection AddNotificationHandler( + this IServiceCollection services) + where TNotificationHandler : INotificationHandler + where TNotification : INotification + { + // Register the handler as transient. This ensures that anything can be injected into it. + var descriptor = new UniqueServiceDescriptor( + typeof(INotificationHandler), + typeof(TNotificationHandler), + ServiceLifetime.Transient); + + if (!services.Contains(descriptor)) { - builder.Services.AddNotificationHandler(); - return builder; + services.Add(descriptor); } - /// - /// Registers a notification async handler against the Umbraco service collection. - /// - /// The type of notification. - /// The type of notification async handler. - /// The Umbraco builder. - /// The . - public static IUmbracoBuilder AddNotificationAsyncHandler(this IUmbracoBuilder builder) - where TNotificationAsyncHandler : INotificationAsyncHandler - where TNotification : INotification + return services; + } + + internal static IServiceCollection AddNotificationAsyncHandler( + this IServiceCollection services) + where TNotificationAsyncHandler : INotificationAsyncHandler + where TNotification : INotification + { + // Register the handler as transient. This ensures that anything can be injected into it. + var descriptor = new ServiceDescriptor( + typeof(INotificationAsyncHandler), + typeof(TNotificationAsyncHandler), + ServiceLifetime.Transient); + + if (!services.Contains(descriptor)) { - builder.Services.AddNotificationAsyncHandler(); - return builder; + services.Add(descriptor); } - internal static IServiceCollection AddNotificationHandler(this IServiceCollection services) - where TNotificationHandler : INotificationHandler - where TNotification : INotification - { - // Register the handler as transient. This ensures that anything can be injected into it. - var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler), typeof(TNotificationHandler), ServiceLifetime.Transient); - - if (!services.Contains(descriptor)) - { - services.Add(descriptor); - } - - return services; - } - - internal static IServiceCollection AddNotificationAsyncHandler(this IServiceCollection services) - where TNotificationAsyncHandler : INotificationAsyncHandler - where TNotification : INotification - { - // Register the handler as transient. This ensures that anything can be injected into it. - var descriptor = new ServiceDescriptor(typeof(INotificationAsyncHandler), typeof(TNotificationAsyncHandler), ServiceLifetime.Transient); - - if (!services.Contains(descriptor)) - { - services.Add(descriptor); - } - - return services; - } + return services; } } diff --git a/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs b/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs index 538f3f1dda..57a8bcfe99 100644 --- a/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs +++ b/src/Umbraco.Core/DependencyInjection/UniqueServiceDescriptor.cs @@ -1,58 +1,68 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Cms.Core.DependencyInjection +namespace Umbraco.Cms.Core.DependencyInjection; + +/// +/// A custom that supports unique checking +/// +/// +/// This is required because the default implementation doesn't implement Equals or GetHashCode. +/// see: https://github.com/dotnet/runtime/issues/47262 +/// +public sealed class UniqueServiceDescriptor : ServiceDescriptor, IEquatable { /// - /// A custom that supports unique checking + /// Initializes a new instance of the class. /// - /// - /// This is required because the default implementation doesn't implement Equals or GetHashCode. - /// see: https://github.com/dotnet/runtime/issues/47262 - /// - public sealed class UniqueServiceDescriptor : ServiceDescriptor, IEquatable + public UniqueServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime) + : base(serviceType, implementationType, lifetime) { - /// - /// Initializes a new instance of the class. - /// - public UniqueServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime) - : base(serviceType, implementationType, lifetime) + } + + /// + public bool Equals(UniqueServiceDescriptor? other) => other != null && Lifetime == other.Lifetime && + EqualityComparer.Default.Equals( + ServiceType, + other.ServiceType) && + EqualityComparer.Default.Equals( + ImplementationType, + other.ImplementationType) && + EqualityComparer.Default.Equals( + ImplementationInstance, other.ImplementationInstance) && + EqualityComparer?>.Default + .Equals( + ImplementationFactory, + other.ImplementationFactory); + + /// + public override bool Equals(object? obj) => Equals(obj as UniqueServiceDescriptor); + + /// + public override int GetHashCode() + { + var hashCode = 493849952; + hashCode = (hashCode * -1521134295) + Lifetime.GetHashCode(); + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ServiceType); + + if (ImplementationType is not null) { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationType); } - /// - public override bool Equals(object? obj) => Equals(obj as UniqueServiceDescriptor); - - /// - public bool Equals(UniqueServiceDescriptor? other) => other != null && Lifetime == other.Lifetime && EqualityComparer.Default.Equals(ServiceType, other.ServiceType) && EqualityComparer.Default.Equals(ImplementationType, other.ImplementationType) && EqualityComparer.Default.Equals(ImplementationInstance, other.ImplementationInstance) && EqualityComparer?>.Default.Equals(ImplementationFactory, other.ImplementationFactory); - - /// - public override int GetHashCode() + if (ImplementationInstance is not null) { - int hashCode = 493849952; - hashCode = (hashCode * -1521134295) + Lifetime.GetHashCode(); - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ServiceType); - - if (ImplementationType is not null) - { - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationType); - } - - if (ImplementationInstance is not null) - { - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationInstance); - } - - if (ImplementationFactory is not null) - { - hashCode = (hashCode * -1521134295) + EqualityComparer?>.Default.GetHashCode(ImplementationFactory); - } - - return hashCode; + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ImplementationInstance); } + + if (ImplementationFactory is not null) + { + hashCode = (hashCode * -1521134295) + + EqualityComparer?>.Default.GetHashCode(ImplementationFactory); + } + + return hashCode; } } diff --git a/src/Umbraco.Core/Deploy/ArtifactBase.cs b/src/Umbraco.Core/Deploy/ArtifactBase.cs index 200b47096d..cc2415f4cd 100644 --- a/src/Umbraco.Core/Deploy/ArtifactBase.cs +++ b/src/Umbraco.Core/Deploy/ArtifactBase.cs @@ -21,8 +21,6 @@ namespace Umbraco.Cms.Core.Deploy protected abstract string GetChecksum(); - #region Abstract implementation of IArtifactSignature - Udi IArtifactSignature.Udi => Udi; public TUdi Udi { get; set; } @@ -45,8 +43,6 @@ namespace Umbraco.Cms.Core.Deploy set => _dependencies = value.OrderBy(x => x.Udi); } - #endregion - public string Name { get; set; } public string Alias { get; set; } = string.Empty; diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs index 618400e395..07ba917dc2 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependency.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs @@ -1,40 +1,42 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represents an artifact dependency. +/// +/// +/// Dependencies have an order property which indicates whether it must be respected when ordering artifacts. +/// +/// Dependencies have a mode which can be Match or Exist depending on whether the checksum should +/// match. +/// +/// +public class ArtifactDependency { /// - /// Represents an artifact dependency. + /// Initializes a new instance of the ArtifactDependency class with an entity identifier and a mode. /// - /// - /// Dependencies have an order property which indicates whether it must be respected when ordering artifacts. - /// Dependencies have a mode which can be Match or Exist depending on whether the checksum should match. - /// - public class ArtifactDependency + /// The entity identifier of the artifact that is a dependency. + /// A value indicating whether the dependency is ordering. + /// The dependency mode. + public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode) { - /// - /// Initializes a new instance of the ArtifactDependency class with an entity identifier and a mode. - /// - /// The entity identifier of the artifact that is a dependency. - /// A value indicating whether the dependency is ordering. - /// The dependency mode. - public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode) - { - Udi = udi; - Ordering = ordering; - Mode = mode; - } - - /// - /// Gets the entity id of the artifact that is a dependency. - /// - public Udi Udi { get; private set; } - - /// - /// Gets a value indicating whether the dependency is ordering. - /// - public bool Ordering { get; private set; } - - /// - /// Gets the dependency mode. - /// - public ArtifactDependencyMode Mode { get; private set; } + Udi = udi; + Ordering = ordering; + Mode = mode; } + + /// + /// Gets the entity id of the artifact that is a dependency. + /// + public Udi Udi { get; } + + /// + /// Gets a value indicating whether the dependency is ordering. + /// + public bool Ordering { get; } + + /// + /// Gets the dependency mode. + /// + public ArtifactDependencyMode Mode { get; } } diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs index a5fff53800..1be524c86f 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs @@ -1,69 +1,44 @@ -using System; using System.Collections; -using System.Collections.Generic; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represents a collection of distinct . +/// +/// The collection cannot contain duplicates and modes are properly managed. +public class ArtifactDependencyCollection : ICollection { - /// - /// Represents a collection of distinct . - /// - /// The collection cannot contain duplicates and modes are properly managed. - public class ArtifactDependencyCollection : ICollection + private readonly Dictionary _dependencies = new(); + + public int Count => _dependencies.Count; + + public IEnumerator GetEnumerator() => _dependencies.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(ArtifactDependency item) { - private readonly Dictionary _dependencies - = new Dictionary(); - - public IEnumerator GetEnumerator() + if (_dependencies.ContainsKey(item.Udi)) { - return _dependencies.Values.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public void Add(ArtifactDependency item) - { - if (_dependencies.ContainsKey(item.Udi)) + ArtifactDependency exist = _dependencies[item.Udi]; + if (item.Mode == ArtifactDependencyMode.Exist || item.Mode == exist.Mode) { - var exist = _dependencies[item.Udi]; - if (item.Mode == ArtifactDependencyMode.Exist || item.Mode == exist.Mode) - return; + return; } - - _dependencies[item.Udi] = item; } - public void Clear() - { - _dependencies.Clear(); - } - - public bool Contains(ArtifactDependency item) - { - return _dependencies.ContainsKey(item.Udi) && - (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); - } - - public void CopyTo(ArtifactDependency[] array, int arrayIndex) - { - _dependencies.Values.CopyTo(array, arrayIndex); - } - - public bool Remove(ArtifactDependency item) - { - throw new NotSupportedException(); - } - - public int Count - { - get { return _dependencies.Count; } - } - - public bool IsReadOnly - { - get { return false; } - } + _dependencies[item.Udi] = item; } + + public void Clear() => _dependencies.Clear(); + + public bool Contains(ArtifactDependency item) => + _dependencies.ContainsKey(item.Udi) && + (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); + + public void CopyTo(ArtifactDependency[] array, int arrayIndex) => _dependencies.Values.CopyTo(array, arrayIndex); + + public bool Remove(ArtifactDependency item) => throw new NotSupportedException(); + + public bool IsReadOnly => false; } diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs index 7a2d108a13..b997b9c759 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Indicates the mode of the dependency. +/// +public enum ArtifactDependencyMode { /// - /// Indicates the mode of the dependency. + /// The dependency must match exactly. /// - public enum ArtifactDependencyMode - { - /// - /// The dependency must match exactly. - /// - Match, + Match, - /// - /// The dependency must exist. - /// - Exist - } + /// + /// The dependency must exist. + /// + Exist, } diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs index 0849f3526f..1b75fe11c0 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs @@ -1,47 +1,46 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represent the state of an artifact being deployed. +/// +public abstract class ArtifactDeployState { /// - /// Represent the state of an artifact being deployed. + /// Gets the artifact. /// - public abstract class ArtifactDeployState - { - /// - /// Creates a new instance of the class from an artifact and an entity. - /// - /// The type of the artifact. - /// The type of the entity. - /// The artifact. - /// The entity. - /// The service connector deploying the artifact. - /// The next pass number. - /// A deploying artifact. - public static ArtifactDeployState Create(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass) - where TArtifact : IArtifact - { - return new ArtifactDeployState(art, entity, connector, nextPass); - } + public IArtifact Artifact => GetArtifactAsIArtifact(); - /// - /// Gets the artifact. - /// - public IArtifact Artifact => GetArtifactAsIArtifact(); + /// + /// Gets or sets the service connector in charge of deploying the artifact. + /// + public IServiceConnector? Connector { get; set; } - /// - /// Gets the artifact as an . - /// - /// The artifact, as an . - /// This is because classes that inherit from this class cannot override the Artifact property - /// with a property that specializes the return type, and so they need to 'new' the property. - protected abstract IArtifact GetArtifactAsIArtifact(); + /// + /// Gets or sets the next pass number. + /// + public int NextPass { get; set; } - /// - /// Gets or sets the service connector in charge of deploying the artifact. - /// - public IServiceConnector? Connector { get; set; } + /// + /// Creates a new instance of the class from an artifact and an entity. + /// + /// The type of the artifact. + /// The type of the entity. + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + /// A deploying artifact. + public static ArtifactDeployState Create(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass) + where TArtifact : IArtifact => + new ArtifactDeployState(art, entity, connector, nextPass); - /// - /// Gets or sets the next pass number. - /// - public int NextPass { get; set; } - } + /// + /// Gets the artifact as an . + /// + /// The artifact, as an . + /// + /// This is because classes that inherit from this class cannot override the Artifact property + /// with a property that specializes the return type, and so they need to 'new' the property. + /// + protected abstract IArtifact GetArtifactAsIArtifact(); } diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs index 72724ee57b..0ff1e20e87 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs @@ -1,42 +1,38 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represent the state of an artifact being deployed. +/// +/// The type of the artifact. +/// The type of the entity. +public class ArtifactDeployState : ArtifactDeployState + where TArtifact : IArtifact { /// - /// Represent the state of an artifact being deployed. + /// Initializes a new instance of the class. /// - /// The type of the artifact. - /// The type of the entity. - public class ArtifactDeployState : ArtifactDeployState - where TArtifact : IArtifact + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + public ArtifactDeployState(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass) { - /// - /// Initializes a new instance of the class. - /// - /// The artifact. - /// The entity. - /// The service connector deploying the artifact. - /// The next pass number. - public ArtifactDeployState(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass) - { - Artifact = art; - Entity = entity; - Connector = connector; - NextPass = nextPass; - } - - /// - /// Gets or sets the artifact. - /// - public new TArtifact Artifact { get; set; } - - /// - /// Gets or sets the entity. - /// - public TEntity? Entity { get; set; } - - /// - protected sealed override IArtifact GetArtifactAsIArtifact() - { - return Artifact; - } + Artifact = art; + Entity = entity; + Connector = connector; + NextPass = nextPass; } + + /// + /// Gets or sets the artifact. + /// + public new TArtifact Artifact { get; set; } + + /// + /// Gets or sets the entity. + /// + public TEntity? Entity { get; set; } + + /// + protected sealed override IArtifact GetArtifactAsIArtifact() => Artifact; } diff --git a/src/Umbraco.Core/Deploy/ArtifactSignature.cs b/src/Umbraco.Core/Deploy/ArtifactSignature.cs index 629d65593c..3dccddba29 100644 --- a/src/Umbraco.Core/Deploy/ArtifactSignature.cs +++ b/src/Umbraco.Core/Deploy/ArtifactSignature.cs @@ -1,21 +1,17 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +public sealed class ArtifactSignature : IArtifactSignature { - public sealed class ArtifactSignature : IArtifactSignature + public ArtifactSignature(Udi udi, string checksum, IEnumerable? dependencies = null) { - public ArtifactSignature(Udi udi, string checksum, IEnumerable? dependencies = null) - { - Udi = udi; - Checksum = checksum; - Dependencies = dependencies ?? Enumerable.Empty(); - } - - public Udi Udi { get; private set; } - - public string Checksum { get; private set; } - - public IEnumerable Dependencies { get; private set; } + Udi = udi; + Checksum = checksum; + Dependencies = dependencies ?? Enumerable.Empty(); } + + public Udi Udi { get; } + + public string Checksum { get; } + + public IEnumerable Dependencies { get; } } diff --git a/src/Umbraco.Core/Deploy/Difference.cs b/src/Umbraco.Core/Deploy/Difference.cs index be0c086c0b..d704642a9f 100644 --- a/src/Umbraco.Core/Deploy/Difference.cs +++ b/src/Umbraco.Core/Deploy/Difference.cs @@ -1,28 +1,38 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +public class Difference { - public class Difference + public Difference(string title, string? text = null, string? category = null) { - public Difference(string title, string? text = null, string? category = null) + Title = title; + Text = text; + Category = category; + } + + public string Title { get; set; } + + public string? Text { get; set; } + + public string? Category { get; set; } + + public override string ToString() + { + var s = Title; + if (!string.IsNullOrWhiteSpace(Category)) { - Title = title; - Text = text; - Category = category; + s += string.Format("[{0}]", Category); } - public string Title { get; set; } - public string? Text { get; set; } - public string? Category { get; set; } - - public override string ToString() + if (!string.IsNullOrWhiteSpace(Text)) { - var s = Title; - if (!string.IsNullOrWhiteSpace(Category)) s += string.Format("[{0}]", Category); - if (!string.IsNullOrWhiteSpace(Text)) + if (s.Length > 0) { - if (s.Length > 0) s += ":"; - s += Text; + s += ":"; } - return s; + + s += Text; } + + return s; } } diff --git a/src/Umbraco.Core/Deploy/Direction.cs b/src/Umbraco.Core/Deploy/Direction.cs index 7a6ee5ae09..30439380f2 100644 --- a/src/Umbraco.Core/Deploy/Direction.cs +++ b/src/Umbraco.Core/Deploy/Direction.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +public enum Direction { - public enum Direction - { - ToArtifact, - FromArtifact - } + ToArtifact, + FromArtifact, } diff --git a/src/Umbraco.Core/Deploy/IArtifact.cs b/src/Umbraco.Core/Deploy/IArtifact.cs index 5eb9c079f3..faea983dee 100644 --- a/src/Umbraco.Core/Deploy/IArtifact.cs +++ b/src/Umbraco.Core/Deploy/IArtifact.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Represents an artifact ie an object that can be transfered between environments. +/// +public interface IArtifact : IArtifactSignature { - /// - /// Represents an artifact ie an object that can be transfered between environments. - /// - public interface IArtifact : IArtifactSignature - { - string Name { get; } - string? Alias { get; } - } + string Name { get; } + + string? Alias { get; } } diff --git a/src/Umbraco.Core/Deploy/IArtifactSignature.cs b/src/Umbraco.Core/Deploy/IArtifactSignature.cs index 695624cd86..f1dd35295f 100644 --- a/src/Umbraco.Core/Deploy/IArtifactSignature.cs +++ b/src/Umbraco.Core/Deploy/IArtifactSignature.cs @@ -1,41 +1,46 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +/// +/// Represents the signature of an artifact. +/// +public interface IArtifactSignature { /// - /// Represents the signature of an artifact. + /// Gets the entity unique identifier of this artifact. /// - public interface IArtifactSignature - { - /// - /// Gets the entity unique identifier of this artifact. - /// - /// - /// The project identifier is independent from the state of the artifact, its data - /// values, dependencies, anything. It never changes and fully identifies the artifact. - /// What an entity uses as a unique identifier will influence what we can transfer - /// between environments. Eg content type "Foo" on one environment is not necessarily the - /// same as "Foo" on another environment, if guids are used as unique identifiers. What is - /// used should be documented for each entity, along with the consequences of the choice. - /// - Udi Udi { get; } + /// + /// + /// The project identifier is independent from the state of the artifact, its data + /// values, dependencies, anything. It never changes and fully identifies the artifact. + /// + /// + /// What an entity uses as a unique identifier will influence what we can transfer + /// between environments. Eg content type "Foo" on one environment is not necessarily the + /// same as "Foo" on another environment, if guids are used as unique identifiers. What is + /// used should be documented for each entity, along with the consequences of the choice. + /// + /// + Udi Udi { get; } - /// - /// Gets the checksum of this artifact. - /// - /// - /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies, - /// but not on their checksums. So the checksum changes when any of the artifact's properties changes, - /// or when the list of dependencies changes. But not if one of these dependencies change. - /// It is assumed that checksum collisions cannot happen ie that no two different artifact's - /// states will ever produce the same checksum, so that if two artifacts have the same checksum then - /// they are identical. - /// - string Checksum { get; } + /// + /// Gets the checksum of this artifact. + /// + /// + /// + /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies, + /// but not on their checksums. So the checksum changes when any of the artifact's properties changes, + /// or when the list of dependencies changes. But not if one of these dependencies change. + /// + /// + /// It is assumed that checksum collisions cannot happen ie that no two different artifact's + /// states will ever produce the same checksum, so that if two artifacts have the same checksum then + /// they are identical. + /// + /// + string Checksum { get; } - /// - /// Gets the dependencies of this artifact. - /// - IEnumerable Dependencies { get; } - } + /// + /// Gets the dependencies of this artifact. + /// + IEnumerable Dependencies { get; } } diff --git a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs index 87a00e7969..6b91926b57 100644 --- a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs +++ b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs @@ -1,34 +1,37 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Defines methods that can convert data type configuration to / from an environment-agnostic string. +/// +/// +/// Configuration may contain values such as content identifiers, that would be local +/// to one environment, and need to be converted in order to be deployed. +/// +[SuppressMessage( + "ReSharper", + "UnusedMember.Global", + Justification = "This is actual only used by Deploy, but we don't want third parties to have references on deploy, that's why this interface is part of core.")] +public interface IDataTypeConfigurationConnector { /// - /// Defines methods that can convert data type configuration to / from an environment-agnostic string. + /// Gets the property editor aliases that the value converter supports by default. /// - /// Configuration may contain values such as content identifiers, that would be local - /// to one environment, and need to be converted in order to be deployed. - [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is actual only used by Deploy, but we don't want third parties to have references on deploy, that's why this interface is part of core.")] - public interface IDataTypeConfigurationConnector - { - /// - /// Gets the property editor aliases that the value converter supports by default. - /// - IEnumerable PropertyEditorAliases { get; } + IEnumerable PropertyEditorAliases { get; } - /// - /// Gets the artifact datatype configuration corresponding to the actual datatype configuration. - /// - /// The datatype. - /// The dependencies. - string? ToArtifact(IDataType dataType, ICollection dependencies); + /// + /// Gets the artifact datatype configuration corresponding to the actual datatype configuration. + /// + /// The datatype. + /// The dependencies. + string? ToArtifact(IDataType dataType, ICollection dependencies); - /// - /// Gets the actual datatype configuration corresponding to the artifact configuration. - /// - /// The datatype. - /// The artifact configuration. - object? FromArtifact(IDataType dataType, string? configuration); - } + /// + /// Gets the actual datatype configuration corresponding to the artifact configuration. + /// + /// The datatype. + /// The artifact configuration. + object? FromArtifact(IDataType dataType, string? configuration); } diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs index c6e2da997b..bdc8fd8d61 100644 --- a/src/Umbraco.Core/Deploy/IDeployContext.cs +++ b/src/Umbraco.Core/Deploy/IDeployContext.cs @@ -1,47 +1,44 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +/// +/// Represents a deployment context. +/// +public interface IDeployContext { /// - /// Represents a deployment context. + /// Gets the unique identifier of the deployment. /// - public interface IDeployContext - { - /// - /// Gets the unique identifier of the deployment. - /// - Guid SessionId { get; } + Guid SessionId { get; } - /// - /// Gets the file source. - /// - /// The file source is used to obtain files from the source environment. - IFileSource FileSource { get; } + /// + /// Gets the file source. + /// + /// The file source is used to obtain files from the source environment. + IFileSource FileSource { get; } - /// - /// Gets the next number in a numerical sequence. - /// - /// The next sequence number. - /// Can be used to uniquely number things during a deployment. - int NextSeq(); + /// + /// Gets items. + /// + IDictionary Items { get; } - /// - /// Gets items. - /// - IDictionary Items { get; } + /// + /// Gets the next number in a numerical sequence. + /// + /// The next sequence number. + /// Can be used to uniquely number things during a deployment. + int NextSeq(); - /// - /// Gets item. - /// - /// The type of the item. - /// The key of the item. - /// The item with the specified key and type, if any, else null. - T? Item(string key) where T : class; + /// + /// Gets item. + /// + /// The type of the item. + /// The key of the item. + /// The item with the specified key and type, if any, else null. + T? Item(string key) + where T : class; - ///// - ///// Gets the global deployment cancellation token. - ///// - //CancellationToken CancellationToken { get; } - } + ///// + ///// Gets the global deployment cancellation token. + ///// + // CancellationToken CancellationToken { get; } } diff --git a/src/Umbraco.Core/Deploy/IFileSource.cs b/src/Umbraco.Core/Deploy/IFileSource.cs index 6e582803a2..ed169b9df5 100644 --- a/src/Umbraco.Core/Deploy/IFileSource.cs +++ b/src/Umbraco.Core/Deploy/IFileSource.cs @@ -1,91 +1,85 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +/// +/// Represents a file source, ie a mean for a target environment involved in a +/// deployment to obtain the content of files being deployed. +/// +public interface IFileSource { /// - /// Represents a file source, ie a mean for a target environment involved in a - /// deployment to obtain the content of files being deployed. + /// Gets the content of a file as a stream. /// - public interface IFileSource - { - /// - /// Gets the content of a file as a stream. - /// - /// A file entity identifier. - /// A stream with read access to the file content. - /// - /// Returns null if no content could be read. - /// The caller should ensure that the stream is properly closed/disposed. - /// - Stream GetFileStream(StringUdi udi); + /// A file entity identifier. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Stream GetFileStream(StringUdi udi); - /// - /// Gets the content of a file as a stream. - /// - /// A file entity identifier. - /// A cancellation token. - /// A stream with read access to the file content. - /// - /// Returns null if no content could be read. - /// The caller should ensure that the stream is properly closed/disposed. - /// - Task GetFileStreamAsync(StringUdi udi, CancellationToken token); + /// + /// Gets the content of a file as a stream. + /// + /// A file entity identifier. + /// A cancellation token. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Task GetFileStreamAsync(StringUdi udi, CancellationToken token); - /// - /// Gets the content of a file as a string. - /// - /// A file entity identifier. - /// A string containing the file content. - /// Returns null if no content could be read. - string GetFileContent(StringUdi udi); + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A string containing the file content. + /// Returns null if no content could be read. + string GetFileContent(StringUdi udi); - /// - /// Gets the content of a file as a string. - /// - /// A file entity identifier. - /// A cancellation token. - /// A string containing the file content. - /// Returns null if no content could be read. - Task GetFileContentAsync(StringUdi udi, CancellationToken token); + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A cancellation token. + /// A string containing the file content. + /// Returns null if no content could be read. + Task GetFileContentAsync(StringUdi udi, CancellationToken token); - /// - /// Gets the length of a file. - /// - /// A file entity identifier. - /// The length of the file, or -1 if the file does not exist. - long GetFileLength(StringUdi udi); + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// The length of the file, or -1 if the file does not exist. + long GetFileLength(StringUdi udi); - /// - /// Gets the length of a file. - /// - /// A file entity identifier. - /// A cancellation token. - /// The length of the file, or -1 if the file does not exist. - Task GetFileLengthAsync(StringUdi udi, CancellationToken token); + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// A cancellation token. + /// The length of the file, or -1 if the file does not exist. + Task GetFileLengthAsync(StringUdi udi, CancellationToken token); - /// - /// Gets files and store them using a file store. - /// - /// The udis of the files to get. - /// A collection of file types which can store the files. - void GetFiles(IEnumerable udis, IFileTypeCollection fileTypes); + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + void GetFiles(IEnumerable udis, IFileTypeCollection fileTypes); - /// - /// Gets files and store them using a file store. - /// - /// The udis of the files to get. - /// A collection of file types which can store the files. - /// A cancellation token. - Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, CancellationToken token); + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + /// A cancellation token. + Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, CancellationToken token); - ///// - ///// Gets the content of a file as a bytes array. - ///// - ///// A file entity identifier. - ///// A byte array containing the file content. - //byte[] GetFileBytes(StringUdi Udi); - } + ///// + ///// Gets the content of a file as a bytes array. + ///// + ///// A file entity identifier. + ///// A byte array containing the file content. + // byte[] GetFileBytes(StringUdi Udi); } diff --git a/src/Umbraco.Core/Deploy/IFileType.cs b/src/Umbraco.Core/Deploy/IFileType.cs index ef6c44e1e6..466c87a3ed 100644 --- a/src/Umbraco.Core/Deploy/IFileType.cs +++ b/src/Umbraco.Core/Deploy/IFileType.cs @@ -1,32 +1,27 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +public interface IFileType { - public interface IFileType - { - Stream GetStream(StringUdi udi); + bool CanSetPhysical { get; } - Task GetStreamAsync(StringUdi udi, CancellationToken token); + Stream GetStream(StringUdi udi); - Stream GetChecksumStream(StringUdi udi); + Task GetStreamAsync(StringUdi udi, CancellationToken token); - long GetLength(StringUdi udi); + Stream GetChecksumStream(StringUdi udi); - void SetStream(StringUdi udi, Stream stream); + long GetLength(StringUdi udi); - Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token); + void SetStream(StringUdi udi, Stream stream); - bool CanSetPhysical { get; } + Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token); - void Set(StringUdi udi, string physicalPath, bool copy = false); + void Set(StringUdi udi, string physicalPath, bool copy = false); - // this is not pretty as *everywhere* in Deploy we take care of ignoring - // the physical path and always rely on Core's virtual IFileSystem but - // Cloud wants to add some of these files to Git and needs the path... - string GetPhysicalPath(StringUdi udi); + // this is not pretty as *everywhere* in Deploy we take care of ignoring + // the physical path and always rely on Core's virtual IFileSystem but + // Cloud wants to add some of these files to Git and needs the path... + string GetPhysicalPath(StringUdi udi); - string GetVirtualPath(StringUdi udi); - } + string GetVirtualPath(StringUdi udi); } diff --git a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs index d19d2ad64a..2ae2bb4bb9 100644 --- a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs +++ b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Deploy -{ - public interface IFileTypeCollection - { - IFileType this[string entityType] { get; } +namespace Umbraco.Cms.Core.Deploy; - bool Contains(string entityType); - } +public interface IFileTypeCollection +{ + IFileType this[string entityType] { get; } + + bool Contains(string entityType); } diff --git a/src/Umbraco.Core/Deploy/IImageSourceParser.cs b/src/Umbraco.Core/Deploy/IImageSourceParser.cs index 084ba1b118..7b9e3f5e96 100644 --- a/src/Umbraco.Core/Deploy/IImageSourceParser.cs +++ b/src/Umbraco.Core/Deploy/IImageSourceParser.cs @@ -1,25 +1,24 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Provides methods to parse image tag sources in property values. +/// +public interface IImageSourceParser { /// - /// Provides methods to parse image tag sources in property values. + /// Parses an Umbraco property value and produces an artifact property value. /// - public interface IImageSourceParser - { - /// - /// Parses an Umbraco property value and produces an artifact property value. - /// - /// The property value. - /// A list of dependencies. - /// The parsed value. - /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. - string? ToArtifact(string? value, ICollection dependencies); + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. + string? ToArtifact(string? value, ICollection dependencies); - /// - /// Parses an artifact property value and produces an Umbraco property value. - /// - /// The artifact property value. - /// The parsed value. - /// Turns umb://media/... into /media/.... - string? FromArtifact(string? value); - } + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns umb://media/... into /media/.... + string? FromArtifact(string? value); } diff --git a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs index 5883f73217..7ec3fff0fa 100644 --- a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs +++ b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs @@ -1,25 +1,27 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Provides methods to parse local link tags in property values. +/// +public interface ILocalLinkParser { /// - /// Provides methods to parse local link tags in property values. + /// Parses an Umbraco property value and produces an artifact property value. /// - public interface ILocalLinkParser - { - /// - /// Parses an Umbraco property value and produces an artifact property value. - /// - /// The property value. - /// A list of dependencies. - /// The parsed value. - /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the dependencies. - string ToArtifact(string value, ICollection dependencies); + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// + /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the + /// dependencies. + /// + string ToArtifact(string value, ICollection dependencies); - /// - /// Parses an artifact property value and produces an Umbraco property value. - /// - /// The artifact property value. - /// The parsed value. - /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. - string FromArtifact(string value); - } + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. + string FromArtifact(string value); } diff --git a/src/Umbraco.Core/Deploy/IMacroParser.cs b/src/Umbraco.Core/Deploy/IMacroParser.cs index 81b014c1cc..1945b2bdb3 100644 --- a/src/Umbraco.Core/Deploy/IMacroParser.cs +++ b/src/Umbraco.Core/Deploy/IMacroParser.cs @@ -1,32 +1,29 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Deploy; -namespace Umbraco.Cms.Core.Deploy +public interface IMacroParser { - public interface IMacroParser - { - /// - /// Parses an Umbraco property value and produces an artifact property value. - /// - /// Property value. - /// A list of dependencies. - /// Parsed value. - string? ToArtifact(string? value, ICollection dependencies); + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// Property value. + /// A list of dependencies. + /// Parsed value. + string? ToArtifact(string? value, ICollection dependencies); - /// - /// Parses an artifact property value and produces an Umbraco property value. - /// - /// Artifact property value. - /// Parsed value. - string? FromArtifact(string? value); + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// Artifact property value. + /// Parsed value. + string? FromArtifact(string? value); - /// - /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier. - /// - /// Value to attempt to convert - /// Alias of the editor used for the parameter - /// Collection to add dependencies to when performing ToArtifact - /// Indicates which action is being performed (to or from artifact) - /// Value with converted identifiers - string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction); - } + /// + /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier. + /// + /// Value to attempt to convert + /// Alias of the editor used for the parameter + /// Collection to add dependencies to when performing ToArtifact + /// Indicates which action is being performed (to or from artifact) + /// Value with converted identifiers + string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction); } diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs index 3f789e2e38..f6cd7c8002 100644 --- a/src/Umbraco.Core/Deploy/IServiceConnector.cs +++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs @@ -1,84 +1,83 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Connects to an Umbraco service. +/// +public interface IServiceConnector : IDiscoverable { /// - /// Connects to an Umbraco service. + /// Gets an artifact. /// - public interface IServiceConnector : IDiscoverable - { - /// - /// Gets an artifact. - /// - /// The entity identifier of the artifact. - /// The corresponding artifact, or null. - IArtifact? GetArtifact(Udi udi); + /// The entity identifier of the artifact. + /// The corresponding artifact, or null. + IArtifact? GetArtifact(Udi udi); - /// - /// Gets an artifact. - /// - /// The entity. - /// The corresponding artifact. - IArtifact GetArtifact(object entity); + /// + /// Gets an artifact. + /// + /// The entity. + /// The corresponding artifact. + IArtifact GetArtifact(object entity); - /// - /// Initializes processing for an artifact. - /// - /// The artifact. - /// The deploy context. - /// The mapped artifact. - ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); + /// + /// Initializes processing for an artifact. + /// + /// The artifact. + /// The deploy context. + /// The mapped artifact. + ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); - /// - /// Processes an artifact. - /// - /// The mapped artifact. - /// The deploy context. - /// The processing pass number. - void Process(ArtifactDeployState dart, IDeployContext context, int pass); + /// + /// Processes an artifact. + /// + /// The mapped artifact. + /// The deploy context. + /// The processing pass number. + void Process(ArtifactDeployState dart, IDeployContext context, int pass); - /// - /// Explodes a range into udis. - /// - /// The range. - /// The list of udis where to add the new udis. - /// Also, it's cool to have a method named Explode. Kaboom! - void Explode(UdiRange range, List udis); + /// + /// Explodes a range into udis. + /// + /// The range. + /// The list of udis where to add the new udis. + /// Also, it's cool to have a method named Explode. Kaboom! + void Explode(UdiRange range, List udis); - /// - /// Gets a named range for a specified udi and selector. - /// - /// The udi. - /// The selector. - /// The named range for the specified udi and selector. - NamedUdiRange GetRange(Udi udi, string selector); + /// + /// Gets a named range for a specified udi and selector. + /// + /// The udi. + /// The selector. + /// The named range for the specified udi and selector. + NamedUdiRange GetRange(Udi udi, string selector); - /// - /// Gets a named range for specified entity type, identifier and selector. - /// - /// The entity type. - /// The identifier. - /// The selector. - /// The named range for the specified entity type, identifier and selector. - /// - /// This is temporary. At least we thought it would be, in sept. 2016. What day is it now? - /// At the moment our UI has a hard time returning proper udis, mainly because Core's tree do - /// not manage guids but only ints... so we have to provide a way to support it. The string id here - /// can be either a real string (for string udis) or an "integer as a string", using the value "-1" to - /// indicate the "root" i.e. an open udi. - /// - NamedUdiRange GetRange(string entityType, string sid, string selector); - - /// - /// Compares two artifacts. - /// - /// The first artifact. - /// The second artifact. - /// A collection of differences to append to, if not null. - /// A boolean value indicating whether the artifacts are identical. - /// ServiceConnectorBase{TArtifact} provides a very basic default implementation. - bool Compare(IArtifact? art1, IArtifact? art2, ICollection? differences = null); - } + /// + /// Gets a named range for specified entity type, identifier and selector. + /// + /// The entity type. + /// The identifier. + /// The selector. + /// The named range for the specified entity type, identifier and selector. + /// + /// This is temporary. At least we thought it would be, in sept. 2016. What day is it now? + /// + /// At the moment our UI has a hard time returning proper udis, mainly because Core's tree do + /// not manage guids but only ints... so we have to provide a way to support it. The string id here + /// can be either a real string (for string udis) or an "integer as a string", using the value "-1" to + /// indicate the "root" i.e. an open udi. + /// + /// + NamedUdiRange GetRange(string entityType, string sid, string selector); + /// + /// Compares two artifacts. + /// + /// The first artifact. + /// The second artifact. + /// A collection of differences to append to, if not null. + /// A boolean value indicating whether the artifacts are identical. + /// ServiceConnectorBase{TArtifact} provides a very basic default implementation. + bool Compare(IArtifact? art1, IArtifact? art2, ICollection? differences = null); } diff --git a/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs b/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs index 66364a08f3..c68906bbbf 100644 --- a/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs +++ b/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs @@ -1,25 +1,24 @@ -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Provides a method to retrieve an artifact's unique identifier. +/// +/// +/// Artifacts are uniquely identified by their , however they represent +/// elements in Umbraco that may be uniquely identified by another value. For example, +/// a content type is uniquely identified by its alias. If someone creates a new content +/// type, and tries to deploy it to a remote environment where a content type with the +/// same alias already exists, both content types end up having different +/// but the same alias. By default, Deploy would fail and throw when trying to save the +/// new content type (duplicate alias). However, if the connector also implements this +/// interface, the situation can be detected beforehand and reported in a nicer way. +/// +public interface IUniqueIdentifyingServiceConnector { /// - /// Provides a method to retrieve an artifact's unique identifier. + /// Gets the unique identifier of the specified artifact. /// - /// - /// Artifacts are uniquely identified by their , however they represent - /// elements in Umbraco that may be uniquely identified by another value. For example, - /// a content type is uniquely identified by its alias. If someone creates a new content - /// type, and tries to deploy it to a remote environment where a content type with the - /// same alias already exists, both content types end up having different - /// but the same alias. By default, Deploy would fail and throw when trying to save the - /// new content type (duplicate alias). However, if the connector also implements this - /// interface, the situation can be detected beforehand and reported in a nicer way. - /// - public interface IUniqueIdentifyingServiceConnector - { - /// - /// Gets the unique identifier of the specified artifact. - /// - /// The artifact. - /// The unique identifier. - string GetUniqueIdentifier(IArtifact artifact); - } + /// The artifact. + /// The unique identifier. + string GetUniqueIdentifier(IArtifact artifact); } diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs index 2c684f2ccd..f2a776c7ca 100644 --- a/src/Umbraco.Core/Deploy/IValueConnector.cs +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -1,37 +1,37 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Defines methods that can convert a property value to / from an environment-agnostic string. +/// +/// +/// Property values may contain values such as content identifiers, that would be local +/// to one environment, and need to be converted in order to be deployed. Connectors also deal +/// with serializing to / from string. +/// +public interface IValueConnector { /// - /// Defines methods that can convert a property value to / from an environment-agnostic string. + /// Gets the property editor aliases that the value converter supports by default. /// - /// Property values may contain values such as content identifiers, that would be local - /// to one environment, and need to be converted in order to be deployed. Connectors also deal - /// with serializing to / from string. - public interface IValueConnector - { - /// - /// Gets the property editor aliases that the value converter supports by default. - /// - IEnumerable PropertyEditorAliases { get; } + IEnumerable PropertyEditorAliases { get; } - /// - /// Gets the deploy property value corresponding to a content property value, and gather dependencies. - /// - /// The content property value. - /// The value property type - /// The content dependencies. - /// The deploy property value. - string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies); + /// + /// Gets the deploy property value corresponding to a content property value, and gather dependencies. + /// + /// The content property value. + /// The value property type + /// The content dependencies. + /// The deploy property value. + string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies); - /// - /// Gets the content property value corresponding to a deploy property value. - /// - /// The deploy property value. - /// The value property type< - /// The current content property value. - /// The content property value. - object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue); - } + /// + /// Gets the content property value corresponding to a deploy property value. + /// + /// The deploy property value. + /// The value property type + /// The current content property value. + /// The content property value. + object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue); } diff --git a/src/Umbraco.Core/Diagnostics/IMarchal.cs b/src/Umbraco.Core/Diagnostics/IMarchal.cs index 988eaca78c..304ff22c5a 100644 --- a/src/Umbraco.Core/Diagnostics/IMarchal.cs +++ b/src/Umbraco.Core/Diagnostics/IMarchal.cs @@ -1,16 +1,15 @@ -using System; +namespace Umbraco.Cms.Core.Diagnostics; -namespace Umbraco.Cms.Core.Diagnostics +/// +/// Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting +/// managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code. +/// +public interface IMarchal { /// - /// Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code. + /// Retrieves a computer-independent description of an exception, and information about the state that existed for the + /// thread when the exception occurred. /// - public interface IMarchal - { - /// - /// Retrieves a computer-independent description of an exception, and information about the state that existed for the thread when the exception occurred. - /// - /// A pointer to an EXCEPTION_POINTERS structure. - IntPtr GetExceptionPointers(); - } + /// A pointer to an EXCEPTION_POINTERS structure. + IntPtr GetExceptionPointers(); } diff --git a/src/Umbraco.Core/Diagnostics/MiniDump.cs b/src/Umbraco.Core/Diagnostics/MiniDump.cs index 25f6e530e1..ac37c69f12 100644 --- a/src/Umbraco.Core/Diagnostics/MiniDump.cs +++ b/src/Umbraco.Core/Diagnostics/MiniDump.cs @@ -1,145 +1,158 @@ -using System; using System.Diagnostics; -using System.IO; using System.Runtime.InteropServices; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.Diagnostics +namespace Umbraco.Cms.Core.Diagnostics; + +// taken from https://blogs.msdn.microsoft.com/dondu/2010/10/24/writing-minidumps-in-c/ +// and https://blogs.msdn.microsoft.com/dondu/2010/10/31/writing-minidumps-from-exceptions-in-c/ +// which itself got it from http://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/ +public static class MiniDump { - // taken from https://blogs.msdn.microsoft.com/dondu/2010/10/24/writing-minidumps-in-c/ - // and https://blogs.msdn.microsoft.com/dondu/2010/10/31/writing-minidumps-from-exceptions-in-c/ - // which itself got it from http://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/ + private static readonly object LockO = new(); - public static class MiniDump + [Flags] + public enum Option : uint { - private static readonly object LockO = new object(); + // From dbghelp.h: + Normal = 0x00000000, + WithDataSegs = 0x00000001, + WithFullMemory = 0x00000002, + WithHandleData = 0x00000004, + FilterMemory = 0x00000008, + ScanMemory = 0x00000010, + WithUnloadedModules = 0x00000020, + WithIndirectlyReferencedMemory = 0x00000040, + FilterModulePaths = 0x00000080, + WithProcessThreadData = 0x00000100, + WithPrivateReadWriteMemory = 0x00000200, + WithoutOptionalData = 0x00000400, + WithFullMemoryInfo = 0x00000800, + WithThreadInfo = 0x00001000, + WithCodeSegs = 0x00002000, + WithoutAuxiliaryState = 0x00004000, + WithFullAuxiliaryState = 0x00008000, + WithPrivateWriteCopyMemory = 0x00010000, + IgnoreInaccessibleMemory = 0x00020000, + ValidTypeFlags = 0x0003ffff, + } - [Flags] - public enum Option : uint + public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false) + { + lock (LockO) { - // From dbghelp.h: - Normal = 0x00000000, - WithDataSegs = 0x00000001, - WithFullMemory = 0x00000002, - WithHandleData = 0x00000004, - FilterMemory = 0x00000008, - ScanMemory = 0x00000010, - WithUnloadedModules = 0x00000020, - WithIndirectlyReferencedMemory = 0x00000040, - FilterModulePaths = 0x00000080, - WithProcessThreadData = 0x00000100, - WithPrivateReadWriteMemory = 0x00000200, - WithoutOptionalData = 0x00000400, - WithFullMemoryInfo = 0x00000800, - WithThreadInfo = 0x00001000, - WithCodeSegs = 0x00002000, - WithoutAuxiliaryState = 0x00004000, - WithFullAuxiliaryState = 0x00008000, - WithPrivateWriteCopyMemory = 0x00010000, - IgnoreInaccessibleMemory = 0x00020000, - ValidTypeFlags = 0x0003ffff, - } + // work around "stack trace is not available while minidump debugging", + // by making sure a local var (that we can inspect) contains the stack trace. + // getting the call stack before it is unwound would require a special exception + // filter everywhere in our code = not! + var stacktrace = withException ? Environment.StackTrace : string.Empty; - //typedef struct _MINIDUMP_EXCEPTION_INFORMATION { - // DWORD ThreadId; - // PEXCEPTION_POINTERS ExceptionPointers; - // BOOL ClientPointers; - //} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION; - [StructLayout(LayoutKind.Sequential, Pack = 4)] // Pack=4 is important! So it works also for x64! - public struct MiniDumpExceptionInformation - { - public uint ThreadId; - public IntPtr ExceptionPointers; - [MarshalAs(UnmanagedType.Bool)] - public bool ClientPointers; - } + var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); - //BOOL - //WINAPI - //MiniDumpWriteDump( - // __in HANDLE hProcess, - // __in DWORD ProcessId, - // __in HANDLE hFile, - // __in MINIDUMP_TYPE DumpType, - // __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, - // __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, - // __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam - // ); - - // Overload requiring MiniDumpExceptionInformation - [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] - private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam); - - // Overload supporting MiniDumpExceptionInformation == NULL - [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] - private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam); - - [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)] - private static extern uint GetCurrentThreadId(); - - private static bool Write(IMarchal marchal, SafeHandle fileHandle, Option options, bool withException = false) - { - using (var currentProcess = Process.GetCurrentProcess()) + if (Directory.Exists(directory) == false) { - var currentProcessHandle = currentProcess.Handle; - var currentProcessId = (uint)currentProcess.Id; - - MiniDumpExceptionInformation exp; - - exp.ThreadId = GetCurrentThreadId(); - exp.ClientPointers = false; - exp.ExceptionPointers = IntPtr.Zero; - - if (withException) - { - exp.ExceptionPointers = marchal.GetExceptionPointers(); - } - - var bRet = exp.ExceptionPointers == IntPtr.Zero - ? MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero) - : MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, ref exp, IntPtr.Zero, IntPtr.Zero); - - return bRet; + Directory.CreateDirectory(directory); } - } - public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false) - { - lock (LockO) + var filename = Path.Combine( + directory, + $"{DateTime.UtcNow:yyyyMMddTHHmmss}.{Guid.NewGuid().ToString("N")[..4]}.dmp"); + using (var stream = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Write)) { - // work around "stack trace is not available while minidump debugging", - // by making sure a local var (that we can inspect) contains the stack trace. - // getting the call stack before it is unwound would require a special exception - // filter everywhere in our code = not! - var stacktrace = withException ? Environment.StackTrace : string.Empty; - - var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); - - if (Directory.Exists(directory) == false) - { - Directory.CreateDirectory(directory); - } - - var filename = Path.Combine(directory, $"{DateTime.UtcNow:yyyyMMddTHHmmss}.{Guid.NewGuid().ToString("N").Substring(0, 4)}.dmp"); - using (var stream = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Write)) - { - return Write(marchal, stream.SafeFileHandle, options, withException); - } - } - } - - public static bool OkToDump(IHostingEnvironment hostingEnvironment) - { - lock (LockO) - { - var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); - if (Directory.Exists(directory) == false) - { - return true; - } - var count = Directory.GetFiles(directory, "*.dmp").Length; - return count < 8; + return Write(marchal, stream.SafeFileHandle, options, withException); } } } + + // BOOL + // WINAPI + // MiniDumpWriteDump( + // __in HANDLE hProcess, + // __in DWORD ProcessId, + // __in HANDLE hFile, + // __in MINIDUMP_TYPE DumpType, + // __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + // __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + // __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam + // ); + + // Overload requiring MiniDumpExceptionInformation + [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam); + + // Overload supporting MiniDumpExceptionInformation == NULL + [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam); + + [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)] + private static extern uint GetCurrentThreadId(); + + private static bool Write(IMarchal marchal, SafeHandle fileHandle, Option options, bool withException = false) + { + using (var currentProcess = Process.GetCurrentProcess()) + { + IntPtr currentProcessHandle = currentProcess.Handle; + var currentProcessId = (uint)currentProcess.Id; + + MiniDumpExceptionInformation exp; + + exp.ThreadId = GetCurrentThreadId(); + exp.ClientPointers = false; + exp.ExceptionPointers = IntPtr.Zero; + + if (withException) + { + exp.ExceptionPointers = marchal.GetExceptionPointers(); + } + + var bRet = exp.ExceptionPointers == IntPtr.Zero + ? MiniDumpWriteDump( + currentProcessHandle, + currentProcessId, + fileHandle, + (uint)options, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero) + : MiniDumpWriteDump( + currentProcessHandle, + currentProcessId, + fileHandle, + (uint)options, + ref exp, + IntPtr.Zero, + IntPtr.Zero); + + return bRet; + } + } + + public static bool OkToDump(IHostingEnvironment hostingEnvironment) + { + lock (LockO) + { + var directory = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data + "/MiniDump"); + if (Directory.Exists(directory) == false) + { + return true; + } + + var count = Directory.GetFiles(directory, "*.dmp").Length; + return count < 8; + } + } + + // typedef struct _MINIDUMP_EXCEPTION_INFORMATION { + // DWORD ThreadId; + // PEXCEPTION_POINTERS ExceptionPointers; + // BOOL ClientPointers; + // } MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION; + [StructLayout(LayoutKind.Sequential, Pack = 4)] // Pack=4 is important! So it works also for x64! + public struct MiniDumpExceptionInformation + { + public uint ThreadId; + public IntPtr ExceptionPointers; + [MarshalAs(UnmanagedType.Bool)] + public bool ClientPointers; + } } diff --git a/src/Umbraco.Core/Diagnostics/NoopMarchal.cs b/src/Umbraco.Core/Diagnostics/NoopMarchal.cs index 273a4fb32c..770aefd50f 100644 --- a/src/Umbraco.Core/Diagnostics/NoopMarchal.cs +++ b/src/Umbraco.Core/Diagnostics/NoopMarchal.cs @@ -1,9 +1,6 @@ -using System; +namespace Umbraco.Cms.Core.Diagnostics; -namespace Umbraco.Cms.Core.Diagnostics +internal class NoopMarchal : IMarchal { - internal class NoopMarchal : IMarchal - { - public IntPtr GetExceptionPointers() => IntPtr.Zero; - } + public IntPtr GetExceptionPointers() => IntPtr.Zero; } diff --git a/src/Umbraco.Core/Dictionary/ICultureDictionary.cs b/src/Umbraco.Core/Dictionary/ICultureDictionary.cs index e8e3c62050..380f7ee287 100644 --- a/src/Umbraco.Core/Dictionary/ICultureDictionary.cs +++ b/src/Umbraco.Core/Dictionary/ICultureDictionary.cs @@ -1,30 +1,28 @@ -using System.Collections.Generic; using System.Globalization; -namespace Umbraco.Cms.Core.Dictionary +namespace Umbraco.Cms.Core.Dictionary; + +/// +/// Represents a dictionary based on a specific culture +/// +public interface ICultureDictionary { /// - /// Represents a dictionary based on a specific culture + /// Returns the current culture /// - public interface ICultureDictionary - { - /// - /// Returns the dictionary value based on the key supplied - /// - /// - /// - string? this[string key] { get; } + CultureInfo Culture { get; } - /// - /// Returns the current culture - /// - CultureInfo Culture { get; } + /// + /// Returns the dictionary value based on the key supplied + /// + /// + /// + string? this[string key] { get; } - /// - /// Returns the child dictionary entries for a given key - /// - /// - /// - IDictionary GetChildren(string key); - } + /// + /// Returns the child dictionary entries for a given key + /// + /// + /// + IDictionary GetChildren(string key); } diff --git a/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs b/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs index 40fbb1bad8..6cb2642b15 100644 --- a/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs +++ b/src/Umbraco.Core/Dictionary/ICultureDictionaryFactory.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Dictionary +namespace Umbraco.Cms.Core.Dictionary; + +public interface ICultureDictionaryFactory { - public interface ICultureDictionaryFactory - { - ICultureDictionary CreateDictionary(); - } + ICultureDictionary CreateDictionary(); } diff --git a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs index 44cc15033f..de968f1676 100644 --- a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs +++ b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionary.cs @@ -1,142 +1,141 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Dictionary +namespace Umbraco.Cms.Core.Dictionary; + +/// +/// A culture dictionary that uses the Umbraco ILocalizationService +/// +/// +/// TODO: The ICultureDictionary needs to represent the 'fast' way to do dictionary item retrieval - for front-end and +/// back office. +/// The ILocalizationService is the service used for interacting with this data from the database which isn't all that +/// fast +/// (even though there is caching involved, if there's lots of dictionary items the caching is not great) +/// +internal class DefaultCultureDictionary : ICultureDictionary { + private readonly ILocalizationService _localizationService; + private readonly IAppCache _requestCache; + private readonly CultureInfo? _specificCulture; + /// - /// A culture dictionary that uses the Umbraco ILocalizationService + /// Default constructor which will use the current thread's culture /// - /// - /// TODO: The ICultureDictionary needs to represent the 'fast' way to do dictionary item retrieval - for front-end and back office. - /// The ILocalizationService is the service used for interacting with this data from the database which isn't all that fast - /// (even though there is caching involved, if there's lots of dictionary items the caching is not great) - /// - internal class DefaultCultureDictionary : ICultureDictionary + /// + /// + public DefaultCultureDictionary(ILocalizationService localizationService, IAppCache requestCache) { - private readonly ILocalizationService _localizationService; - private readonly IAppCache _requestCache; - private readonly CultureInfo? _specificCulture; + _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + } - /// - /// Default constructor which will use the current thread's culture - /// - /// - /// - public DefaultCultureDictionary(ILocalizationService localizationService, IAppCache requestCache) - { - _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - } + /// + /// Constructor for testing to specify a static culture + /// + /// + /// + /// + public DefaultCultureDictionary(CultureInfo specificCulture, ILocalizationService localizationService, IAppCache requestCache) + { + _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + _specificCulture = specificCulture ?? throw new ArgumentNullException(nameof(specificCulture)); + } - /// - /// Constructor for testing to specify a static culture - /// - /// - /// - /// - public DefaultCultureDictionary(CultureInfo specificCulture, ILocalizationService localizationService, IAppCache requestCache) - { - _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - _specificCulture = specificCulture ?? throw new ArgumentNullException(nameof(specificCulture)); - } + /// + /// Returns the current culture + /// + public CultureInfo Culture => _specificCulture ?? Thread.CurrentThread.CurrentUICulture; - /// - /// Returns the dictionary value based on the key supplied - /// - /// - /// - public string? this[string key] - { - get + private ILanguage? Language => + + // ensure it's stored/retrieved from request cache + // NOTE: This is no longer necessary since these are cached at the runtime level, but we can leave it here for now. + _requestCache.GetCacheItem( + typeof(DefaultCultureDictionary).Name + "Culture" + Culture.Name, + () => { - var found = _localizationService.GetDictionaryItemByKey(key); - if (found == null) + // find a language that matches the current culture or any of its parent cultures + CultureInfo culture = Culture; + while (culture != CultureInfo.InvariantCulture) { - return string.Empty; + ILanguage? language = _localizationService.GetLanguageByIsoCode(culture.Name); + if (language != null) + { + return language; + } + + culture = culture.Parent; } - var byLang = found.Translations?.FirstOrDefault(x => x.Language?.Equals(Language) ?? false); - if (byLang == null) - { - return string.Empty; - } + return null; + }); - return byLang.Value; - } - } - - /// - /// Returns the current culture - /// - public CultureInfo Culture => _specificCulture ?? System.Threading.Thread.CurrentThread.CurrentUICulture; - - /// - /// Returns the child dictionary entries for a given key - /// - /// - /// - /// - /// NOTE: The result of this is not cached anywhere - the underlying repository does not cache - /// the child lookups because that is done by a query lookup. This method isn't used in our codebase - /// so I don't think this is a performance issue but if devs are using this it could be optimized here. - /// - public IDictionary GetChildren(string key) + /// + /// Returns the dictionary value based on the key supplied + /// + /// + /// + public string? this[string key] + { + get { - var result = new Dictionary(); - - var found = _localizationService.GetDictionaryItemByKey(key); + IDictionaryItem? found = _localizationService.GetDictionaryItemByKey(key); if (found == null) { - return result; + return string.Empty; } - var children = _localizationService.GetDictionaryItemChildren(found.Key); - if (children == null) + IDictionaryTranslation? byLang = + found.Translations.FirstOrDefault(x => x.Language?.Equals(Language) ?? false); + if (byLang == null) { - return result; + return string.Empty; } - foreach (var dictionaryItem in children) - { - var byLang = dictionaryItem.Translations?.FirstOrDefault((x => x.Language?.Equals(Language) ?? false)); - if (byLang != null && dictionaryItem.ItemKey is not null && byLang.Value is not null) - { - result.Add(dictionaryItem.ItemKey, byLang.Value); - } - } + return byLang.Value; + } + } + /// + /// Returns the child dictionary entries for a given key + /// + /// + /// + /// + /// NOTE: The result of this is not cached anywhere - the underlying repository does not cache + /// the child lookups because that is done by a query lookup. This method isn't used in our codebase + /// so I don't think this is a performance issue but if devs are using this it could be optimized here. + /// + public IDictionary GetChildren(string key) + { + var result = new Dictionary(); + + IDictionaryItem? found = _localizationService.GetDictionaryItemByKey(key); + if (found == null) + { return result; } - private ILanguage? Language + IEnumerable? children = _localizationService.GetDictionaryItemChildren(found.Key); + if (children == null) { - get + return result; + } + + foreach (IDictionaryItem dictionaryItem in children) + { + IDictionaryTranslation? byLang = dictionaryItem.Translations.FirstOrDefault(x => x.Language?.Equals(Language) ?? false); + if (byLang != null && dictionaryItem.ItemKey is not null && byLang.Value is not null) { - //ensure it's stored/retrieved from request cache - //NOTE: This is no longer necessary since these are cached at the runtime level, but we can leave it here for now. - return _requestCache.GetCacheItem(typeof (DefaultCultureDictionary).Name + "Culture" + Culture.Name, - () => { - // find a language that matches the current culture or any of its parent cultures - var culture = Culture; - while(culture != CultureInfo.InvariantCulture) - { - var language = _localizationService.GetLanguageByIsoCode(culture.Name); - if(language != null) - { - return language; - } - culture = culture.Parent; - } - return null; - }); + result.Add(dictionaryItem.ItemKey, byLang.Value); } } + + return result; } } diff --git a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs index 8713e338ea..4c4eb030cc 100644 --- a/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs +++ b/src/Umbraco.Core/Dictionary/UmbracoCultureDictionaryFactory.cs @@ -1,28 +1,26 @@ -using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Dictionary +namespace Umbraco.Cms.Core.Dictionary; + +/// +/// A culture dictionary factory used to create an Umbraco.Core.Dictionary.ICultureDictionary. +/// +/// +/// In the future this will allow use to potentially store dictionary items elsewhere and allows for maximum +/// flexibility. +/// +public class DefaultCultureDictionaryFactory : ICultureDictionaryFactory { - /// - /// A culture dictionary factory used to create an Umbraco.Core.Dictionary.ICultureDictionary. - /// - /// - /// In the future this will allow use to potentially store dictionary items elsewhere and allows for maximum flexibility. - /// - public class DefaultCultureDictionaryFactory : ICultureDictionaryFactory + private readonly AppCaches _appCaches; + private readonly ILocalizationService _localizationService; + + public DefaultCultureDictionaryFactory(ILocalizationService localizationService, AppCaches appCaches) { - private readonly ILocalizationService _localizationService; - private readonly AppCaches _appCaches; - - public DefaultCultureDictionaryFactory(ILocalizationService localizationService, AppCaches appCaches) - { - _localizationService = localizationService; - _appCaches = appCaches; - } - - public ICultureDictionary CreateDictionary() - { - return new DefaultCultureDictionary(_localizationService, _appCaches.RequestCache); - } + _localizationService = localizationService; + _appCaches = appCaches; } + + public ICultureDictionary CreateDictionary() => + new DefaultCultureDictionary(_localizationService, _appCaches.RequestCache); } diff --git a/src/Umbraco.Core/Direction.cs b/src/Umbraco.Core/Direction.cs index 152a3663fd..874a00a4ac 100644 --- a/src/Umbraco.Core/Direction.cs +++ b/src/Umbraco.Core/Direction.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public enum Direction { - public enum Direction - { - Ascending = 0, - Descending = 1 - } + Ascending = 0, + Descending = 1, } diff --git a/src/Umbraco.Core/DisposableObjectSlim.cs b/src/Umbraco.Core/DisposableObjectSlim.cs index 4304098324..6cc7f38d91 100644 --- a/src/Umbraco.Core/DisposableObjectSlim.cs +++ b/src/Umbraco.Core/DisposableObjectSlim.cs @@ -1,56 +1,50 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Abstract implementation of managed IDisposable. +/// +/// +/// This is for objects that do NOT have unmanaged resources. +/// Can also be used as a pattern for when inheriting is not possible. +/// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx +/// See also: https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/ +/// Note: if an object's ctor throws, it will never be disposed, and so if that ctor +/// has allocated disposable objects, it should take care of disposing them. +/// +public abstract class DisposableObjectSlim : IDisposable { /// - /// Abstract implementation of managed IDisposable. + /// Gets a value indicating whether this instance is disposed. /// /// - /// This is for objects that do NOT have unmanaged resources. - /// - /// Can also be used as a pattern for when inheriting is not possible. - /// - /// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx - /// See also: https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/ - /// - /// Note: if an object's ctor throws, it will never be disposed, and so if that ctor - /// has allocated disposable objects, it should take care of disposing them. + /// for internal tests only (not thread safe) /// - public abstract class DisposableObjectSlim : IDisposable - { - /// - /// Gets a value indicating whether this instance is disposed. - /// - /// - /// for internal tests only (not thread safe) - /// - public bool Disposed { get; private set; } + public bool Disposed { get; private set; } - /// - /// Disposes managed resources - /// - protected abstract void DisposeResources(); - - /// - /// Disposes managed resources - /// - /// True if disposing via Dispose method and not a finalizer. Always true for this class. - protected virtual void Dispose(bool disposing) - { - if (!Disposed) - { - if (disposing) - { - DisposeResources(); - } - - Disposed = true; - } - } - - /// + /// #pragma warning disable CA1063 // Implement IDisposable Correctly - public void Dispose() => Dispose(disposing: true); // We do not use GC.SuppressFinalize because this has no finalizer + public void Dispose() => Dispose(true); // We do not use GC.SuppressFinalize because this has no finalizer #pragma warning restore CA1063 // Implement IDisposable Correctly + + /// + /// Disposes managed resources + /// + protected abstract void DisposeResources(); + + /// + /// Disposes managed resources + /// + /// True if disposing via Dispose method and not a finalizer. Always true for this class. + protected virtual void Dispose(bool disposing) + { + if (!Disposed) + { + if (disposing) + { + DisposeResources(); + } + + Disposed = true; + } } } diff --git a/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs index 01acd02c10..8ae47fce08 100644 --- a/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs +++ b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs @@ -1,10 +1,10 @@ namespace Umbraco.Cms.Core.DistributedLocking; /// -/// Represents the type of distributed lock. +/// Represents the type of distributed lock. /// public enum DistributedLockType { ReadLock, - WriteLock + WriteLock, } diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs index 2f27929a6c..570af005b5 100644 --- a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs @@ -1,14 +1,12 @@ -using System; - namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; /// -/// Base class for all DistributedLockingExceptions. +/// Base class for all DistributedLockingExceptions. /// public class DistributedLockingException : ApplicationException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DistributedLockingException(string message) : base(message) @@ -16,7 +14,7 @@ public class DistributedLockingException : ApplicationException } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// // ReSharper disable once UnusedMember.Global public DistributedLockingException(string message, Exception innerException) diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs index 9d65023790..064a046803 100644 --- a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; /// -/// Base class for all DistributedLocking timeout related exceptions. +/// Base class for all DistributedLocking timeout related exceptions. /// public abstract class DistributedLockingTimeoutException : DistributedLockingException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// protected DistributedLockingTimeoutException(int lockId, bool isWrite) : base($"Failed to acquire {(isWrite ? "write" : "read")} lock for id: {lockId}.") diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs index 4d37238c0d..8e21004cec 100644 --- a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; /// -/// Exception thrown when a read lock could not be obtained in a timely manner. +/// Exception thrown when a read lock could not be obtained in a timely manner. /// public class DistributedReadLockTimeoutException : DistributedLockingTimeoutException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DistributedReadLockTimeoutException(int lockId) : base(lockId, false) diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs index abf84470e0..068684f310 100644 --- a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; /// -/// Exception thrown when a write lock could not be obtained in a timely manner. +/// Exception thrown when a write lock could not be obtained in a timely manner. /// public class DistributedWriteLockTimeoutException : DistributedLockingTimeoutException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DistributedWriteLockTimeoutException(int lockId) : base(lockId, true) diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs index 202bb594bc..261bd802e3 100644 --- a/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs @@ -1,19 +1,17 @@ -using System; - namespace Umbraco.Cms.Core.DistributedLocking; /// -/// Interface representing a DistributedLock. +/// Interface representing a DistributedLock. /// public interface IDistributedLock : IDisposable { /// - /// Gets the LockId. + /// Gets the LockId. /// int LockId { get; } /// - /// Gets the DistributedLockType. + /// Gets the DistributedLockType. /// DistributedLockType LockType { get; } } diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs index 5df8a23650..57252364d3 100644 --- a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs @@ -1,50 +1,52 @@ -using System; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking.Exceptions; namespace Umbraco.Cms.Core.DistributedLocking; /// -/// Represents a class responsible for managing distributed locks. +/// Represents a class responsible for managing distributed locks. /// /// -/// In general the rules for distributed locks are as follows. -/// -/// -/// Cannot obtain a write lock if a read lock exists for same lock id (except during an upgrade from reader -> writer) -/// -/// -/// Cannot obtain a write lock if a write lock exists for same lock id. -/// -/// -/// Cannot obtain a read lock if a write lock exists for same lock id. -/// -/// -/// Can obtain a read lock if a read lock exists for same lock id. -/// -/// +/// In general the rules for distributed locks are as follows. +/// +/// +/// Cannot obtain a write lock if a read lock exists for same lock id (except during an upgrade from +/// reader -> writer) +/// +/// +/// Cannot obtain a write lock if a write lock exists for same lock id. +/// +/// +/// Cannot obtain a read lock if a write lock exists for same lock id. +/// +/// +/// Can obtain a read lock if a read lock exists for same lock id. +/// +/// /// public interface IDistributedLockingMechanism { /// - /// Gets a value indicating whether this distributed locking mechanism can be used. + /// Gets a value indicating whether this distributed locking mechanism can be used. /// bool Enabled { get; } /// - /// Obtains a distributed read lock. + /// Obtains a distributed read lock. /// /// - /// When timeout is null, implementations should use . + /// When timeout is null, implementations should use + /// . /// /// Failed to obtain distributed read lock in time. IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null); /// - /// Obtains a distributed read lock. + /// Obtains a distributed read lock. /// /// - /// When timeout is null, implementations should use . + /// When timeout is null, implementations should use + /// . /// /// Failed to obtain distributed write lock in time. IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null); diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs index 1bd1cfe206..ecc1c99cfa 100644 --- a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs @@ -1,7 +1,7 @@ namespace Umbraco.Cms.Core.DistributedLocking; /// -/// Picks an appropriate IDistributedLockingMechanism when multiple are registered +/// Picks an appropriate IDistributedLockingMechanism when multiple are registered /// public interface IDistributedLockingMechanismFactory { diff --git a/src/Umbraco.Core/Editors/BackOfficePreviewModel.cs b/src/Umbraco.Core/Editors/BackOfficePreviewModel.cs index d8bd73aca9..6ab0b76e33 100644 --- a/src/Umbraco.Core/Editors/BackOfficePreviewModel.cs +++ b/src/Umbraco.Core/Editors/BackOfficePreviewModel.cs @@ -1,21 +1,21 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Features; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +public class BackOfficePreviewModel { - public class BackOfficePreviewModel + private readonly UmbracoFeatures _features; + + public BackOfficePreviewModel(UmbracoFeatures features, IEnumerable languages) { - private readonly UmbracoFeatures _features; - - public BackOfficePreviewModel(UmbracoFeatures features, IEnumerable languages) - { - _features = features; - Languages = languages; - } - - public IEnumerable Languages { get; } - public bool DisableDevicePreview => _features.Disabled.DisableDevicePreview; - public string? PreviewExtendedHeaderView => _features.Enabled.PreviewExtendedView; + _features = features; + Languages = languages; } + + public IEnumerable Languages { get; } + + public bool DisableDevicePreview => _features.Disabled.DisableDevicePreview; + + public string? PreviewExtendedHeaderView => _features.Enabled.PreviewExtendedView; } diff --git a/src/Umbraco.Core/Editors/EditorValidatorCollection.cs b/src/Umbraco.Core/Editors/EditorValidatorCollection.cs index 91bc3e191b..a1c46cdb57 100644 --- a/src/Umbraco.Core/Editors/EditorValidatorCollection.cs +++ b/src/Umbraco.Core/Editors/EditorValidatorCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +public class EditorValidatorCollection : BuilderCollectionBase { - public class EditorValidatorCollection : BuilderCollectionBase + public EditorValidatorCollection(Func> items) + : base(items) { - public EditorValidatorCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Editors/EditorValidatorCollectionBuilder.cs b/src/Umbraco.Core/Editors/EditorValidatorCollectionBuilder.cs index 223778b79d..b7b5269ee7 100644 --- a/src/Umbraco.Core/Editors/EditorValidatorCollectionBuilder.cs +++ b/src/Umbraco.Core/Editors/EditorValidatorCollectionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +public class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase { - public class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase - { - protected override EditorValidatorCollectionBuilder This => this; - } + protected override EditorValidatorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Editors/EditorValidatorOfT.cs b/src/Umbraco.Core/Editors/EditorValidatorOfT.cs index a70509237a..3e2b899519 100644 --- a/src/Umbraco.Core/Editors/EditorValidatorOfT.cs +++ b/src/Umbraco.Core/Editors/EditorValidatorOfT.cs @@ -1,19 +1,16 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +/// +/// Provides a base class for implementations. +/// +/// The validated object type. +public abstract class EditorValidator : IEditorValidator { - /// - /// Provides a base class for implementations. - /// - /// The validated object type. - public abstract class EditorValidator : IEditorValidator - { - public Type ModelType => typeof (T); + public Type ModelType => typeof(T); - public IEnumerable Validate(object model) => Validate((T) model); + public IEnumerable Validate(object model) => Validate((T)model); - protected abstract IEnumerable Validate(T model); - } + protected abstract IEnumerable Validate(T model); } diff --git a/src/Umbraco.Core/Editors/IEditorValidator.cs b/src/Umbraco.Core/Editors/IEditorValidator.cs index 17bb195e4b..2f6bc9f110 100644 --- a/src/Umbraco.Core/Editors/IEditorValidator.cs +++ b/src/Umbraco.Core/Editors/IEditorValidator.cs @@ -1,34 +1,31 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Editors +namespace Umbraco.Cms.Core.Editors; + +// note - about IEditorValidator +// +// interface: IEditorValidator +// base class: EditorValidator +// static validation: EditorValidator.Validate() +// composition: via EditorValidationCollection and builder +// initialized with all IEditorValidator instances +// +// validation is used exclusively in ContentTypeControllerBase +// currently the only implementations are for Models Builder. + +/// +/// Provides a general object validator. +/// +public interface IEditorValidator : IDiscoverable { - // note - about IEditorValidator - // - // interface: IEditorValidator - // base class: EditorValidator - // static validation: EditorValidator.Validate() - // composition: via EditorValidationCollection and builder - // initialized with all IEditorValidator instances - // - // validation is used exclusively in ContentTypeControllerBase - // currently the only implementations are for Models Builder. + /// + /// Gets the object type validated by this validator. + /// + Type ModelType { get; } /// - /// Provides a general object validator. + /// Validates an object. /// - public interface IEditorValidator : IDiscoverable - { - /// - /// Gets the object type validated by this validator. - /// - Type ModelType { get; } - - /// - /// Validates an object. - /// - IEnumerable Validate(object model); - } + IEnumerable Validate(object model); } diff --git a/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs index 23fc59da24..be9b05230f 100644 --- a/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs +++ b/src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs @@ -1,8 +1,6 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -10,161 +8,191 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Editors -{ - public class UserEditorAuthorizationHelper - { - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; +namespace Umbraco.Cms.Core.Editors; - public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) +public class UserEditorAuthorizationHelper +{ + private readonly AppCaches _appCaches; + private readonly IContentService _contentService; + private readonly IEntityService _entityService; + private readonly IMediaService _mediaService; + + public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) + { + _contentService = contentService; + _mediaService = mediaService; + _entityService = entityService; + _appCaches = appCaches; + } + + /// + /// Checks if the current user has access to save the user data + /// + /// The current user trying to save user data + /// The user instance being saved (can be null if it's a new user) + /// The start content ids of the user being saved (can be null or empty) + /// The start media ids of the user being saved (can be null or empty) + /// The user aliases of the user being saved (can be null or empty) + /// + public Attempt IsAuthorized( + IUser? currentUser, + IUser? savingUser, + IEnumerable? startContentIds, + IEnumerable? startMediaIds, + IEnumerable? userGroupAliases) + { + var currentIsAdmin = currentUser?.IsAdmin() ?? false; + + // a) A non-admin cannot save an admin + if (savingUser != null) { - _contentService = contentService; - _mediaService = mediaService; - _entityService = entityService; - _appCaches = appCaches; + if (savingUser.IsAdmin() && currentIsAdmin == false) + { + return Attempt.Fail("The current user is not an administrator so cannot save another administrator"); + } } - /// - /// Checks if the current user has access to save the user data - /// - /// The current user trying to save user data - /// The user instance being saved (can be null if it's a new user) - /// The start content ids of the user being saved (can be null or empty) - /// The start media ids of the user being saved (can be null or empty) - /// The user aliases of the user being saved (can be null or empty) - /// - public Attempt IsAuthorized(IUser? currentUser, - IUser? savingUser, - IEnumerable? startContentIds, IEnumerable? startMediaIds, - IEnumerable? userGroupAliases) + // b) If a start node is changing, a user cannot set a start node on another user that they don't have access to, this even goes for admins + + // only validate any start nodes that have changed. + // a user can remove any start nodes and add start nodes that they have access to + // but they cannot add a start node that they do not have access to + IEnumerable? changedStartContentIds = savingUser == null + ? startContentIds + : startContentIds == null || savingUser.StartContentIds is null + ? null + : startContentIds.Except(savingUser.StartContentIds).ToArray(); + IEnumerable? changedStartMediaIds = savingUser == null + ? startMediaIds + : startMediaIds == null || savingUser.StartMediaIds is null + ? null + : startMediaIds.Except(savingUser.StartMediaIds).ToArray(); + Attempt pathResult = currentUser is null + ? Attempt.Fail() + : AuthorizePath(currentUser, changedStartContentIds, changedStartMediaIds); + if (pathResult == false) { - var currentIsAdmin = currentUser?.IsAdmin() ?? false; + return pathResult; + } - // a) A non-admin cannot save an admin + // c) an admin can manage any group or section access + if (currentIsAdmin) + { + return Attempt.Succeed(); + } - if (savingUser != null) - { - if (savingUser.IsAdmin() && currentIsAdmin == false) - return Attempt.Fail("The current user is not an administrator so cannot save another administrator"); - } - - // b) If a start node is changing, a user cannot set a start node on another user that they don't have access to, this even goes for admins - - //only validate any start nodes that have changed. - //a user can remove any start nodes and add start nodes that they have access to - //but they cannot add a start node that they do not have access to - - var changedStartContentIds = savingUser == null - ? startContentIds - : startContentIds == null || savingUser.StartContentIds is null - ? null - : startContentIds.Except(savingUser.StartContentIds).ToArray(); - var changedStartMediaIds = savingUser == null - ? startMediaIds - : startMediaIds == null || savingUser.StartMediaIds is null - ? null - : startMediaIds.Except(savingUser.StartMediaIds).ToArray(); - var pathResult = currentUser is null ? Attempt.Fail() : AuthorizePath(currentUser, changedStartContentIds, changedStartMediaIds); - if (pathResult == false) - return pathResult; - - // c) an admin can manage any group or section access - - if (currentIsAdmin) - return Attempt.Succeed(); - - if (userGroupAliases != null) - { - var savingGroupAliases = userGroupAliases.ToArray(); - var existingGroupAliases = savingUser == null + if (userGroupAliases != null) + { + var savingGroupAliases = userGroupAliases.ToArray(); + var existingGroupAliases = savingUser == null ? new string[0] : savingUser.Groups.Select(x => x.Alias).ToArray(); - var addedGroupAliases = savingGroupAliases.Except(existingGroupAliases); + IEnumerable addedGroupAliases = savingGroupAliases.Except(existingGroupAliases); - // As we know the current user is not admin, it is only allowed to use groups that the user do have themselves. - var savingGroupAliasesNotAllowed = addedGroupAliases.Except(currentUser?.Groups.Select(x=> x.Alias) ?? Enumerable.Empty()).ToArray(); - if (savingGroupAliasesNotAllowed.Any()) + // As we know the current user is not admin, it is only allowed to use groups that the user do have themselves. + var savingGroupAliasesNotAllowed = addedGroupAliases + .Except(currentUser?.Groups.Select(x => x.Alias) ?? Enumerable.Empty()).ToArray(); + if (savingGroupAliasesNotAllowed.Any()) + { + return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) + + "', the current user is not part of them or admin"); + } + + // only validate any groups that have changed. + // a non-admin user can remove groups and add groups that they have access to + // but they cannot add a group that they do not have access to or that grants them + // path or section access that they don't have access to. + var newGroups = savingUser == null + ? savingGroupAliases + : savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray(); + + var userGroupsChanged = savingUser != null && newGroups.Length > 0; + + if (userGroupsChanged) + { + // d) A user cannot assign a group to another user that they do not belong to + var currentUserGroups = currentUser?.Groups.Select(x => x.Alias).ToArray(); + foreach (var group in newGroups) { - return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) + "', the current user is not part of them or admin"); - } - - //only validate any groups that have changed. - //a non-admin user can remove groups and add groups that they have access to - //but they cannot add a group that they do not have access to or that grants them - //path or section access that they don't have access to. - - var newGroups = savingUser == null - ? savingGroupAliases - : savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray(); - - var userGroupsChanged = savingUser != null && newGroups.Length > 0; - - if (userGroupsChanged) - { - // d) A user cannot assign a group to another user that they do not belong to - var currentUserGroups = currentUser?.Groups.Select(x => x.Alias).ToArray(); - foreach (var group in newGroups) + if (currentUserGroups?.Contains(group) == false) { - if (currentUserGroups?.Contains(group) == false) - { - return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member"); - } + return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member"); } } } - - return Attempt.Succeed(); } - private Attempt AuthorizePath(IUser currentUser, IEnumerable? startContentIds, IEnumerable? startMediaIds) + return Attempt.Succeed(); + } + + private Attempt AuthorizePath(IUser currentUser, IEnumerable? startContentIds, IEnumerable? startMediaIds) + { + if (startContentIds != null) { - if (startContentIds != null) + foreach (var contentId in startContentIds) { - foreach (var contentId in startContentIds) + if (contentId == Constants.System.Root) { - if (contentId == Constants.System.Root) + var hasAccess = ContentPermissions.HasPathAccess( + "-1", + currentUser.CalculateContentStartNodeIds(_entityService, _appCaches), + Constants.System.RecycleBinContent); + if (hasAccess == false) { - var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService, _appCaches), Constants.System.RecycleBinContent); - if (hasAccess == false) - return Attempt.Fail("The current user does not have access to the content root"); + return Attempt.Fail("The current user does not have access to the content root"); } - else + } + else + { + IContent? content = _contentService.GetById(contentId); + if (content == null) { - var content = _contentService.GetById(contentId); - if (content == null) continue; - var hasAccess = currentUser.HasPathAccess(content, _entityService, _appCaches); - if (hasAccess == false) - return Attempt.Fail("The current user does not have access to the content path " + content.Path); + continue; + } + + var hasAccess = currentUser.HasPathAccess(content, _entityService, _appCaches); + if (hasAccess == false) + { + return Attempt.Fail("The current user does not have access to the content path " + + content.Path); } } } - - if (startMediaIds != null) - { - foreach (var mediaId in startMediaIds) - { - if (mediaId == Constants.System.Root) - { - var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches), Constants.System.RecycleBinMedia); - if (hasAccess == false) - return Attempt.Fail("The current user does not have access to the media root"); - } - else - { - var media = _mediaService.GetById(mediaId); - if (media == null) continue; - var hasAccess = currentUser.HasPathAccess(media, _entityService, _appCaches); - if (hasAccess == false) - return Attempt.Fail("The current user does not have access to the media path " + media.Path); - } - } - } - - return Attempt.Succeed(); } + + if (startMediaIds != null) + { + foreach (var mediaId in startMediaIds) + { + if (mediaId == Constants.System.Root) + { + var hasAccess = ContentPermissions.HasPathAccess( + "-1", + currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches), + Constants.System.RecycleBinMedia); + if (hasAccess == false) + { + return Attempt.Fail("The current user does not have access to the media root"); + } + } + else + { + IMedia? media = _mediaService.GetById(mediaId); + if (media == null) + { + continue; + } + + var hasAccess = currentUser.HasPathAccess(media, _entityService, _appCaches); + if (hasAccess == false) + { + return Attempt.Fail("The current user does not have access to the media path " + media.Path); + } + } + } + } + + return Attempt.Succeed(); } } diff --git a/src/Umbraco.Core/Enum.cs b/src/Umbraco.Core/Enum.cs index 9ca1111a30..6084dfe971 100644 --- a/src/Umbraco.Core/Enum.cs +++ b/src/Umbraco.Core/Enum.cs @@ -1,110 +1,103 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Provides utility methods for handling enumerations. +/// +/// +/// Taken from http://damieng.com/blog/2010/10/17/enums-better-syntax-improved-performance-and-tryparse-in-net-3-5 +/// +public static class Enum + where T : struct { - /// - /// Provides utility methods for handling enumerations. - /// - /// - /// Taken from http://damieng.com/blog/2010/10/17/enums-better-syntax-improved-performance-and-tryparse-in-net-3-5 - /// - public static class Enum - where T : struct + private static readonly List Values; + private static readonly Dictionary InsensitiveNameToValue; + private static readonly Dictionary SensitiveNameToValue; + private static readonly Dictionary IntToValue; + private static readonly Dictionary ValueToName; + + static Enum() { - private static readonly List Values; - private static readonly Dictionary InsensitiveNameToValue; - private static readonly Dictionary SensitiveNameToValue; - private static readonly Dictionary IntToValue; - private static readonly Dictionary ValueToName; + Values = Enum.GetValues(typeof(T)).Cast().ToList(); - static Enum() + IntToValue = new Dictionary(); + ValueToName = new Dictionary(); + SensitiveNameToValue = new Dictionary(); + InsensitiveNameToValue = new Dictionary(); + + foreach (T value in Values) { - Values = Enum.GetValues(typeof(T)).Cast().ToList(); + var name = value.ToString(); - IntToValue = new Dictionary(); - ValueToName = new Dictionary(); - SensitiveNameToValue = new Dictionary(); - InsensitiveNameToValue = new Dictionary(); - - foreach (var value in Values) - { - var name = value.ToString(); - - IntToValue[Convert.ToInt32(value)] = value; - ValueToName[value] = name!; - SensitiveNameToValue[name!] = value; - InsensitiveNameToValue[name!.ToLowerInvariant()] = value; - } - } - - public static bool IsDefined(T value) - { - return ValueToName.Keys.Contains(value); - } - - public static bool IsDefined(string value) - { - return SensitiveNameToValue.Keys.Contains(value); - } - - public static bool IsDefined(int value) - { - return IntToValue.Keys.Contains(value); - } - - public static IEnumerable GetValues() - { - return Values; - } - - public static string[] GetNames() - { - return ValueToName.Values.ToArray(); - } - - public static string? GetName(T value) - { - return ValueToName.TryGetValue(value, out var name) ? name : null; - } - - public static T Parse(string value, bool ignoreCase = false) - { - var names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; - if (ignoreCase) value = value.ToLowerInvariant(); - - if (names.TryGetValue(value, out var parsed)) - return parsed; - - throw new ArgumentException($"Value \"{value}\"is not a valid {typeof(T).Name} enumeration value.", nameof(value)); - } - - public static bool TryParse(string value, out T returnValue, bool ignoreCase = false) - { - var names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; - if (ignoreCase) value = value.ToLowerInvariant(); - return names.TryGetValue(value, out returnValue); - } - - public static T? ParseOrNull(string value) - { - if (string.IsNullOrWhiteSpace(value)) - return null; - - if (InsensitiveNameToValue.TryGetValue(value.ToLowerInvariant(), out var parsed)) - return parsed; - - return null; - } - - public static T? CastOrNull(int value) - { - if (IntToValue.TryGetValue(value, out var foundValue)) - return foundValue; - - return null; + IntToValue[Convert.ToInt32(value)] = value; + ValueToName[value] = name!; + SensitiveNameToValue[name!] = value; + InsensitiveNameToValue[name!.ToLowerInvariant()] = value; } } + + public static bool IsDefined(T value) => ValueToName.Keys.Contains(value); + + public static bool IsDefined(string value) => SensitiveNameToValue.Keys.Contains(value); + + public static bool IsDefined(int value) => IntToValue.Keys.Contains(value); + + public static IEnumerable GetValues() => Values; + + public static string[] GetNames() => ValueToName.Values.ToArray(); + + public static string? GetName(T value) => ValueToName.TryGetValue(value, out var name) ? name : null; + + public static T Parse(string value, bool ignoreCase = false) + { + Dictionary names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; + if (ignoreCase) + { + value = value.ToLowerInvariant(); + } + + if (names.TryGetValue(value, out T parsed)) + { + return parsed; + } + + throw new ArgumentException( + $"Value \"{value}\"is not a valid {typeof(T).Name} enumeration value.", + nameof(value)); + } + + public static bool TryParse(string value, out T returnValue, bool ignoreCase = false) + { + Dictionary names = ignoreCase ? InsensitiveNameToValue : SensitiveNameToValue; + if (ignoreCase) + { + value = value.ToLowerInvariant(); + } + + return names.TryGetValue(value, out returnValue); + } + + public static T? ParseOrNull(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + if (InsensitiveNameToValue.TryGetValue(value.ToLowerInvariant(), out T parsed)) + { + return parsed; + } + + return null; + } + + public static T? CastOrNull(int value) + { + if (IntToValue.TryGetValue(value, out T foundValue)) + { + return foundValue; + } + + return null; + } } diff --git a/src/Umbraco.Core/EnvironmentHelper.cs b/src/Umbraco.Core/EnvironmentHelper.cs index 097ffc9629..04b3bc91ff 100644 --- a/src/Umbraco.Core/EnvironmentHelper.cs +++ b/src/Umbraco.Core/EnvironmentHelper.cs @@ -1,17 +1,14 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Currently just used to get the machine name for use with file names +/// +internal class EnvironmentHelper { /// - /// Currently just used to get the machine name for use with file names + /// Returns the machine name that is safe to use in file paths. /// - internal class EnvironmentHelper - { - /// - /// Returns the machine name that is safe to use in file paths. - /// - public static string FileSafeMachineName => Environment.MachineName.ReplaceNonAlphanumericChars('-'); - - } + public static string FileSafeMachineName => Environment.MachineName.ReplaceNonAlphanumericChars('-'); } diff --git a/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs index c9958a5fc9..22c7ef4c7e 100644 --- a/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEnumerableObjectEventArgs.cs @@ -1,59 +1,79 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Represents event data, for events that support cancellation, and expose impacted objects. +/// +/// The type of the exposed, impacted objects. +public class CancellableEnumerableObjectEventArgs : CancellableObjectEventArgs>, + IEquatable> { - /// - /// Represents event data, for events that support cancellation, and expose impacted objects. - /// - /// The type of the exposed, impacted objects. - public class CancellableEnumerableObjectEventArgs : CancellableObjectEventArgs>, IEquatable> + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) { - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(eventObject, canCancel, messages, additionalData) - { } + } - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) - { } + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) - { } + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel) - : base(eventObject, canCancel) - { } + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) + { + } - public CancellableEnumerableObjectEventArgs(IEnumerable eventObject) - : base(eventObject) - { } + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject) + : base(eventObject) + { + } - public bool Equals(CancellableEnumerableObjectEventArgs? other) + public bool Equals(CancellableEnumerableObjectEventArgs? other) + { + if (other is null || other.EventObject is null) { - if (other is null || other.EventObject is null) return false; - if (ReferenceEquals(this, other)) return true; - - return EventObject?.SequenceEqual(other.EventObject) ?? false; + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, other)) { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CancellableEnumerableObjectEventArgs)obj); + return true; } - public override int GetHashCode() - { - if (EventObject is not null) - { - return HashCodeHelper.GetHashCode(EventObject); - } + return EventObject?.SequenceEqual(other.EventObject) ?? false; + } - return base.GetHashCode(); + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((CancellableEnumerableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + if (EventObject is not null) + { + return HashCodeHelper.GetHashCode(EventObject); + } + + return base.GetHashCode(); } } diff --git a/src/Umbraco.Core/Events/CancellableEventArgs.cs b/src/Umbraco.Core/Events/CancellableEventArgs.cs index a991f6532b..7768da05f5 100644 --- a/src/Umbraco.Core/Events/CancellableEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEventArgs.cs @@ -1,141 +1,157 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Represents event data for events that support cancellation. +/// +public class CancellableEventArgs : EventArgs, IEquatable { - /// - /// Represents event data for events that support cancellation. - /// - public class CancellableEventArgs : EventArgs, IEquatable + private static readonly ReadOnlyDictionary EmptyAdditionalData = new(new Dictionary()); + + private bool _cancel; + private IDictionary? _eventState; + + public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) { - private bool _cancel; - private IDictionary? _eventState; + CanCancel = canCancel; + Messages = messages; + AdditionalData = new ReadOnlyDictionary(additionalData); + } - private static readonly ReadOnlyDictionary EmptyAdditionalData = new ReadOnlyDictionary(new Dictionary()); + public CancellableEventArgs(bool canCancel, EventMessages eventMessages) + { + CanCancel = canCancel; + Messages = eventMessages ?? throw new ArgumentNullException("eventMessages"); + AdditionalData = EmptyAdditionalData; + } - public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) + public CancellableEventArgs(bool canCancel) + { + CanCancel = canCancel; + + // create a standalone messages + Messages = new EventMessages(); + AdditionalData = EmptyAdditionalData; + } + + public CancellableEventArgs(EventMessages eventMessages) + : this(true, eventMessages) + { + } + + public CancellableEventArgs() + : this(true) + { + } + + /// + /// Flag to determine if this instance will support being cancellable + /// + public bool CanCancel { get; set; } + + /// + /// If this instance supports cancellation, this gets/sets the cancel value + /// + public bool Cancel + { + get { - CanCancel = canCancel; - Messages = messages; - AdditionalData = new ReadOnlyDictionary(additionalData); - } - - public CancellableEventArgs(bool canCancel, EventMessages eventMessages) - { - if (eventMessages == null) throw new ArgumentNullException("eventMessages"); - CanCancel = canCancel; - Messages = eventMessages; - AdditionalData = EmptyAdditionalData; - } - - public CancellableEventArgs(bool canCancel) - { - CanCancel = canCancel; - //create a standalone messages - Messages = new EventMessages(); - AdditionalData = EmptyAdditionalData; - } - - public CancellableEventArgs(EventMessages eventMessages) - : this(true, eventMessages) - { } - - public CancellableEventArgs() - : this(true) - { } - - /// - /// Flag to determine if this instance will support being cancellable - /// - public bool CanCancel { get; set; } - - /// - /// If this instance supports cancellation, this gets/sets the cancel value - /// - public bool Cancel - { - get + if (CanCancel == false) { - if (CanCancel == false) - { - throw new InvalidOperationException("This event argument class does not support canceling."); - } - return _cancel; + throw new InvalidOperationException("This event argument class does not support canceling."); } - set + + return _cancel; + } + + set + { + if (CanCancel == false) { - if (CanCancel == false) - { - throw new InvalidOperationException("This event argument class does not support canceling."); - } - _cancel = value; + throw new InvalidOperationException("This event argument class does not support canceling."); } - } - /// - /// if this instance supports cancellation, this will set Cancel to true with an affiliated cancellation message - /// - /// - public void CancelOperation(EventMessage cancelationMessage) - { - Cancel = true; - cancelationMessage.IsDefaultEventMessage = true; - Messages.Add(cancelationMessage); - } - - /// - /// Returns the EventMessages object which is used to add messages to the message collection for this event - /// - public EventMessages Messages { get; } - - /// - /// In some cases raised evens might need to contain additional arbitrary readonly data which can be read by event subscribers - /// - /// - /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility - /// so we cannot change the strongly typed nature for some events. - /// - public ReadOnlyDictionary AdditionalData { get; set; } - - /// - /// This can be used by event subscribers to store state in the event args so they easily deal with custom state data between a starting ("ing") - /// event and an ending ("ed") event - /// - public IDictionary EventState - { - get => _eventState ?? (_eventState = new Dictionary()); - set => _eventState = value; - } - - public bool Equals(CancellableEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(AdditionalData, other.AdditionalData); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((CancellableEventArgs) obj); - } - - public override int GetHashCode() - { - return AdditionalData != null ? AdditionalData.GetHashCode() : 0; - } - - public static bool operator ==(CancellableEventArgs? left, CancellableEventArgs? right) - { - return Equals(left, right); - } - - public static bool operator !=(CancellableEventArgs left, CancellableEventArgs right) - { - return Equals(left, right) == false; + _cancel = value; } } + + /// + /// Returns the EventMessages object which is used to add messages to the message collection for this event + /// + public EventMessages Messages { get; } + + /// + /// In some cases raised evens might need to contain additional arbitrary readonly data which can be read by event + /// subscribers + /// + /// + /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards + /// compatibility + /// so we cannot change the strongly typed nature for some events. + /// + public ReadOnlyDictionary AdditionalData { get; set; } + + /// + /// This can be used by event subscribers to store state in the event args so they easily deal with custom state data + /// between a starting ("ing") + /// event and an ending ("ed") event + /// + public IDictionary EventState + { + get => _eventState ??= new Dictionary(); + set => _eventState = value; + } + + public static bool operator ==(CancellableEventArgs? left, CancellableEventArgs? right) => Equals(left, right); + + public static bool operator !=(CancellableEventArgs left, CancellableEventArgs right) => Equals(left, right) == false; + + public bool Equals(CancellableEventArgs? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Equals(AdditionalData, other.AdditionalData); + } + + /// + /// if this instance supports cancellation, this will set Cancel to true with an affiliated cancellation message + /// + /// + public void CancelOperation(EventMessage cancelationMessage) + { + Cancel = true; + cancelationMessage.IsDefaultEventMessage = true; + Messages.Add(cancelationMessage); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((CancellableEventArgs)obj); + } + + public override int GetHashCode() => AdditionalData != null ? AdditionalData.GetHashCode() : 0; } diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index 2697b773c2..26aa61b67a 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -1,46 +1,38 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Provides a base class for classes representing event data, for events that support cancellation, and expose an +/// impacted object. +/// +public abstract class CancellableObjectEventArgs : CancellableEventArgs { - /// - /// Provides a base class for classes representing event data, for events that support cancellation, and expose an impacted object. - /// - public abstract class CancellableObjectEventArgs : CancellableEventArgs + protected CancellableObjectEventArgs(object? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(canCancel, messages, additionalData) => + EventObject = eventObject; + + protected CancellableObjectEventArgs(object? eventObject, bool canCancel, EventMessages eventMessages) + : base(canCancel, eventMessages) => + EventObject = eventObject; + + protected CancellableObjectEventArgs(object? eventObject, EventMessages eventMessages) + : this(eventObject, true, eventMessages) { - protected CancellableObjectEventArgs(object? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(canCancel, messages, additionalData) - { - EventObject = eventObject; - } - - protected CancellableObjectEventArgs(object? eventObject, bool canCancel, EventMessages eventMessages) - : base(canCancel, eventMessages) - { - EventObject = eventObject; - } - - protected CancellableObjectEventArgs(object? eventObject, EventMessages eventMessages) - : this(eventObject, true, eventMessages) - { - } - - protected CancellableObjectEventArgs(object? eventObject, bool canCancel) - : base(canCancel) - { - EventObject = eventObject; - } - - protected CancellableObjectEventArgs(object? eventObject) - : this(eventObject, true) - { - } - - /// - /// Gets or sets the impacted object. - /// - /// - /// This is protected so that inheritors can expose it with their own name - /// - public object? EventObject { get; set; } } + + protected CancellableObjectEventArgs(object? eventObject, bool canCancel) + : base(canCancel) => + EventObject = eventObject; + + protected CancellableObjectEventArgs(object? eventObject) + : this(eventObject, true) + { + } + + /// + /// Gets or sets the impacted object. + /// + /// + /// This is protected so that inheritors can expose it with their own name + /// + public object? EventObject { get; set; } } diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgsOfTEventObject.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgsOfTEventObject.cs index 939fd8e11b..5d9865c253 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgsOfTEventObject.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgsOfTEventObject.cs @@ -1,87 +1,102 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Represent event data, for events that support cancellation, and expose an impacted object. +/// +/// The type of the exposed, impacted object. +public class CancellableObjectEventArgs : CancellableObjectEventArgs, + IEquatable> { - /// - /// Represent event data, for events that support cancellation, and expose an impacted object. - /// - /// The type of the exposed, impacted object. - public class CancellableObjectEventArgs : CancellableObjectEventArgs, IEquatable> + public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) { - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(eventObject, canCancel, messages, additionalData) + } + + public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } + + public CancellableObjectEventArgs(TEventObject? eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel) + : base(eventObject, canCancel) + { + } + + public CancellableObjectEventArgs(TEventObject? eventObject) + : base(eventObject) + { + } + + /// + /// Gets or sets the impacted object. + /// + /// + /// This is protected so that inheritors can expose it with their own name + /// + protected new TEventObject? EventObject + { + get => (TEventObject?)base.EventObject; + set => base.EventObject = value; + } + + public static bool operator ==( + CancellableObjectEventArgs left, + CancellableObjectEventArgs right) => Equals(left, right); + + public static bool operator !=( + CancellableObjectEventArgs left, + CancellableObjectEventArgs right) => !Equals(left, right); + + public bool Equals(CancellableObjectEventArgs? other) + { + if (other is null) { + return false; } - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) + if (ReferenceEquals(this, other)) { + return true; } - public CancellableObjectEventArgs(TEventObject? eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) + return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); + } + + public override bool Equals(object? obj) + { + if (obj is null) { + return false; } - public CancellableObjectEventArgs(TEventObject? eventObject, bool canCancel) - : base(eventObject, canCancel) + if (ReferenceEquals(this, obj)) { + return true; } - public CancellableObjectEventArgs(TEventObject? eventObject) - : base(eventObject) + if (obj.GetType() != GetType()) { + return false; } - /// - /// Gets or sets the impacted object. - /// - /// - /// This is protected so that inheritors can expose it with their own name - /// - protected new TEventObject? EventObject - { - get => (TEventObject?) base.EventObject; - set => base.EventObject = value; - } + return Equals((CancellableObjectEventArgs)obj); + } - public bool Equals(CancellableObjectEventArgs? other) + public override int GetHashCode() + { + unchecked { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); - } - - public override bool Equals(object? obj) - { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CancellableObjectEventArgs)obj); - } - - public override int GetHashCode() - { - unchecked + if (EventObject is not null) { - if (EventObject is not null) - { - return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); - } - - return base.GetHashCode() * 397; + return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); } - } - public static bool operator ==(CancellableObjectEventArgs left, CancellableObjectEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(CancellableObjectEventArgs left, CancellableObjectEventArgs right) - { - return !Equals(left, right); + return base.GetHashCode() * 397; } } } diff --git a/src/Umbraco.Core/Events/ContentCacheEventArgs.cs b/src/Umbraco.Core/Events/ContentCacheEventArgs.cs index 78f714f754..732e6f2452 100644 --- a/src/Umbraco.Core/Events/ContentCacheEventArgs.cs +++ b/src/Umbraco.Core/Events/ContentCacheEventArgs.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Cms.Core.Events +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Events; + +public class ContentCacheEventArgs : CancelEventArgs { - public class ContentCacheEventArgs : System.ComponentModel.CancelEventArgs { } } diff --git a/src/Umbraco.Core/Events/CopyEventArgs.cs b/src/Umbraco.Core/Events/CopyEventArgs.cs index 6a4969710a..bead8213b6 100644 --- a/src/Umbraco.Core/Events/CopyEventArgs.cs +++ b/src/Umbraco.Core/Events/CopyEventArgs.cs @@ -1,91 +1,99 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class CopyEventArgs : CancellableObjectEventArgs, IEquatable> { - public class CopyEventArgs : CancellableObjectEventArgs, IEquatable> + public CopyEventArgs(TEntity original, TEntity copy, bool canCancel, int parentId) + : base(original, canCancel) { - public CopyEventArgs(TEntity original, TEntity copy, bool canCancel, int parentId) - : base(original, canCancel) + Copy = copy; + ParentId = parentId; + } + + public CopyEventArgs(TEntity eventObject, TEntity copy, int parentId) + : base(eventObject) + { + Copy = copy; + ParentId = parentId; + } + + public CopyEventArgs(TEntity eventObject, TEntity copy, bool canCancel, int parentId, bool relateToOriginal) + : base(eventObject, canCancel) + { + Copy = copy; + ParentId = parentId; + RelateToOriginal = relateToOriginal; + } + + /// + /// The copied entity + /// + public TEntity Copy { get; set; } + + /// + /// The original entity + /// + public TEntity? Original => EventObject; + + /// + /// Gets or Sets the Id of the objects new parent. + /// + public int ParentId { get; } + + public bool RelateToOriginal { get; set; } + + public static bool operator ==(CopyEventArgs left, CopyEventArgs right) => Equals(left, right); + + public bool Equals(CopyEventArgs? other) + { + if (ReferenceEquals(null, other)) { - Copy = copy; - ParentId = parentId; + return false; } - public CopyEventArgs(TEntity eventObject, TEntity copy, int parentId) - : base(eventObject) + if (ReferenceEquals(this, other)) { - Copy = copy; - ParentId = parentId; + return true; } - public CopyEventArgs(TEntity eventObject, TEntity copy, bool canCancel, int parentId, bool relateToOriginal) - : base(eventObject, canCancel) + return base.Equals(other) && EqualityComparer.Default.Equals(Copy, other.Copy) && + ParentId == other.ParentId && RelateToOriginal == other.RelateToOriginal; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - Copy = copy; - ParentId = parentId; - RelateToOriginal = relateToOriginal; + return false; } - /// - /// The copied entity - /// - public TEntity Copy { get; set; } - - /// - /// The original entity - /// - public TEntity? Original + if (ReferenceEquals(this, obj)) { - get { return EventObject; } + return true; } - /// - /// Gets or Sets the Id of the objects new parent. - /// - public int ParentId { get; private set; } - - public bool RelateToOriginal { get; set; } - - public bool Equals(CopyEventArgs? other) + if (obj.GetType() != GetType()) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && EqualityComparer.Default.Equals(Copy, other.Copy) && ParentId == other.ParentId && RelateToOriginal == other.RelateToOriginal; + return false; } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CopyEventArgs) obj); - } + return Equals((CopyEventArgs)obj); + } - public override int GetHashCode() + public override int GetHashCode() + { + unchecked { - unchecked + var hashCode = base.GetHashCode(); + if (Copy is not null) { - int hashCode = base.GetHashCode(); - if (Copy is not null) - { - hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Copy); - } - - hashCode = (hashCode * 397) ^ ParentId; - hashCode = (hashCode * 397) ^ RelateToOriginal.GetHashCode(); - return hashCode; + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Copy); } - } - public static bool operator ==(CopyEventArgs left, CopyEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(CopyEventArgs left, CopyEventArgs right) - { - return !Equals(left, right); + hashCode = (hashCode * 397) ^ ParentId; + hashCode = (hashCode * 397) ^ RelateToOriginal.GetHashCode(); + return hashCode; } } + + public static bool operator !=(CopyEventArgs left, CopyEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs index 1696e07ec6..3ca366834f 100644 --- a/src/Umbraco.Core/Events/DeleteEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteEventArgs.cs @@ -1,202 +1,209 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +[SupersedeEvent(typeof(SaveEventArgs<>))] +[SupersedeEvent(typeof(PublishEventArgs<>))] +[SupersedeEvent(typeof(MoveEventArgs<>))] +[SupersedeEvent(typeof(CopyEventArgs<>))] +public class DeleteEventArgs : CancellableEnumerableObjectEventArgs, + IEquatable>, IDeletingMediaFilesEventArgs { - [SupersedeEvent(typeof(SaveEventArgs<>))] - [SupersedeEvent(typeof(PublishEventArgs<>))] - [SupersedeEvent(typeof(MoveEventArgs<>))] - [SupersedeEvent(typeof(CopyEventArgs<>))] - public class DeleteEventArgs : CancellableEnumerableObjectEventArgs, IEquatable>, IDeletingMediaFilesEventArgs + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) => MediaFilesToDelete = new List(); + + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base( + eventObject, + eventMessages) => MediaFilesToDelete = new List(); + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public DeleteEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public DeleteEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + /// + public DeleteEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting multiple entities that are used in the delete operation + /// + /// + public DeleteEventArgs(IEnumerable eventObject) + : base(eventObject) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting a single entity instance + /// + /// + public DeleteEventArgs(TEntity eventObject) + : base(new List { eventObject }) => + MediaFilesToDelete = new List(); + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public DeleteEventArgs(TEntity eventObject, bool canCancel) + : base(new List { eventObject }, canCancel) => + MediaFilesToDelete = new List(); + + /// + /// Returns all entities that were deleted during the operation + /// + public IEnumerable DeletedEntities { - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) : base(eventObject, canCancel, eventMessages) + get => EventObject ?? Enumerable.Empty(); + set => EventObject = value; + } + + /// + /// A list of media files that can be added to during a deleted operation for which Umbraco will ensure are removed + /// + public List MediaFilesToDelete { get; } + + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) => + Equals(left, right); + + public bool Equals(DeleteEventArgs? other) + { + if (ReferenceEquals(null, other)) { - MediaFilesToDelete = new List(); + return false; } - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) + if (ReferenceEquals(this, other)) { - MediaFilesToDelete = new List(); + return true; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, eventMessages) + return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - MediaFilesToDelete = new List(); + return false; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) - : base(new List { eventObject }, canCancel, eventMessages) + if (ReferenceEquals(this, obj)) { - MediaFilesToDelete = new List(); + return true; } - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - /// - public DeleteEventArgs(IEnumerable eventObject, bool canCancel) : base(eventObject, canCancel) + if (obj.GetType() != GetType()) { - MediaFilesToDelete = new List(); + return false; } - /// - /// Constructor accepting multiple entities that are used in the delete operation - /// - /// - public DeleteEventArgs(IEnumerable eventObject) : base(eventObject) - { - MediaFilesToDelete = new List(); - } + return Equals((DeleteEventArgs)obj); + } - /// - /// Constructor accepting a single entity instance - /// - /// - public DeleteEventArgs(TEntity eventObject) - : base(new List { eventObject }) + public override int GetHashCode() + { + unchecked { - MediaFilesToDelete = new List(); - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public DeleteEventArgs(TEntity eventObject, bool canCancel) - : base(new List { eventObject }, canCancel) - { - MediaFilesToDelete = new List(); - } - - /// - /// Returns all entities that were deleted during the operation - /// - public IEnumerable DeletedEntities - { - get => EventObject ?? Enumerable.Empty(); - set => EventObject = value; - } - - /// - /// A list of media files that can be added to during a deleted operation for which Umbraco will ensure are removed - /// - public List MediaFilesToDelete { get; private set; } - - public bool Equals(DeleteEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((DeleteEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397) ^ MediaFilesToDelete.GetHashCode(); - } - } - - public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) - { - return !Equals(left, right); + return (base.GetHashCode() * 397) ^ MediaFilesToDelete.GetHashCode(); } } - public class DeleteEventArgs : CancellableEventArgs, IEquatable - { - public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) - : base(canCancel, eventMessages) - { - Id = id; - } - - public DeleteEventArgs(int id, bool canCancel) - : base(canCancel) - { - Id = id; - } - - public DeleteEventArgs(int id) - { - Id = id; - } - - /// - /// Gets the Id of the object being deleted. - /// - public int Id { get; private set; } - - public bool Equals(DeleteEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && Id == other.Id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((DeleteEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397) ^ Id; - } - } - - public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) - { - return !Equals(left, right); - } - } + public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) => + !Equals(left, right); +} + +public class DeleteEventArgs : CancellableEventArgs, IEquatable +{ + public DeleteEventArgs(int id, bool canCancel, EventMessages eventMessages) + : base(canCancel, eventMessages) => + Id = id; + + public DeleteEventArgs(int id, bool canCancel) + : base(canCancel) => + Id = id; + + public DeleteEventArgs(int id) => Id = id; + + /// + /// Gets the Id of the object being deleted. + /// + public int Id { get; } + + public static bool operator ==(DeleteEventArgs left, DeleteEventArgs right) => Equals(left, right); + + public bool Equals(DeleteEventArgs? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((DeleteEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ Id; + } + } + + public static bool operator !=(DeleteEventArgs left, DeleteEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs index e27c155ec4..d298f5bbec 100644 --- a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs +++ b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs @@ -1,183 +1,188 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Contains types and methods that allow publishing general notifications. +/// +public partial class EventAggregator : IEventAggregator { - /// - /// Contains types and methods that allow publishing general notifications. - /// - public partial class EventAggregator : IEventAggregator + private static readonly ConcurrentDictionary NotificationAsyncHandlers + = new(); + + private static readonly ConcurrentDictionary NotificationHandlers = new(); + + private Task PublishNotificationAsync(INotification notification, CancellationToken cancellationToken = default) { - private static readonly ConcurrentDictionary s_notificationAsyncHandlers - = new ConcurrentDictionary(); - - private static readonly ConcurrentDictionary s_notificationHandlers - = new ConcurrentDictionary(); - - private Task PublishNotificationAsync(INotification notification, CancellationToken cancellationToken = default) - { - Type notificationType = notification.GetType(); - NotificationAsyncHandlerWrapper asyncHandler = s_notificationAsyncHandlers.GetOrAdd( - notificationType, - t => - { - var value = Activator.CreateInstance( - typeof(NotificationAsyncHandlerWrapperImpl<>).MakeGenericType(notificationType)); - return value is not null - ? (NotificationAsyncHandlerWrapper)value - : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); - }); - - return asyncHandler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync); - } - - private void PublishNotification(INotification notification) - { - Type notificationType = notification.GetType(); - NotificationHandlerWrapper? asyncHandler = s_notificationHandlers.GetOrAdd( - notificationType, - t => - { - var value = Activator.CreateInstance( - typeof(NotificationHandlerWrapperImpl<>).MakeGenericType(notificationType)); - return value is not null ? (NotificationHandlerWrapper)value : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); - }); - - asyncHandler?.Handle(notification, _serviceFactory, PublishCore); - } - - private async Task PublishCoreAsync( - IEnumerable> allHandlers, - INotification notification, - CancellationToken cancellationToken) - { - foreach (Func handler in allHandlers) + Type notificationType = notification.GetType(); + NotificationAsyncHandlerWrapper asyncHandler = NotificationAsyncHandlers.GetOrAdd( + notificationType, + t => { - await handler(notification, cancellationToken).ConfigureAwait(false); - } - } + var value = Activator.CreateInstance( + typeof(NotificationAsyncHandlerWrapperImpl<>).MakeGenericType(notificationType)); + return value is not null + ? (NotificationAsyncHandlerWrapper)value + : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); + }); - private void PublishCore( - IEnumerable> allHandlers, - INotification notification) - { - foreach (Action handler in allHandlers) + return asyncHandler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync); + } + + private void PublishNotification(INotification notification) + { + Type notificationType = notification.GetType(); + NotificationHandlerWrapper? asyncHandler = NotificationHandlers.GetOrAdd( + notificationType, + t => { - handler(notification); - } + var value = Activator.CreateInstance( + typeof(NotificationHandlerWrapperImpl<>).MakeGenericType(notificationType)); + return value is not null + ? (NotificationHandlerWrapper)value + : throw new InvalidCastException("Activator could not create instance of NotificationHandler"); + }); + + asyncHandler?.Handle(notification, _serviceFactory, PublishCore); + } + + private async Task PublishCoreAsync( + IEnumerable> allHandlers, + INotification notification, + CancellationToken cancellationToken) + { + foreach (Func handler in allHandlers) + { + await handler(notification, cancellationToken).ConfigureAwait(false); } } - internal abstract class NotificationHandlerWrapper + private void PublishCore( + IEnumerable> allHandlers, + INotification notification) { - public abstract void Handle( - INotification notification, - ServiceFactory serviceFactory, - Action>, INotification> publish); - } - - internal abstract class NotificationAsyncHandlerWrapper - { - public abstract Task HandleAsync( - INotification notification, - CancellationToken cancellationToken, - ServiceFactory serviceFactory, - Func>, INotification, CancellationToken, Task> publish); - } - - internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper - where TNotification : INotification - { - /// - /// - /// Background - During v9 build we wanted an in-process message bus to facilitate removal of the old static event handlers.
- /// Instead of taking a dependency on MediatR we (the community) implemented our own using MediatR as inspiration. - ///
- /// - /// - /// Some things worth knowing about MediatR. - /// - /// All handlers are by default registered with transient lifetime, but can easily depend on services with state. - /// Both the Mediatr instance and its handler resolver are registered transient and as such it is always possible to depend on scoped services in a handler. - /// - /// - /// - /// - /// Our EventAggregator started out registered with a transient lifetime but later (before initial release) the registration was changed to singleton, presumably - /// because there are a lot of singleton services in Umbraco which like to publish notifications and it's a pain to use scoped services from a singleton. - ///
- /// The problem with a singleton EventAggregator is it forces handlers to create a service scope and service locate any scoped services - /// they wish to make use of e.g. a unit of work (think entity framework DBContext). - ///
- /// - /// - /// Moving forwards it probably makes more sense to register EventAggregator transient but doing so now would mean an awful lot of service location to avoid breaking changes. - ///
- /// For now we can do the next best thing which is to create a scope for each published notification, thus enabling the transient handlers to take a dependency on a scoped service. - ///
- /// - /// - /// Did discuss using HttpContextAccessor/IScopedServiceProvider to enable sharing of scopes when publisher has http context, - /// but decided against because it's inconsistent with what happens in background threads and will just cause confusion. - /// - ///
- public override Task HandleAsync( - INotification notification, - CancellationToken cancellationToken, - ServiceFactory serviceFactory, - Func>, INotification, CancellationToken, Task> publish) + foreach (Action handler in allHandlers) { - // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. - // TODO: go back to using ServiceFactory to resolve - IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); - using IServiceScope scope = scopeFactory.CreateScope(); - IServiceProvider container = scope.ServiceProvider; - - IEnumerable> handlers = container - .GetServices>() - .Select(x => new Func( - (theNotification, theToken) => - x.HandleAsync((TNotification)theNotification, theToken))); - - return publish(handlers, notification, cancellationToken); - } - } - - internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper - where TNotification : INotification - { - /// - /// See remarks on for explanation on - /// what's going on with the IServiceProvider stuff here. - /// - public override void Handle( - INotification notification, - ServiceFactory serviceFactory, - Action>, INotification> publish) - { - // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. - // TODO: go back to using ServiceFactory to resolve - IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); - using IServiceScope scope = scopeFactory.CreateScope(); - IServiceProvider container = scope.ServiceProvider; - - IEnumerable> handlers = container - .GetServices>() - .Select(x => new Action( - (theNotification) => - x.Handle((TNotification)theNotification))); - - publish(handlers, notification); + handler(notification); } } } + +internal abstract class NotificationHandlerWrapper +{ + public abstract void Handle( + INotification notification, + ServiceFactory serviceFactory, + Action>, INotification> publish); +} + +internal abstract class NotificationAsyncHandlerWrapper +{ + public abstract Task HandleAsync( + INotification notification, + CancellationToken cancellationToken, + ServiceFactory serviceFactory, + Func>, INotification, CancellationToken, Task> + publish); +} + +internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper + where TNotification : INotification +{ + /// + /// + /// Background - During v9 build we wanted an in-process message bus to facilitate removal of the old static event + /// handlers.
+ /// Instead of taking a dependency on MediatR we (the community) implemented our own using MediatR as inspiration. + ///
+ /// + /// Some things worth knowing about MediatR. + /// + /// + /// All handlers are by default registered with transient lifetime, but can easily depend on services + /// with state. + /// + /// + /// Both the Mediatr instance and its handler resolver are registered transient and as such it is always + /// possible to depend on scoped services in a handler. + /// + /// + /// + /// + /// Our EventAggregator started out registered with a transient lifetime but later (before initial release) the + /// registration was changed to singleton, presumably + /// because there are a lot of singleton services in Umbraco which like to publish notifications and it's a pain to + /// use scoped services from a singleton. + ///
+ /// The problem with a singleton EventAggregator is it forces handlers to create a service scope and service locate + /// any scoped services + /// they wish to make use of e.g. a unit of work (think entity framework DBContext). + ///
+ /// + /// Moving forwards it probably makes more sense to register EventAggregator transient but doing so now would mean + /// an awful lot of service location to avoid breaking changes. + ///
+ /// For now we can do the next best thing which is to create a scope for each published notification, thus enabling + /// the transient handlers to take a dependency on a scoped service. + ///
+ /// + /// Did discuss using HttpContextAccessor/IScopedServiceProvider to enable sharing of scopes when publisher has + /// http context, + /// but decided against because it's inconsistent with what happens in background threads and will just cause + /// confusion. + /// + ///
+ public override Task HandleAsync( + INotification notification, + CancellationToken cancellationToken, + ServiceFactory serviceFactory, + Func>, INotification, CancellationToken, Task> publish) + { + // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. + // TODO: go back to using ServiceFactory to resolve + IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider container = scope.ServiceProvider; + + IEnumerable> handlers = container + .GetServices>() + .Select(x => new Func( + (theNotification, theToken) => + x.HandleAsync((TNotification)theNotification, theToken))); + + return publish(handlers, notification, cancellationToken); + } +} + +internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper + where TNotification : INotification +{ + /// + /// See remarks on for explanation on + /// what's going on with the IServiceProvider stuff here. + /// + public override void Handle( + INotification notification, + ServiceFactory serviceFactory, + Action>, INotification> publish) + { + // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. + // TODO: go back to using ServiceFactory to resolve + IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider container = scope.ServiceProvider; + + IEnumerable> handlers = container + .GetServices>() + .Select(x => new Action( + theNotification => + x.Handle((TNotification)theNotification))); + + publish(handlers, notification); + } +} diff --git a/src/Umbraco.Core/Events/EventAggregator.cs b/src/Umbraco.Core/Events/EventAggregator.cs index 5bf54b516a..277b24eb06 100644 --- a/src/Umbraco.Core/Events/EventAggregator.cs +++ b/src/Umbraco.Core/Events/EventAggregator.cs @@ -1,117 +1,112 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// A factory method used to resolve all services. +/// For multiple instances, it will resolve against . +/// +/// Type of service to resolve. +/// An instance of type . +public delegate object ServiceFactory(Type serviceType); + +/// +/// Extensions for . +/// +public static class ServiceFactoryExtensions { /// - /// A factory method used to resolve all services. - /// For multiple instances, it will resolve against . + /// Gets an instance of . /// - /// Type of service to resolve. - /// An instance of type . - public delegate object ServiceFactory(Type serviceType); - - /// - public partial class EventAggregator : IEventAggregator - { - private readonly ServiceFactory _serviceFactory; - - /// - /// Initializes a new instance of the class. - /// - /// The service instance factory. - public EventAggregator(ServiceFactory serviceFactory) - => _serviceFactory = serviceFactory; - - /// - public Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) - where TNotification : INotification - { - // TODO: Introduce codegen efficient Guard classes to reduce noise. - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - PublishNotification(notification); - return PublishNotificationAsync(notification, cancellationToken); - } - - /// - public void Publish(TNotification notification) - where TNotification : INotification - { - // TODO: Introduce codegen efficient Guard classes to reduce noise. - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - PublishNotification(notification); - var task = PublishNotificationAsync(notification); - if (task is not null) - { - Task.WaitAll(task); - } - } - - public bool PublishCancelable(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification - { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - Publish(notification); - return notification.Cancel; - } - - public async Task PublishCancelableAsync(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification - { - if (notification is null) - { - throw new ArgumentNullException(nameof(notification)); - } - - Task? task = PublishAsync(notification); - if (task is not null) - { - await task; - } - - return notification.Cancel; - } - } + /// The type to return. + /// The service factory. + /// The new instance. + public static T GetInstance(this ServiceFactory factory) + => (T)factory(typeof(T)); /// - /// Extensions for . + /// Gets a collection of instances of . /// - public static class ServiceFactoryExtensions - { - /// - /// Gets an instance of . - /// - /// The type to return. - /// The service factory. - /// The new instance. - public static T GetInstance(this ServiceFactory factory) - => (T)factory(typeof(T)); + /// The collection item type to return. + /// The service factory. + /// The new instance collection. + public static IEnumerable GetInstances(this ServiceFactory factory) + => (IEnumerable)factory(typeof(IEnumerable)); +} - /// - /// Gets a collection of instances of . - /// - /// The collection item type to return. - /// The service factory. - /// The new instance collection. - public static IEnumerable GetInstances(this ServiceFactory factory) - => (IEnumerable)factory(typeof(IEnumerable)); +/// +public partial class EventAggregator : IEventAggregator +{ + private readonly ServiceFactory _serviceFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The service instance factory. + public EventAggregator(ServiceFactory serviceFactory) + => _serviceFactory = serviceFactory; + + /// + public Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification + { + // TODO: Introduce codegen efficient Guard classes to reduce noise. + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + PublishNotification(notification); + return PublishNotificationAsync(notification, cancellationToken); + } + + /// + public void Publish(TNotification notification) + where TNotification : INotification + { + // TODO: Introduce codegen efficient Guard classes to reduce noise. + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + PublishNotification(notification); + Task task = PublishNotificationAsync(notification); + if (task is not null) + { + Task.WaitAll(task); + } + } + + public bool PublishCancelable(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + Publish(notification); + return notification.Cancel; + } + + public async Task PublishCancelableAsync(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification + { + if (notification is null) + { + throw new ArgumentNullException(nameof(notification)); + } + + Task? task = PublishAsync(notification); + if (task is not null) + { + await task; + } + + return notification.Cancel; } } diff --git a/src/Umbraco.Core/Events/EventDefinition.cs b/src/Umbraco.Core/Events/EventDefinition.cs index aa6f2899cd..3f7cd382ed 100644 --- a/src/Umbraco.Core/Events/EventDefinition.cs +++ b/src/Umbraco.Core/Events/EventDefinition.cs @@ -1,73 +1,61 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class EventDefinition : EventDefinitionBase { - public class EventDefinition : EventDefinitionBase + private readonly EventArgs _args; + private readonly object _sender; + private readonly EventHandler _trackedEvent; + + public EventDefinition(EventHandler trackedEvent, object sender, EventArgs args, string? eventName = null) + : base(sender, args, eventName) { - private readonly EventHandler _trackedEvent; - private readonly object _sender; - private readonly EventArgs _args; - - public EventDefinition(EventHandler trackedEvent, object sender, EventArgs args, string? eventName = null) - : base(sender, args, eventName) - { - _trackedEvent = trackedEvent; - _sender = sender; - _args = args; - } - - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _args); - } - } + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; } - public class EventDefinition : EventDefinitionBase + public override void RaiseEvent() { - private readonly EventHandler _trackedEvent; - private readonly object _sender; - private readonly TEventArgs _args; - - public EventDefinition(EventHandler trackedEvent, object sender, TEventArgs args, string? eventName = null) - : base(sender, args, eventName) - { - _trackedEvent = trackedEvent; - _sender = sender; - _args = args; - } - - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _args); - } - } - } - - public class EventDefinition : EventDefinitionBase - { - private readonly TypedEventHandler _trackedEvent; - private readonly TSender _sender; - private readonly TEventArgs _args; - - public EventDefinition(TypedEventHandler trackedEvent, TSender sender, TEventArgs args, string? eventName = null) - : base(sender, args, eventName) - { - _trackedEvent = trackedEvent; - _sender = sender; - _args = args; - } - - public override void RaiseEvent() - { - if (_trackedEvent != null) - { - _trackedEvent(_sender, _args); - } - } + _trackedEvent?.Invoke(_sender, _args); + } +} + +public class EventDefinition : EventDefinitionBase +{ + private readonly TEventArgs _args; + private readonly object _sender; + private readonly EventHandler _trackedEvent; + + public EventDefinition(EventHandler trackedEvent, object sender, TEventArgs args, string? eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + _trackedEvent?.Invoke(_sender, _args); + } +} + +public class EventDefinition : EventDefinitionBase +{ + private readonly TEventArgs _args; + private readonly TSender _sender; + private readonly TypedEventHandler _trackedEvent; + + public EventDefinition(TypedEventHandler trackedEvent, TSender sender, TEventArgs args, string? eventName = null) + : base(sender, args, eventName) + { + _trackedEvent = trackedEvent; + _sender = sender; + _args = args; + } + + public override void RaiseEvent() + { + _trackedEvent?.Invoke(_sender, _args); } } diff --git a/src/Umbraco.Core/Events/EventDefinitionBase.cs b/src/Umbraco.Core/Events/EventDefinitionBase.cs index 4223924234..8ac84c470d 100644 --- a/src/Umbraco.Core/Events/EventDefinitionBase.cs +++ b/src/Umbraco.Core/Events/EventDefinitionBase.cs @@ -1,73 +1,93 @@ -using System; using System.Reflection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public abstract class EventDefinitionBase : IEventDefinition, IEquatable { - public abstract class EventDefinitionBase : IEventDefinition, IEquatable + protected EventDefinitionBase(object? sender, object? args, string? eventName = null) { - protected EventDefinitionBase(object? sender, object? args, string? eventName = null) - { - Sender = sender ?? throw new ArgumentNullException(nameof(sender)); - Args = args ?? throw new ArgumentNullException(nameof(args)); - EventName = eventName; + Sender = sender ?? throw new ArgumentNullException(nameof(sender)); + Args = args ?? throw new ArgumentNullException(nameof(args)); + EventName = eventName; - if (EventName.IsNullOrWhiteSpace()) + if (EventName.IsNullOrWhiteSpace()) + { + // don't match "Ing" suffixed names + Attempt findResult = + EventNameExtractor.FindEvent(sender, args, EventNameExtractor.MatchIngNames); + + if (findResult.Success == false) { - // don't match "Ing" suffixed names - var findResult = EventNameExtractor.FindEvent(sender, args, exclude:EventNameExtractor.MatchIngNames); - - if (findResult.Success == false) - throw new AmbiguousMatchException("Could not automatically find the event name, the event name will need to be explicitly registered for this event definition. " - + $"Sender: {sender.GetType()} Args: {args.GetType()}" - + " Error: " + findResult.Result?.Error); - EventName = findResult.Result?.Name; + throw new AmbiguousMatchException( + "Could not automatically find the event name, the event name will need to be explicitly registered for this event definition. " + + $"Sender: {sender.GetType()} Args: {args.GetType()}" + + " Error: " + findResult.Result?.Error); } - } - public object Sender { get; } - public object Args { get; } - public string? EventName { get; } - - public abstract void RaiseEvent(); - - public bool Equals(EventDefinitionBase? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Args.Equals(other.Args) && string.Equals(EventName, other.EventName) && Sender.Equals(other.Sender); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((EventDefinitionBase) obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = Args.GetHashCode(); - if (EventName is not null) - { - hashCode = (hashCode * 397) ^ EventName.GetHashCode(); - } - hashCode = (hashCode * 397) ^ Sender.GetHashCode(); - return hashCode; - } - } - - public static bool operator ==(EventDefinitionBase left, EventDefinitionBase right) - { - return Equals(left, right); - } - - public static bool operator !=(EventDefinitionBase left, EventDefinitionBase right) - { - return Equals(left, right) == false; + EventName = findResult.Result?.Name; } } + + public object Sender { get; } + + public bool Equals(EventDefinitionBase? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Args.Equals(other.Args) && string.Equals(EventName, other.EventName) && Sender.Equals(other.Sender); + } + + public object Args { get; } + + public string? EventName { get; } + + public static bool operator ==(EventDefinitionBase left, EventDefinitionBase right) => Equals(left, right); + + public abstract void RaiseEvent(); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((EventDefinitionBase)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Args.GetHashCode(); + if (EventName is not null) + { + hashCode = (hashCode * 397) ^ EventName.GetHashCode(); + } + + hashCode = (hashCode * 397) ^ Sender.GetHashCode(); + return hashCode; + } + } + + public static bool operator !=(EventDefinitionBase left, EventDefinitionBase right) => Equals(left, right) == false; } diff --git a/src/Umbraco.Core/Events/EventDefinitionFilter.cs b/src/Umbraco.Core/Events/EventDefinitionFilter.cs index 47b0f9a44e..4872b23e8b 100644 --- a/src/Umbraco.Core/Events/EventDefinitionFilter.cs +++ b/src/Umbraco.Core/Events/EventDefinitionFilter.cs @@ -1,24 +1,23 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// The filter used in the GetEvents method which determines +/// how the result list is filtered +/// +public enum EventDefinitionFilter { /// - /// The filter used in the GetEvents method which determines - /// how the result list is filtered + /// Returns all events tracked /// - public enum EventDefinitionFilter - { - /// - /// Returns all events tracked - /// - All, + All, - /// - /// Deduplicates events and only returns the first duplicate instance tracked - /// - FirstIn, + /// + /// Deduplicates events and only returns the first duplicate instance tracked + /// + FirstIn, - /// - /// Deduplicates events and only returns the last duplicate instance tracked - /// - LastIn - } + /// + /// Deduplicates events and only returns the last duplicate instance tracked + /// + LastIn, } diff --git a/src/Umbraco.Core/Events/EventExtensions.cs b/src/Umbraco.Core/Events/EventExtensions.cs index 4d98cbbcca..6d9fd8103b 100644 --- a/src/Umbraco.Core/Events/EventExtensions.cs +++ b/src/Umbraco.Core/Events/EventExtensions.cs @@ -1,46 +1,51 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Extension methods for cancellable event operations +/// +public static class EventExtensions { - /// - /// Extension methods for cancellable event operations - /// - public static class EventExtensions - { - // keep these two for backward compatibility reasons but understand that - // they are *not* part of any scope / event dispatcher / anything... + // keep these two for backward compatibility reasons but understand that + // they are *not* part of any scope / event dispatcher / anything... - /// - /// Raises a cancelable event and returns a value indicating whether the event should be cancelled. - /// - /// The type of the event source. - /// The type of the event data. - /// The event handler. - /// The event source. - /// The event data. - /// A value indicating whether the cancelable event should be cancelled - /// A cancelable event is raised by a component when it is about to perform an action that can be canceled. - public static bool IsRaisedEventCancelled(this TypedEventHandler eventHandler, TArgs args, TSender sender) - where TArgs : CancellableEventArgs + /// + /// Raises a cancelable event and returns a value indicating whether the event should be cancelled. + /// + /// The type of the event source. + /// The type of the event data. + /// The event handler. + /// The event source. + /// The event data. + /// A value indicating whether the cancelable event should be cancelled + /// A cancelable event is raised by a component when it is about to perform an action that can be canceled. + public static bool IsRaisedEventCancelled(this TypedEventHandler eventHandler, TArgs args, TSender sender) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - /// - /// Raises an event. - /// - /// The type of the event source. - /// The type of the event data. - /// The event handler. - /// The event source. - /// The event data. - public static void RaiseEvent(this TypedEventHandler eventHandler, TArgs args, TSender sender) - where TArgs : EventArgs + eventHandler(sender, args); + return args.Cancel; + } + + /// + /// Raises an event. + /// + /// The type of the event source. + /// The type of the event data. + /// The event handler. + /// The event source. + /// The event data. + public static void RaiseEvent(this TypedEventHandler eventHandler, TArgs args, TSender sender) + where TArgs : EventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return; - eventHandler(sender, args); + return; } + + eventHandler(sender, args); } } diff --git a/src/Umbraco.Core/Events/EventMessage.cs b/src/Umbraco.Core/Events/EventMessage.cs index eef0985c23..8ba2c98bf8 100644 --- a/src/Umbraco.Core/Events/EventMessage.cs +++ b/src/Umbraco.Core/Events/EventMessage.cs @@ -1,27 +1,29 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// An event message +/// +public sealed class EventMessage { /// - /// An event message + /// Initializes a new instance of the class. /// - public sealed class EventMessage + public EventMessage(string category, string message, EventMessageType messageType = EventMessageType.Default) { - /// - /// Initializes a new instance of the class. - /// - public EventMessage(string category, string message, EventMessageType messageType = EventMessageType.Default) - { - Category = category; - Message = message; - MessageType = messageType; - } - - public string Category { get; private set; } - public string Message { get; private set; } - public EventMessageType MessageType { get; private set; } - - /// - /// This is used to track if this message should be used as a default message so that Umbraco doesn't also append it's own default messages - /// - public bool IsDefaultEventMessage { get; set; } + Category = category; + Message = message; + MessageType = messageType; } + + public string Category { get; } + + public string Message { get; } + + public EventMessageType MessageType { get; } + + /// + /// This is used to track if this message should be used as a default message so that Umbraco doesn't also append it's + /// own default messages + /// + public bool IsDefaultEventMessage { get; set; } } diff --git a/src/Umbraco.Core/Events/EventMessageType.cs b/src/Umbraco.Core/Events/EventMessageType.cs index afbed0d590..a3c6ebf2f9 100644 --- a/src/Umbraco.Core/Events/EventMessageType.cs +++ b/src/Umbraco.Core/Events/EventMessageType.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// The type of event message +/// +public enum EventMessageType { - /// - /// The type of event message - /// - public enum EventMessageType - { - Default = 0, - Info = 1, - Error = 2, - Success = 3, - Warning = 4 - } + Default = 0, + Info = 1, + Error = 2, + Success = 3, + Warning = 4, } diff --git a/src/Umbraco.Core/Events/EventMessages.cs b/src/Umbraco.Core/Events/EventMessages.cs index 23b40118c7..68d19f27fd 100644 --- a/src/Umbraco.Core/Events/EventMessages.cs +++ b/src/Umbraco.Core/Events/EventMessages.cs @@ -1,29 +1,17 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Event messages collection +/// +public sealed class EventMessages : DisposableObjectSlim { - /// - /// Event messages collection - /// - public sealed class EventMessages : DisposableObjectSlim - { - private readonly List _msgs = new List(); + private readonly List _msgs = new(); - public void Add(EventMessage msg) - { - _msgs.Add(msg); - } + public int Count => _msgs.Count; - public int Count => _msgs.Count; + public void Add(EventMessage msg) => _msgs.Add(msg); - public IEnumerable GetAll() - { - return _msgs; - } + public IEnumerable GetAll() => _msgs; - protected override void DisposeResources() - { - _msgs.Clear(); - } - } + protected override void DisposeResources() => _msgs.Clear(); } diff --git a/src/Umbraco.Core/Events/EventNameExtractor.cs b/src/Umbraco.Core/Events/EventNameExtractor.cs index c74d2e293e..16f772dcb2 100644 --- a/src/Umbraco.Core/Events/EventNameExtractor.cs +++ b/src/Umbraco.Core/Events/EventNameExtractor.cs @@ -1,168 +1,184 @@ -using System; using System.Collections.Concurrent; -using System.Linq; using System.Reflection; using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// There is actually no way to discover an event name in c# at the time of raising the event. It is possible +/// to get the event name from the handler that is being executed based on the event being raised, however that is not +/// what we want in this case. We need to find the event name before it is being raised - you would think that it's +/// possible +/// with reflection or anything but that is not the case, the delegate that defines an event has no info attached to +/// it, it +/// is literally just an event. +/// So what this does is take the sender and event args objects, looks up all public/static events on the sender that +/// have +/// a generic event handler with generic arguments (but only) one, then we match the type of event arguments with the +/// ones +/// being passed in. As it turns out, in our services this will work for the majority of our events! In some cases it +/// may not +/// work and we'll have to supply a string but hopefully this saves a bit of magic strings. +/// We can also write tests to validate these are all working correctly for all services. +/// +public class EventNameExtractor { /// - /// There is actually no way to discover an event name in c# at the time of raising the event. It is possible - /// to get the event name from the handler that is being executed based on the event being raised, however that is not - /// what we want in this case. We need to find the event name before it is being raised - you would think that it's possible - /// with reflection or anything but that is not the case, the delegate that defines an event has no info attached to it, it - /// is literally just an event. - /// - /// So what this does is take the sender and event args objects, looks up all public/static events on the sender that have - /// a generic event handler with generic arguments (but only) one, then we match the type of event arguments with the ones - /// being passed in. As it turns out, in our services this will work for the majority of our events! In some cases it may not - /// work and we'll have to supply a string but hopefully this saves a bit of magic strings. - /// - /// We can also write tests to validate these are all working correctly for all services. + /// Used to cache all candidate events for a given type so we don't re-look them up /// - public class EventNameExtractor + private static readonly ConcurrentDictionary CandidateEvents = new(); + + /// + /// Used to cache all matched event names by (sender type + arg type) so we don't re-look them up + /// + private static readonly ConcurrentDictionary, string[]> MatchedEventNames = new(); + + /// + /// Finds the event name on the sender that matches the args type + /// + /// + /// + /// + /// A filter to exclude matched event names, this filter should return true to exclude the event name from being + /// matched + /// + /// + /// null if not found or an ambiguous match + /// + public static Attempt FindEvent(Type senderType, Type argsType, Func exclude) { + var events = FindEvents(senderType, argsType, exclude); - /// - /// Finds the event name on the sender that matches the args type - /// - /// - /// - /// - /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched - /// - /// - /// null if not found or an ambiguous match - /// - public static Attempt FindEvent(Type senderType, Type argsType, Func exclude) + switch (events.Length) { - var events = FindEvents(senderType, argsType, exclude); + case 0: + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); - switch (events.Length) - { - case 0: - return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); + case 1: + return Attempt.Succeed(new EventNameExtractorResult(events[0])); - case 1: - return Attempt.Succeed(new EventNameExtractorResult(events[0])); - - default: - //there's more than one left so it's ambiguous! - return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); - } + default: + // there's more than one left so it's ambiguous! + return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); } + } - public static string[] FindEvents(Type senderType, Type argsType, Func exclude) + public static string[] FindEvents(Type senderType, Type argsType, Func exclude) + { + var found = MatchedEventNames.GetOrAdd(new Tuple(senderType, argsType), tuple => { - var found = MatchedEventNames.GetOrAdd(new Tuple(senderType, argsType), tuple => + EventInfoArgs[] events = CandidateEvents.GetOrAdd(senderType, t => { - var events = CandidateEvents.GetOrAdd(senderType, t => - { - return t.GetEvents(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) - //we can only look for events handlers with generic types because that is the only - // way that we can try to find a matching event based on the arg type passed in - .Where(x => x.EventHandlerType?.IsGenericType ?? false) - .Select(x => new EventInfoArgs(x, x.EventHandlerType!.GetGenericArguments())) - //we are only looking for event handlers that have more than one generic argument - .Where(x => - { - if (x.GenericArgs.Length == 1) return true; + return t.GetEvents(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.FlattenHierarchy) - //special case for our own TypedEventHandler - if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && x.GenericArgs.Length == 2) - { - return true; - } + // we can only look for events handlers with generic types because that is the only + // way that we can try to find a matching event based on the arg type passed in + .Where(x => x.EventHandlerType?.IsGenericType ?? false) + .Select(x => new EventInfoArgs(x, x.EventHandlerType!.GetGenericArguments())) - return false; - }) - .ToArray(); - }); - - return events.Where(x => - { - if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) - return true; - - //special case for our own TypedEventHandler - if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) - && x.GenericArgs.Length == 2 - && x.GenericArgs[1] == tuple.Item2) + // we are only looking for event handlers that have more than one generic argument + .Where(x => { - return true; - } + if (x.GenericArgs.Length == 1) + { + return true; + } - return false; - }).Select(x => x.EventInfo.Name).ToArray(); + // special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && + x.GenericArgs.Length == 2) + { + return true; + } + + return false; + }) + .ToArray(); }); - return found.Where(x => exclude(x) == false).ToArray(); - } - - /// - /// Finds the event name on the sender that matches the args type - /// - /// - /// - /// - /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched - /// - /// - /// null if not found or an ambiguous match - /// - public static Attempt FindEvent(object sender, object args, Func exclude) - { - return FindEvent(sender.GetType(), args.GetType(), exclude); - } - - /// - /// Return true if the event is named with an ING name such as "Saving" or "RollingBack" - /// - /// - /// - public static bool MatchIngNames(string eventName) - { - var splitter = new Regex(@"(? - /// Return true if the event is not named with an ING name such as "Saving" or "RollingBack" - ///
- /// - /// - public static bool MatchNonIngNames(string eventName) - { - var splitter = new Regex(@"(? { - EventInfo = eventInfo; - GenericArgs = genericArgs; - } + if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) + { + return true; + } + + // special case for our own TypedEventHandler + if (x.EventInfo.EventHandlerType?.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) + && x.GenericArgs.Length == 2 + && x.GenericArgs[1] == tuple.Item2) + { + return true; + } + + return false; + }).Select(x => x.EventInfo.Name).ToArray(); + }); + + return found.Where(x => exclude(x) == false).ToArray(); + } + + /// + /// Finds the event name on the sender that matches the args type + /// + /// + /// + /// + /// A filter to exclude matched event names, this filter should return true to exclude the event name from being + /// matched + /// + /// + /// null if not found or an ambiguous match + /// + public static Attempt + FindEvent(object sender, object args, Func exclude) => + FindEvent(sender.GetType(), args.GetType(), exclude); + + /// + /// Return true if the event is named with an ING name such as "Saving" or "RollingBack" + /// + /// + /// + public static bool MatchIngNames(string eventName) + { + var splitter = new Regex(@"(? - /// Used to cache all candidate events for a given type so we don't re-look them up - /// - private static readonly ConcurrentDictionary CandidateEvents = new ConcurrentDictionary(); + return words[0].EndsWith("ing"); + } - /// - /// Used to cache all matched event names by (sender type + arg type) so we don't re-look them up - /// - private static readonly ConcurrentDictionary, string[]> MatchedEventNames = new ConcurrentDictionary, string[]>(); + /// + /// Return true if the event is not named with an ING name such as "Saving" or "RollingBack" + /// + /// + /// + public static bool MatchNonIngNames(string eventName) + { + var splitter = new Regex(@"(? Name = name; - public EventNameExtractorResult(string? name) - { - Name = name; - } + public EventNameExtractorResult(EventNameExtractorError? error) => Error = error; - public EventNameExtractorResult(EventNameExtractorError? error) - { - Error = error; - } - } + public EventNameExtractorError? Error { get; } + + public string? Name { get; } } diff --git a/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs index 2026f41ff3..06b7ff81f4 100644 --- a/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs +++ b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs @@ -1,18 +1,17 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Events -{ - public class ExportedMemberEventArgs : EventArgs - { - public IMember Member { get; } - public MemberExportModel Exported { get; } +namespace Umbraco.Cms.Core.Events; - public ExportedMemberEventArgs(IMember member, MemberExportModel exported) - { - Member = member; - Exported = exported; - } +public class ExportedMemberEventArgs : EventArgs +{ + public ExportedMemberEventArgs(IMember member, MemberExportModel exported) + { + Member = member; + Exported = exported; } + + public IMember Member { get; } + + public MemberExportModel Exported { get; } } diff --git a/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs index 9a6a4357e0..4aaeeac29d 100644 --- a/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs +++ b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public interface IDeletingMediaFilesEventArgs { - public interface IDeletingMediaFilesEventArgs - { - List MediaFilesToDelete { get; } - } + List MediaFilesToDelete { get; } } diff --git a/src/Umbraco.Core/Events/IEventAggregator.cs b/src/Umbraco.Core/Events/IEventAggregator.cs index c654bb6c86..379f532be2 100644 --- a/src/Umbraco.Core/Events/IEventAggregator.cs +++ b/src/Umbraco.Core/Events/IEventAggregator.cs @@ -1,52 +1,49 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Threading; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Defines an object that channels events from multiple objects into a single object +/// to simplify registration for clients. +/// +public interface IEventAggregator { /// - /// Defines an object that channels events from multiple objects into a single object - /// to simplify registration for clients. + /// Asynchronously send a notification to multiple handlers of both sync and async /// - public interface IEventAggregator - { - /// - /// Asynchronously send a notification to multiple handlers of both sync and async - /// - /// The type of notification being handled. - /// The notification object. - /// An optional cancellation token. - /// A task that represents the publish operation. - Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) - where TNotification : INotification; + /// The type of notification being handled. + /// The notification object. + /// An optional cancellation token. + /// A task that represents the publish operation. + Task PublishAsync(TNotification notification, CancellationToken cancellationToken = default) + where TNotification : INotification; - /// - /// Synchronously send a notification to multiple handlers of both sync and async - /// - /// The type of notification being handled. - /// The notification object. - void Publish(TNotification notification) - where TNotification : INotification; + /// + /// Synchronously send a notification to multiple handlers of both sync and async + /// + /// The type of notification being handled. + /// The notification object. + void Publish(TNotification notification) + where TNotification : INotification; - /// - /// Publishes a cancelable notification to the notification subscribers - /// - /// The type of notification being handled. - /// - /// True if the notification was cancelled by a subscriber, false otherwise - bool PublishCancelable(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification; + /// + /// Publishes a cancelable notification to the notification subscribers + /// + /// The type of notification being handled. + /// + /// True if the notification was cancelled by a subscriber, false otherwise + bool PublishCancelable(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification; - /// - /// Publishes a cancelable notification async to the notification subscribers - /// - /// The type of notification being handled. - /// - /// True if the notification was cancelled by a subscriber, false otherwise - Task PublishCancelableAsync(TCancelableNotification notification) - where TCancelableNotification : ICancelableNotification; - } + /// + /// Publishes a cancelable notification async to the notification subscribers + /// + /// The type of notification being handled. + /// + /// True if the notification was cancelled by a subscriber, false otherwise + Task PublishCancelableAsync(TCancelableNotification notification) + where TCancelableNotification : ICancelableNotification; } diff --git a/src/Umbraco.Core/Events/IEventDefinition.cs b/src/Umbraco.Core/Events/IEventDefinition.cs index e3918113e1..d10b931548 100644 --- a/src/Umbraco.Core/Events/IEventDefinition.cs +++ b/src/Umbraco.Core/Events/IEventDefinition.cs @@ -1,11 +1,12 @@ -namespace Umbraco.Cms.Core.Events -{ - public interface IEventDefinition - { - object Sender { get; } - object Args { get; } - string? EventName { get; } +namespace Umbraco.Cms.Core.Events; - void RaiseEvent(); - } +public interface IEventDefinition +{ + object Sender { get; } + + object Args { get; } + + string? EventName { get; } + + void RaiseEvent(); } diff --git a/src/Umbraco.Core/Events/IEventDispatcher.cs b/src/Umbraco.Core/Events/IEventDispatcher.cs index bef94b6d4a..9d15a74c02 100644 --- a/src/Umbraco.Core/Events/IEventDispatcher.cs +++ b/src/Umbraco.Core/Events/IEventDispatcher.cs @@ -1,98 +1,98 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// Dispatches events from within a scope. +/// +/// +/// +/// The name of the event is auto-magically discovered by matching the sender type, args type, and +/// eventHandler type. If the match is not unique, then the name parameter must be used to specify the +/// name in an explicit way. +/// +/// +/// What happens when an event is dispatched depends on the scope settings. It can be anything from +/// "trigger immediately" to "just ignore". Refer to the scope documentation for more details. +/// +/// +public interface IEventDispatcher { + // not sure about the Dispatch & DispatchCancelable signatures at all for now + // nor about the event name thing, etc - but let's keep it like this + /// - /// Dispatches events from within a scope. + /// Dispatches a cancelable event. /// - /// - /// The name of the event is auto-magically discovered by matching the sender type, args type, and - /// eventHandler type. If the match is not unique, then the name parameter must be used to specify the - /// name in an explicit way. - /// What happens when an event is dispatched depends on the scope settings. It can be anything from - /// "trigger immediately" to "just ignore". Refer to the scope documentation for more details. - /// - public interface IEventDispatcher - { - // not sure about the Dispatch & DispatchCancelable signatures at all for now - // nor about the event name thing, etc - but let's keep it like this + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? name = null); - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? name = null); + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? name = null) + where TArgs : CancellableEventArgs; - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? name = null) - where TArgs : CancellableEventArgs; + /// + /// Dispatches a cancelable event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// A value indicating whether the cancelable event was cancelled. + /// See general remarks on the interface. + bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null) + where TArgs : CancellableEventArgs; - /// - /// Dispatches a cancelable event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// A value indicating whether the cancelable event was cancelled. - /// See general remarks on the interface. - bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null) - where TArgs : CancellableEventArgs; + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? name = null); - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? name = null); + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? name = null); - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? name = null); + /// + /// Dispatches an event. + /// + /// The event handler. + /// The object that raised the event. + /// The event data. + /// The optional name of the event. + /// See general remarks on the interface. + void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null); - /// - /// Dispatches an event. - /// - /// The event handler. - /// The object that raised the event. - /// The event data. - /// The optional name of the event. - /// See general remarks on the interface. - void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? name = null); + /// + /// Notifies the dispatcher that the scope is exiting. + /// + /// A value indicating whether the scope completed. + void ScopeExit(bool completed); - /// - /// Notifies the dispatcher that the scope is exiting. - /// - /// A value indicating whether the scope completed. - void ScopeExit(bool completed); - - /// - /// Gets the collected events. - /// - /// The collected events. - IEnumerable GetEvents(EventDefinitionFilter filter); - } + /// + /// Gets the collected events. + /// + /// The collected events. + IEnumerable GetEvents(EventDefinitionFilter filter); } diff --git a/src/Umbraco.Core/Events/IEventMessagesAccessor.cs b/src/Umbraco.Core/Events/IEventMessagesAccessor.cs index cffff705da..e88ba73dee 100644 --- a/src/Umbraco.Core/Events/IEventMessagesAccessor.cs +++ b/src/Umbraco.Core/Events/IEventMessagesAccessor.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public interface IEventMessagesAccessor { - public interface IEventMessagesAccessor - { - EventMessages? EventMessages { get; set; } - } + EventMessages? EventMessages { get; set; } } diff --git a/src/Umbraco.Core/Events/IEventMessagesFactory.cs b/src/Umbraco.Core/Events/IEventMessagesFactory.cs index 6abf6e8d41..9ade74d20a 100644 --- a/src/Umbraco.Core/Events/IEventMessagesFactory.cs +++ b/src/Umbraco.Core/Events/IEventMessagesFactory.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Events -{ - /// - /// Event messages factory - /// - public interface IEventMessagesFactory - { - EventMessages Get(); +namespace Umbraco.Cms.Core.Events; - EventMessages? GetOrDefault(); - } +/// +/// Event messages factory +/// +public interface IEventMessagesFactory +{ + EventMessages Get(); + + EventMessages? GetOrDefault(); } diff --git a/src/Umbraco.Core/Events/INotificationAsyncHandler.cs b/src/Umbraco.Core/Events/INotificationAsyncHandler.cs index cdcc21542f..25a46ed250 100644 --- a/src/Umbraco.Core/Events/INotificationAsyncHandler.cs +++ b/src/Umbraco.Core/Events/INotificationAsyncHandler.cs @@ -1,25 +1,22 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Threading; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Defines a handler for a async notification. +/// +/// The type of notification being handled. +public interface INotificationAsyncHandler + where TNotification : INotification { /// - /// Defines a handler for a async notification. + /// Handles a notification /// - /// The type of notification being handled. - public interface INotificationAsyncHandler - where TNotification : INotification - { - /// - /// Handles a notification - /// - /// The notification - /// The cancellation token. - /// A representing the asynchronous operation. - Task HandleAsync(TNotification notification, CancellationToken cancellationToken); - } + /// The notification + /// The cancellation token. + /// A representing the asynchronous operation. + Task HandleAsync(TNotification notification, CancellationToken cancellationToken); } diff --git a/src/Umbraco.Core/Events/INotificationHandler.cs b/src/Umbraco.Core/Events/INotificationHandler.cs index 548bec39b8..2111009faa 100644 --- a/src/Umbraco.Core/Events/INotificationHandler.cs +++ b/src/Umbraco.Core/Events/INotificationHandler.cs @@ -3,19 +3,18 @@ using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// Defines a handler for a notification. +/// +/// The type of notification being handled. +public interface INotificationHandler + where TNotification : INotification { /// - /// Defines a handler for a notification. + /// Handles a notification /// - /// The type of notification being handled. - public interface INotificationHandler - where TNotification : INotification - { - /// - /// Handles a notification - /// - /// The notification - void Handle(TNotification notification); - } + /// The notification + void Handle(TNotification notification); } diff --git a/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs b/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs index 58fdafc341..89962bbb9c 100644 --- a/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs +++ b/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs @@ -1,45 +1,45 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public interface IScopedNotificationPublisher { - public interface IScopedNotificationPublisher - { - /// - /// Suppresses all notifications from being added/created until the result object is disposed. - /// - /// - IDisposable Suppress(); + /// + /// Suppresses all notifications from being added/created until the result object is disposed. + /// + /// + IDisposable Suppress(); - /// - /// Publishes a cancelable notification to the notification subscribers - /// - /// - /// True if the notification was cancelled by a subscriber, false otherwise - bool PublishCancelable(ICancelableNotification notification); + /// + /// Publishes a cancelable notification to the notification subscribers + /// + /// + /// True if the notification was cancelled by a subscriber, false otherwise + bool PublishCancelable(ICancelableNotification notification); - /// - /// Publishes a cancelable notification to the notification subscribers - /// - /// - /// True if the notification was cancelled by a subscriber, false otherwise - Task PublishCancelableAsync(ICancelableNotification notification); + /// + /// Publishes a cancelable notification to the notification subscribers + /// + /// + /// True if the notification was cancelled by a subscriber, false otherwise + Task PublishCancelableAsync(ICancelableNotification notification); - /// - /// Publishes a notification to the notification subscribers - /// - /// - /// The notification is published upon successful completion of the current scope, i.e. when things have been saved/published/deleted etc. - void Publish(INotification notification); + /// + /// Publishes a notification to the notification subscribers + /// + /// + /// + /// The notification is published upon successful completion of the current scope, i.e. when things have been + /// saved/published/deleted etc. + /// + void Publish(INotification notification); - /// - /// Invokes publishing of all pending notifications within the current scope - /// - /// - void ScopeExit(bool completed); - } + /// + /// Invokes publishing of all pending notifications within the current scope + /// + /// + void ScopeExit(bool completed); } diff --git a/src/Umbraco.Core/Events/MacroErrorEventArgs.cs b/src/Umbraco.Core/Events/MacroErrorEventArgs.cs index 8d0e8dbfe1..876f7b99eb 100644 --- a/src/Umbraco.Core/Events/MacroErrorEventArgs.cs +++ b/src/Umbraco.Core/Events/MacroErrorEventArgs.cs @@ -1,42 +1,41 @@ -using System; using Umbraco.Cms.Core.Macros; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +// Provides information on the macro that caused an error +public class MacroErrorEventArgs : EventArgs { - // Provides information on the macro that caused an error - public class MacroErrorEventArgs : EventArgs - { - /// - /// Name of the faulting macro. - /// - public string? Name { get; set; } + /// + /// Name of the faulting macro. + /// + public string? Name { get; set; } - /// - /// Alias of the faulting macro. - /// - public string? Alias { get; set; } + /// + /// Alias of the faulting macro. + /// + public string? Alias { get; set; } - /// - /// Filename, file path, fully qualified class name, or other key used by the macro engine to do it's processing of the faulting macro. - /// - public string? MacroSource { get; set; } + /// + /// Filename, file path, fully qualified class name, or other key used by the macro engine to do it's processing of the + /// faulting macro. + /// + public string? MacroSource { get; set; } - /// - /// Exception raised. - /// - public Exception? Exception { get; set; } + /// + /// Exception raised. + /// + public Exception? Exception { get; set; } - /// - /// Gets or sets the desired behaviour when a matching macro causes an error. See - /// for definitions. By setting this in your event - /// you can override the default behaviour defined in UmbracoSettings.config. - /// - /// Macro error behaviour enum. - public MacroErrorBehaviour Behaviour { get; set; } + /// + /// Gets or sets the desired behaviour when a matching macro causes an error. See + /// for definitions. By setting this in your event + /// you can override the default behaviour defined in UmbracoSettings.config. + /// + /// Macro error behaviour enum. + public MacroErrorBehaviour Behaviour { get; set; } - /// - /// The HTML code to display when Behavior is Content. - /// - public string? Html { get; set; } - } + /// + /// The HTML code to display when Behavior is Content. + /// + public string? Html { get; set; } } diff --git a/src/Umbraco.Core/Events/MoveEventArgs.cs b/src/Umbraco.Core/Events/MoveEventArgs.cs index 2f65056353..312f1b8146 100644 --- a/src/Umbraco.Core/Events/MoveEventArgs.cs +++ b/src/Umbraco.Core/Events/MoveEventArgs.cs @@ -1,151 +1,163 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class MoveEventArgs : CancellableObjectEventArgs, IEquatable> { - public class MoveEventArgs : CancellableObjectEventArgs, IEquatable> + private IEnumerable>? _moveInfoCollection; + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// + /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(bool canCancel, EventMessages eventMessages, params MoveEventInfo[] moveInfo) + : base(default, canCancel, eventMessages) { - private IEnumerable>? _moveInfoCollection; - - /// - /// Constructor accepting a collection of MoveEventInfo objects - /// - /// - /// - /// - /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation - /// - public MoveEventArgs(bool canCancel, EventMessages eventMessages, params MoveEventInfo[] moveInfo) - : base(default, canCancel, eventMessages) + if (moveInfo.FirstOrDefault() is null) { - if (moveInfo.FirstOrDefault() is null) + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + + // assign the legacy props + EventObject = moveInfo.First().Entity; + } + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(EventMessages eventMessages, params MoveEventInfo[] moveInfo) + : base(default, eventMessages) + { + if (moveInfo.FirstOrDefault() is null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + + // assign the legacy props + EventObject = moveInfo.First().Entity; + } + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// + /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(bool canCancel, params MoveEventInfo[] moveInfo) + : base(default, canCancel) + { + if (moveInfo.FirstOrDefault() is null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + + // assign the legacy props + EventObject = moveInfo.First().Entity; + } + + /// + /// Constructor accepting a collection of MoveEventInfo objects + /// + /// + /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation + /// + public MoveEventArgs(params MoveEventInfo[] moveInfo) + : base(default) + { + if (moveInfo.FirstOrDefault() is null) + { + throw new ArgumentException("moveInfo argument must contain at least one item"); + } + + MoveInfoCollection = moveInfo; + + // assign the legacy props + EventObject = moveInfo.First().Entity; + } + + /// + /// Gets all MoveEventInfo objects used to create the object + /// + public IEnumerable>? MoveInfoCollection + { + get => _moveInfoCollection; + set + { + MoveEventInfo? first = value?.FirstOrDefault(); + if (first is null) { - throw new ArgumentException("moveInfo argument must contain at least one item"); + throw new InvalidOperationException("MoveInfoCollection must have at least one item"); } - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } + _moveInfoCollection = value; - /// - /// Constructor accepting a collection of MoveEventInfo objects - /// - /// - /// - /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation - /// - public MoveEventArgs(EventMessages eventMessages, params MoveEventInfo[] moveInfo) - : base(default, eventMessages) - { - if (moveInfo.FirstOrDefault() is null) - { - throw new ArgumentException("moveInfo argument must contain at least one item"); - } - - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } - - /// - /// Constructor accepting a collection of MoveEventInfo objects - /// - /// - /// - /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation - /// - public MoveEventArgs(bool canCancel, params MoveEventInfo[] moveInfo) - : base(default, canCancel) - { - if (moveInfo.FirstOrDefault() is null) - { - throw new ArgumentException("moveInfo argument must contain at least one item"); - } - - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } - - /// - /// Constructor accepting a collection of MoveEventInfo objects - /// - /// - /// A collection of MoveEventInfo objects that exposes all entities that have been moved during a single move operation - /// - public MoveEventArgs(params MoveEventInfo[] moveInfo) - : base(default) - { - if (moveInfo.FirstOrDefault() is null) - { - throw new ArgumentException("moveInfo argument must contain at least one item"); - } - - MoveInfoCollection = moveInfo; - //assign the legacy props - EventObject = moveInfo.First().Entity; - } - - - /// - /// Gets all MoveEventInfo objects used to create the object - /// - public IEnumerable>? MoveInfoCollection - { - get { return _moveInfoCollection; } - set - { - var first = value?.FirstOrDefault(); - if (first is null) - { - throw new InvalidOperationException("MoveInfoCollection must have at least one item"); - } - - _moveInfoCollection = value; - - //assign the legacy props - EventObject = first.Entity; - } - } - - public bool Equals(MoveEventArgs? other) - { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && (MoveInfoCollection?.Equals(other.MoveInfoCollection) ?? false); - } - - public override bool Equals(object? obj) - { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MoveEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - if (MoveInfoCollection is not null) - { - return (base.GetHashCode() * 397) ^ MoveInfoCollection.GetHashCode(); - } - - return base.GetHashCode() * 397; - } - } - - public static bool operator ==(MoveEventArgs left, MoveEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(MoveEventArgs left, MoveEventArgs right) - { - return !Equals(left, right); + // assign the legacy props + EventObject = first.Entity; } } + + public static bool operator ==(MoveEventArgs left, MoveEventArgs right) => Equals(left, right); + + public bool Equals(MoveEventArgs? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && (MoveInfoCollection?.Equals(other.MoveInfoCollection) ?? false); + } + + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((MoveEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + if (MoveInfoCollection is not null) + { + return (base.GetHashCode() * 397) ^ MoveInfoCollection.GetHashCode(); + } + + return base.GetHashCode() * 397; + } + } + + public static bool operator !=(MoveEventArgs left, MoveEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/MoveEventInfo.cs b/src/Umbraco.Core/Events/MoveEventInfo.cs index 126a3fd230..92c09c92a8 100644 --- a/src/Umbraco.Core/Events/MoveEventInfo.cs +++ b/src/Umbraco.Core/Events/MoveEventInfo.cs @@ -1,55 +1,70 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class MoveEventInfo : IEquatable> { - public class MoveEventInfo : IEquatable> + public MoveEventInfo(TEntity entity, string originalPath, int newParentId) { - public MoveEventInfo(TEntity entity, string originalPath, int newParentId) + Entity = entity; + OriginalPath = originalPath; + NewParentId = newParentId; + } + + public TEntity Entity { get; set; } + + public string OriginalPath { get; set; } + + public int NewParentId { get; set; } + + public static bool operator ==(MoveEventInfo left, MoveEventInfo right) => Equals(left, right); + + public bool Equals(MoveEventInfo? other) + { + if (ReferenceEquals(null, other)) { - Entity = entity; - OriginalPath = originalPath; - NewParentId = newParentId; + return false; } - public TEntity Entity { get; set; } - public string OriginalPath { get; set; } - public int NewParentId { get; set; } - - public bool Equals(MoveEventInfo? other) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return EqualityComparer.Default.Equals(Entity, other.Entity) && NewParentId == other.NewParentId && string.Equals(OriginalPath, other.OriginalPath); + return true; } - public override bool Equals(object? obj) + return EqualityComparer.Default.Equals(Entity, other.Entity) && NewParentId == other.NewParentId && + string.Equals(OriginalPath, other.OriginalPath); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MoveEventInfo) obj); + return false; } - public override int GetHashCode() + if (ReferenceEquals(this, obj)) { - unchecked - { - var hashCode = Entity is not null ? EqualityComparer.Default.GetHashCode(Entity) : base.GetHashCode(); - hashCode = (hashCode * 397) ^ NewParentId; - hashCode = (hashCode * 397) ^ OriginalPath.GetHashCode(); - return hashCode; - } + return true; } - public static bool operator ==(MoveEventInfo left, MoveEventInfo right) + if (obj.GetType() != GetType()) { - return Equals(left, right); + return false; } - public static bool operator !=(MoveEventInfo left, MoveEventInfo right) + return Equals((MoveEventInfo)obj); + } + + public override int GetHashCode() + { + unchecked { - return !Equals(left, right); + var hashCode = Entity is not null + ? EqualityComparer.Default.GetHashCode(Entity) + : base.GetHashCode(); + hashCode = (hashCode * 397) ^ NewParentId; + hashCode = (hashCode * 397) ^ OriginalPath.GetHashCode(); + return hashCode; } } + + public static bool operator !=(MoveEventInfo left, MoveEventInfo right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/NewEventArgs.cs b/src/Umbraco.Core/Events/NewEventArgs.cs index d3e8436d0e..0db72488aa 100644 --- a/src/Umbraco.Core/Events/NewEventArgs.cs +++ b/src/Umbraco.Core/Events/NewEventArgs.cs @@ -1,130 +1,136 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class NewEventArgs : CancellableObjectEventArgs, IEquatable> { - public class NewEventArgs : CancellableObjectEventArgs, IEquatable> + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, int parentId, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) { + Alias = alias; + ParentId = parentId; + } + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, TEntity? parent, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + Alias = alias; + Parent = parent; + } - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, int parentId, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) + public NewEventArgs(TEntity eventObject, string alias, int parentId, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + Alias = alias; + ParentId = parentId; + } + + public NewEventArgs(TEntity eventObject, string alias, TEntity? parent, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + Alias = alias; + Parent = parent; + } + + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, int parentId) + : base(eventObject, canCancel) + { + Alias = alias; + ParentId = parentId; + } + + public NewEventArgs(TEntity eventObject, bool canCancel, string alias, TEntity? parent) + : base(eventObject, canCancel) + { + Alias = alias; + Parent = parent; + } + + public NewEventArgs(TEntity eventObject, string alias, int parentId) + : base(eventObject) + { + Alias = alias; + ParentId = parentId; + } + + public NewEventArgs(TEntity eventObject, string alias, TEntity? parent) + : base(eventObject) + { + Alias = alias; + Parent = parent; + } + + /// + /// The entity being created + /// + public TEntity? Entity => EventObject; + + /// + /// Gets or Sets the Alias. + /// + public string Alias { get; } + + /// + /// Gets or Sets the Id of the parent. + /// + public int ParentId { get; } + + /// + /// Gets or Sets the parent IContent object. + /// + public TEntity? Parent { get; } + + public static bool operator ==(NewEventArgs left, NewEventArgs right) => Equals(left, right); + + public bool Equals(NewEventArgs? other) + { + if (ReferenceEquals(null, other)) { - Alias = alias; - ParentId = parentId; + return false; } - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, TEntity? parent, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) + if (ReferenceEquals(this, other)) { - Alias = alias; - Parent = parent; + return true; } - public NewEventArgs(TEntity eventObject, string @alias, int parentId, EventMessages eventMessages) - : base(eventObject, eventMessages) + return base.Equals(other) && string.Equals(Alias, other.Alias) && + EqualityComparer.Default.Equals(Parent, other.Parent) && ParentId == other.ParentId; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - Alias = alias; - ParentId = parentId; + return false; } - public NewEventArgs(TEntity eventObject, string @alias, TEntity? parent, EventMessages eventMessages) - : base(eventObject, eventMessages) + if (ReferenceEquals(this, obj)) { - Alias = alias; - Parent = parent; + return true; } - - - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, int parentId) : base(eventObject, canCancel) + if (obj.GetType() != GetType()) { - Alias = alias; - ParentId = parentId; + return false; } - public NewEventArgs(TEntity eventObject, bool canCancel, string @alias, TEntity? parent) - : base(eventObject, canCancel) + return Equals((NewEventArgs?)obj); + } + + public override int GetHashCode() + { + unchecked { - Alias = alias; - Parent = parent; - } - - public NewEventArgs(TEntity eventObject, string @alias, int parentId) : base(eventObject) - { - Alias = alias; - ParentId = parentId; - } - - public NewEventArgs(TEntity eventObject, string @alias, TEntity? parent) - : base(eventObject) - { - Alias = alias; - Parent = parent; - } - - /// - /// The entity being created - /// - public TEntity? Entity - { - get { return EventObject; } - } - - /// - /// Gets or Sets the Alias. - /// - public string Alias { get; private set; } - - /// - /// Gets or Sets the Id of the parent. - /// - public int ParentId { get; private set; } - - /// - /// Gets or Sets the parent IContent object. - /// - public TEntity? Parent { get; private set; } - - public bool Equals(NewEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && string.Equals(Alias, other.Alias) && EqualityComparer.Default.Equals(Parent, other.Parent) && ParentId == other.ParentId; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((NewEventArgs?) obj); - } - - public override int GetHashCode() - { - unchecked + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ Alias.GetHashCode(); + if (Parent is not null) { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ Alias.GetHashCode(); - if (Parent is not null) - { - hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Parent); - } - - hashCode = (hashCode * 397) ^ ParentId; - return hashCode; + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(Parent); } - } - public static bool operator ==(NewEventArgs left, NewEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(NewEventArgs left, NewEventArgs right) - { - return !Equals(left, right); + hashCode = (hashCode * 397) ^ ParentId; + return hashCode; } } + + public static bool operator !=(NewEventArgs left, NewEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs index a36368ea54..20398502a1 100644 --- a/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs +++ b/src/Umbraco.Core/Events/PassThroughEventDispatcher.cs @@ -1,60 +1,60 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// An IEventDispatcher that immediately raise all events. +/// +/// +/// This means that events will be raised during the scope transaction, +/// whatever happens, and the transaction could roll back in the end. +/// +internal class PassThroughEventDispatcher : IEventDispatcher { - /// - /// An IEventDispatcher that immediately raise all events. - /// - /// This means that events will be raised during the scope transaction, - /// whatever happens, and the transaction could roll back in the end. - internal class PassThroughEventDispatcher : IEventDispatcher + public bool DispatchCancelable(EventHandler? eventHandler, object sender, CancellableEventArgs args, string? eventName = null) { - public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler? eventHandler, object sender, TArgs args, string? eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler? eventHandler, TSender sender, TArgs args, string? eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } + eventHandler(sender, args); + return args.Cancel; + } - public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } + public void Dispatch(EventHandler? eventHandler, object sender, EventArgs args, string? eventName = null) => + eventHandler?.Invoke(sender, args); - public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - { - eventHandler?.Invoke(sender, args); - } + public void Dispatch(EventHandler? eventHandler, object sender, TArgs args, string? eventName = null) => eventHandler?.Invoke(sender, args); - public IEnumerable GetEvents(EventDefinitionFilter filter) - { - return Enumerable.Empty(); - } + public void Dispatch(TypedEventHandler? eventHandler, TSender sender, TArgs args, string? eventName = null) => eventHandler?.Invoke(sender, args); - public void ScopeExit(bool completed) - { } + public IEnumerable GetEvents(EventDefinitionFilter filter) => + Enumerable.Empty(); + + public void ScopeExit(bool completed) + { } } diff --git a/src/Umbraco.Core/Events/PublishEventArgs.cs b/src/Umbraco.Core/Events/PublishEventArgs.cs index 80b6dcd8c7..8a48a0cfa9 100644 --- a/src/Umbraco.Core/Events/PublishEventArgs.cs +++ b/src/Umbraco.Core/Events/PublishEventArgs.cs @@ -1,128 +1,141 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class PublishEventArgs : CancellableEnumerableObjectEventArgs, + IEquatable> { - public class PublishEventArgs : CancellableEnumerableObjectEventArgs, IEquatable> + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + /// + /// + public PublishEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) { - /// - /// Constructor accepting multiple entities that are used in the publish operation - /// - /// - /// - /// - public PublishEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) + } + + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + /// + public PublishEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public PublishEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public PublishEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) + { + } + + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + /// + /// + public PublishEventArgs(IEnumerable eventObject, bool canCancel, bool isAllPublished) + : base(eventObject, canCancel) + { + } + + /// + /// Constructor accepting multiple entities that are used in the publish operation + /// + /// + public PublishEventArgs(IEnumerable eventObject) + : base(eventObject) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + public PublishEventArgs(TEntity eventObject) + : base(new List { eventObject }) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public PublishEventArgs(TEntity eventObject, bool canCancel, bool isAllPublished) + : base(new List { eventObject }, canCancel) + { + } + + /// + /// Returns all entities that were published during the operation + /// + public IEnumerable? PublishedEntities => EventObject; + + public static bool operator ==(PublishEventArgs left, PublishEventArgs right) => + Equals(left, right); + + public bool Equals(PublishEventArgs? other) + { + if (ReferenceEquals(null, other)) { + return false; } - /// - /// Constructor accepting multiple entities that are used in the publish operation - /// - /// - /// - public PublishEventArgs(IEnumerable eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) + if (ReferenceEquals(this, other)) { + return true; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public PublishEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, eventMessages) + return base.Equals(other); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { + return false; } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public PublishEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) - : base(new List { eventObject }, canCancel, eventMessages) + if (ReferenceEquals(this, obj)) { + return true; } - /// - /// Constructor accepting multiple entities that are used in the publish operation - /// - /// - /// - /// - public PublishEventArgs(IEnumerable eventObject, bool canCancel, bool isAllPublished) - : base(eventObject, canCancel) + if (obj.GetType() != GetType()) { + return false; } - /// - /// Constructor accepting multiple entities that are used in the publish operation - /// - /// - public PublishEventArgs(IEnumerable eventObject) - : base(eventObject) - { - } + return Equals((PublishEventArgs)obj); + } - /// - /// Constructor accepting a single entity instance - /// - /// - public PublishEventArgs(TEntity eventObject) - : base(new List { eventObject }) + public override int GetHashCode() + { + unchecked { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public PublishEventArgs(TEntity eventObject, bool canCancel, bool isAllPublished) - : base(new List { eventObject }, canCancel) - { - } - - /// - /// Returns all entities that were published during the operation - /// - public IEnumerable? PublishedEntities => EventObject; - - public bool Equals(PublishEventArgs? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((PublishEventArgs) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397); - } - } - - public static bool operator ==(PublishEventArgs left, PublishEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(PublishEventArgs left, PublishEventArgs right) - { - return !Equals(left, right); + return base.GetHashCode() * 397; } } + + public static bool operator !=(PublishEventArgs left, PublishEventArgs right) => + !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcher.cs b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs index e79cd67cd8..bc8eac29a1 100644 --- a/src/Umbraco.Core/Events/QueuingEventDispatcher.cs +++ b/src/Umbraco.Core/Events/QueuingEventDispatcher.cs @@ -1,43 +1,38 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// An IEventDispatcher that queues events, and raise them when the scope +/// exits and has been completed. +/// +public class QueuingEventDispatcher : QueuingEventDispatcherBase { - /// - /// An IEventDispatcher that queues events, and raise them when the scope - /// exits and has been completed. - /// - public class QueuingEventDispatcher : QueuingEventDispatcherBase + private readonly MediaFileManager _mediaFileManager; + + public QueuingEventDispatcher(MediaFileManager mediaFileManager) + : base(true) => + _mediaFileManager = mediaFileManager; + + protected override void ScopeExitCompleted() { - private readonly MediaFileManager _mediaFileManager; - public QueuingEventDispatcher(MediaFileManager mediaFileManager) - : base(true) + // processing only the last instance of each event... + // this is probably far from perfect, because if eg a content is saved in a list + // and then as a single content, the two events will probably not be de-duplicated, + // but it's better than nothing + foreach (IEventDefinition e in GetEvents(EventDefinitionFilter.LastIn)) { - _mediaFileManager = mediaFileManager; - } + e.RaiseEvent(); - protected override void ScopeExitCompleted() - { - // processing only the last instance of each event... - // this is probably far from perfect, because if eg a content is saved in a list - // and then as a single content, the two events will probably not be de-duplicated, - // but it's better than nothing - - foreach (var e in GetEvents(EventDefinitionFilter.LastIn)) + // separating concerns means that this should probably not be here, + // but then where should it be (without making things too complicated)? + if (e.Args is IDeletingMediaFilesEventArgs delete && delete.MediaFilesToDelete.Count > 0) { - e.RaiseEvent(); - - // separating concerns means that this should probably not be here, - // but then where should it be (without making things too complicated)? - var delete = e.Args as IDeletingMediaFilesEventArgs; - if (delete != null && delete.MediaFilesToDelete.Count > 0) - _mediaFileManager.DeleteMediaFiles(delete.MediaFilesToDelete); + _mediaFileManager.DeleteMediaFiles(delete.MediaFilesToDelete); } } - - - } } diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs index 71b7647b4f..c259e271e5 100644 --- a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs +++ b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs @@ -1,344 +1,423 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +/// +/// An IEventDispatcher that queues events. +/// +/// +/// Can raise, or ignore, cancelable events, depending on option. +/// +/// Implementations must override ScopeExitCompleted to define what +/// to do with the events when the scope exits and has been completed. +/// +/// If the scope exits without being completed, events are ignored. +/// +public abstract class QueuingEventDispatcherBase : IEventDispatcher { - /// - /// An IEventDispatcher that queues events. - /// - /// - /// Can raise, or ignore, cancelable events, depending on option. - /// Implementations must override ScopeExitCompleted to define what - /// to do with the events when the scope exits and has been completed. - /// If the scope exits without being completed, events are ignored. - /// - public abstract class QueuingEventDispatcherBase : IEventDispatcher + private readonly bool _raiseCancelable; + + // events will be enlisted in the order they are raised + private List? _events; + + protected QueuingEventDispatcherBase(bool raiseCancelable) => _raiseCancelable = raiseCancelable; + + private List Events => _events ??= new List(); + + public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) { - //events will be enlisted in the order they are raised - private List? _events; - private readonly bool _raiseCancelable; - - protected QueuingEventDispatcherBase(bool raiseCancelable) + if (eventHandler == null) { - _raiseCancelable = raiseCancelable; - } - - private List Events => _events ?? (_events = new List()); - - public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string? eventName = null) - { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs + if (_raiseCancelable == false) { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) - where TArgs : CancellableEventArgs + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return args.Cancel; - if (_raiseCancelable == false) return args.Cancel; - eventHandler(sender, args); return args.Cancel; } - public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) + if (_raiseCancelable == false) { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + return args.Cancel; } - public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + return args.Cancel; } - public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) + if (_raiseCancelable == false) { - if (eventHandler == null) return; - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + return args.Cancel; } - public IEnumerable GetEvents(EventDefinitionFilter filter) + eventHandler(sender, args); + return args.Cancel; + } + + public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string? eventName = null) + { + if (eventHandler == null) { - if (_events == null) - return Enumerable.Empty(); - - IReadOnlyList events; - switch (filter) - { - case EventDefinitionFilter.All: - events = _events; - break; - case EventDefinitionFilter.FirstIn: - var l1 = new OrderedHashSet(); - foreach (var e in _events) - l1.Add(e); - events = l1; - break; - case EventDefinitionFilter.LastIn: - var l2 = new OrderedHashSet(keepOldest: false); - foreach (var e in _events) - l2.Add(e); - events = l2; - break; - default: - throw new ArgumentOutOfRangeException("filter", filter, null); - } - - return FilterSupersededAndUpdateToLatestEntity(events); + return; } - private class EventDefinitionInfos + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string? eventName = null) + { + if (eventHandler == null) { - public IEventDefinition? EventDefinition { get; set; } - public Type[]? SupersedeTypes { get; set; } + return; } - // this is way too convoluted, the supersede attribute is used only on DeleteEventargs to specify - // that it supersedes save, publish, move and copy - BUT - publish event args is also used for - // unpublishing and should NOT be superseded - so really it should not be managed at event args - // level but at event level - // - // what we want is: - // if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should - // not trigger for the entity - and even though, does it make any sense? making a copy of an entity - // should ... trigger? - // - // not going to refactor it all - we probably want to *always* trigger event but tell people that - // due to scopes, they should not expected eg a saved entity to still be around - however, now, - // going to write a ugly condition to deal with U4-10764 + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } - // iterates over the events (latest first) and filter out any events or entities in event args that are included - // in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want - // to raise the Saved event (well actually we just don't want to include it in the args for that saved event) - internal static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events) + public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string? eventName = null) + { + if (eventHandler == null) { - // keeps the 'latest' entity and associated event data - var entities = new List>(); + return; + } - // collects the event definitions - // collects the arguments in result, that require their entities to be updated - var result = new List(); - var resultArgs = new List(); + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } - // eagerly fetch superseded arg types for each arg type - var argTypeSuperceeding = events.Select(x => x.Args.GetType()) - .Distinct() - .ToDictionary(x => x, x => x.GetCustomAttributes(false).Select(y => y.SupersededEventArgsType).ToArray()); + public IEnumerable GetEvents(EventDefinitionFilter filter) + { + if (_events == null) + { + return Enumerable.Empty(); + } - // iterate over all events and filter - // - // process the list in reverse, because events are added in the order they are raised and we want to keep - // the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity - // is Deleted after being Saved, we want to filter out the Saved event - for (var index = events.Count - 1; index >= 0; index--) - { - var def = events[index]; - - var infos = new EventDefinitionInfos + IReadOnlyList events; + switch (filter) + { + case EventDefinitionFilter.All: + events = _events; + break; + case EventDefinitionFilter.FirstIn: + var l1 = new OrderedHashSet(); + foreach (IEventDefinition e in _events) { - EventDefinition = def, - SupersedeTypes = argTypeSuperceeding[def.Args.GetType()] - }; - - var args = def.Args as CancellableObjectEventArgs; - if (args == null) - { - // not a cancellable event arg, include event definition in result - result.Add(def); + l1.Add(e); } - else + + events = l1; + break; + case EventDefinitionFilter.LastIn: + var l2 = new OrderedHashSet(false); + foreach (IEventDefinition e in _events) { - // event object can either be a single object or an enumerable of objects - // try to get as an enumerable, get null if it's not - var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); - if (eventObjects == null) - { - // single object, cast as an IEntity - // if cannot cast, cannot filter, nothing - just include event definition in result - var eventEntity = args.EventObject as IEntity; - if (eventEntity == null) - { - result.Add(def); - continue; - } - - // look for this entity in superseding event args - // found = must be removed (ie not added), else track - if (IsSuperceeded(eventEntity, infos, entities) == false) - { - // track - entities.Add(Tuple.Create(eventEntity, infos)); - - // track result arguments - // include event definition in result - resultArgs.Add(args); - result.Add(def); - } - } - else - { - // enumerable of objects - var toRemove = new List(); - foreach (var eventObject in eventObjects) - { - // extract the event object, cast as an IEntity - // if cannot cast, cannot filter, nothing to do - just leave it in the list & continue - var eventEntity = eventObject as IEntity; - if (eventEntity == null) - continue; - - // look for this entity in superseding event args - // found = must be removed, else track - if (IsSuperceeded(eventEntity, infos, entities)) - toRemove.Add(eventEntity); - else - entities.Add(Tuple.Create(eventEntity, infos)); - } - - // remove superseded entities - foreach (var entity in toRemove) - eventObjects.Remove(entity); - - // if there are still entities in the list, keep the event definition - if (eventObjects.Count > 0) - { - if (toRemove.Count > 0) - { - // re-assign if changed - args.EventObject = eventObjects; - } - - // track result arguments - // include event definition in result - resultArgs.Add(args); - result.Add(def); - } - } + l2.Add(e); } - } - // go over all args in result, and update them with the latest instanceof each entity - UpdateToLatestEntities(entities, resultArgs); - - // reverse, since we processed the list in reverse - result.Reverse(); - - return result; + events = l2; + break; + default: + throw new ArgumentOutOfRangeException("filter", filter, null); } - // edits event args to use the latest instance of each entity - private static void UpdateToLatestEntities(IEnumerable> entities, IEnumerable args) - { - // get the latest entities - // ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates) - var latestEntities = new OrderedHashSet(keepOldest: true); - foreach (var entity in entities.OrderByDescending(entity => entity.Item1.UpdateDate)) - latestEntities.Add(entity.Item1); + return FilterSupersededAndUpdateToLatestEntity(events); + } - foreach (var arg in args) + public void ScopeExit(bool completed) + { + if (_events == null) + { + return; + } + + if (completed) + { + ScopeExitCompleted(); + } + + _events.Clear(); + } + + // this is way too convoluted, the supersede attribute is used only on DeleteEventargs to specify + // that it supersedes save, publish, move and copy - BUT - publish event args is also used for + // unpublishing and should NOT be superseded - so really it should not be managed at event args + // level but at event level + // + // what we want is: + // if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should + // not trigger for the entity - and even though, does it make any sense? making a copy of an entity + // should ... trigger? + // + // not going to refactor it all - we probably want to *always* trigger event but tell people that + // due to scopes, they should not expected eg a saved entity to still be around - however, now, + // going to write a ugly condition to deal with U4-10764 + + // iterates over the events (latest first) and filter out any events or entities in event args that are included + // in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want + // to raise the Saved event (well actually we just don't want to include it in the args for that saved event) + internal static IEnumerable FilterSupersededAndUpdateToLatestEntity( + IReadOnlyList events) + { + // keeps the 'latest' entity and associated event data + var entities = new List>(); + + // collects the event definitions + // collects the arguments in result, that require their entities to be updated + var result = new List(); + var resultArgs = new List(); + + // eagerly fetch superseded arg types for each arg type + var argTypeSuperceeding = events.Select(x => x.Args.GetType()) + .Distinct() + .ToDictionary( + x => x, + x => x.GetCustomAttributes(false).Select(y => y.SupersededEventArgsType) + .ToArray()); + + // iterate over all events and filter + // + // process the list in reverse, because events are added in the order they are raised and we want to keep + // the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity + // is Deleted after being Saved, we want to filter out the Saved event + for (var index = events.Count - 1; index >= 0; index--) + { + IEventDefinition def = events[index]; + + var infos = new EventDefinitionInfos + { + EventDefinition = def, + SupersedeTypes = argTypeSuperceeding[def.Args.GetType()], + }; + + var args = def.Args as CancellableObjectEventArgs; + if (args == null) + { + // not a cancellable event arg, include event definition in result + result.Add(def); + } + else { // event object can either be a single object or an enumerable of objects // try to get as an enumerable, get null if it's not - var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject); + IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); if (eventObjects == null) { - // single object - // look for a more recent entity for that object, and replace if any - // works by "equalling" entities ie the more recent one "equals" this one (though different object) - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject)); - if (foundEntity != null) - arg.EventObject = foundEntity; + // single object, cast as an IEntity + // if cannot cast, cannot filter, nothing - just include event definition in result + if (args.EventObject is not IEntity eventEntity) + { + result.Add(def); + continue; + } + + // look for this entity in superseding event args + // found = must be removed (ie not added), else track + if (IsSuperceeded(eventEntity, infos, entities) == false) + { + // track + entities.Add(Tuple.Create(eventEntity, infos)); + + // track result arguments + // include event definition in result + resultArgs.Add(args); + result.Add(def); + } } else { // enumerable of objects - // same as above but for each object - var updated = false; - for (var i = 0; i < eventObjects.Count; i++) + var toRemove = new List(); + foreach (var eventObject in eventObjects) { - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i])); - if (foundEntity == null) continue; - eventObjects[i] = foundEntity; - updated = true; + // extract the event object, cast as an IEntity + // if cannot cast, cannot filter, nothing to do - just leave it in the list & continue + if (eventObject is not IEntity eventEntity) + { + continue; + } + + // look for this entity in superseding event args + // found = must be removed, else track + if (IsSuperceeded(eventEntity, infos, entities)) + { + toRemove.Add(eventEntity); + } + else + { + entities.Add(Tuple.Create(eventEntity, infos)); + } } - if (updated) - arg.EventObject = eventObjects; + // remove superseded entities + foreach (IEntity entity in toRemove) + { + eventObjects.Remove(entity); + } + + // if there are still entities in the list, keep the event definition + if (eventObjects.Count > 0) + { + if (toRemove.Count > 0) + { + // re-assign if changed + args.EventObject = eventObjects; + } + + // track result arguments + // include event definition in result + resultArgs.Add(args); + result.Add(def); + } } } } - // determines if a given entity, appearing in a given event definition, should be filtered out, - // considering the entities that have already been visited - an entity is filtered out if it - // appears in another even definition, which supersedes this event definition. - private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List> entities) + // go over all args in result, and update them with the latest instanceof each entity + UpdateToLatestEntities(entities, resultArgs); + + // reverse, since we processed the list in reverse + result.Reverse(); + + return result; + } + + protected abstract void ScopeExitCompleted(); + + // edits event args to use the latest instance of each entity + private static void UpdateToLatestEntities( + IEnumerable> entities, + IEnumerable args) + { + // get the latest entities + // ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates) + var latestEntities = new OrderedHashSet(true); + foreach (Tuple entity in entities.OrderByDescending(entity => + entity.Item1.UpdateDate)) { - //var argType = meta.EventArgsType; - var argType = infos.EventDefinition?.Args.GetType(); + latestEntities.Add(entity.Item1); + } - // look for other instances of the same entity, coming from an event args that supersedes other event args, - // ie is marked with the attribute, and is not this event args (cannot supersede itself) - var superceeding = entities - .Where(x => x.Item2.SupersedeTypes?.Length > 0 // has the attribute - && x.Item2.EventDefinition?.Args.GetType() != argType // is not the same - && Equals(x.Item1, entity)) // same entity - .ToArray(); - - // first time we see this entity = not filtered - if (superceeding.Length == 0) - return false; - - // delete event args does NOT supersedes 'unpublished' event - if ((argType?.IsGenericType ?? false) && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition?.EventName == "Unpublished") - return false; - - // found occurrences, need to determine if this event args is superseded - if (argType?.IsGenericType ?? false) + foreach (CancellableObjectEventArgs arg in args) + { + // event object can either be a single object or an enumerable of objects + // try to get as an enumerable, get null if it's not + IList? eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject); + if (eventObjects == null) { - // generic, must compare type arguments - var supercededBy = superceeding.FirstOrDefault(x => - x.Item2.SupersedeTypes?.Any(y => - // superseding a generic type which has the same generic type definition - // (but ... no matter the generic type parameters? could be different?) - y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition() - // or superceeding a non-generic type which is ... (but... how is this ever possible? argType *is* generic? - || y.IsGenericTypeDefinition == false && y == argType) ?? false); - return supercededBy != null; + // single object + // look for a more recent entity for that object, and replace if any + // works by "equalling" entities ie the more recent one "equals" this one (though different object) + IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject)); + if (foundEntity != null) + { + arg.EventObject = foundEntity; + } } else { - // non-generic, can compare types 1:1 - var supercededBy = superceeding.FirstOrDefault(x => - x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false); - return supercededBy != null; + // enumerable of objects + // same as above but for each object + var updated = false; + for (var i = 0; i < eventObjects.Count; i++) + { + IEntity? foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i])); + if (foundEntity == null) + { + continue; + } + + eventObjects[i] = foundEntity; + updated = true; + } + + if (updated) + { + arg.EventObject = eventObjects; + } } } + } - public void ScopeExit(bool completed) + // determines if a given entity, appearing in a given event definition, should be filtered out, + // considering the entities that have already been visited - an entity is filtered out if it + // appears in another even definition, which supersedes this event definition. + private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List> entities) + { + // var argType = meta.EventArgsType; + Type? argType = infos.EventDefinition?.Args.GetType(); + + // look for other instances of the same entity, coming from an event args that supersedes other event args, + // ie is marked with the attribute, and is not this event args (cannot supersede itself) + Tuple[] superceeding = entities + .Where(x => x.Item2.SupersedeTypes?.Length > 0 // has the attribute + && x.Item2.EventDefinition?.Args.GetType() != argType // is not the same + && Equals(x.Item1, entity)) // same entity + .ToArray(); + + // first time we see this entity = not filtered + if (superceeding.Length == 0) { - if (_events == null) return; - if (completed) - ScopeExitCompleted(); - _events.Clear(); + return false; } - protected abstract void ScopeExitCompleted(); + // delete event args does NOT supersedes 'unpublished' event + if ((argType?.IsGenericType ?? false) && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && + infos.EventDefinition?.EventName == "Unpublished") + { + return false; + } + + // found occurrences, need to determine if this event args is superseded + if (argType?.IsGenericType ?? false) + { + // generic, must compare type arguments + Tuple? supercededBy = superceeding.FirstOrDefault(x => + x.Item2.SupersedeTypes?.Any(y => + + // superseding a generic type which has the same generic type definition + // (but ... no matter the generic type parameters? could be different?) + (y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition()) + + // or superceeding a non-generic type which is ... (but... how is this ever possible? argType *is* generic? + || (y.IsGenericTypeDefinition == false && y == argType)) ?? false); + return supercededBy != null; + } + else + { + // non-generic, can compare types 1:1 + Tuple? supercededBy = superceeding.FirstOrDefault(x => + x.Item2.SupersedeTypes?.Any(y => y == argType) ?? false); + return supercededBy != null; + } + } + + private class EventDefinitionInfos + { + public IEventDefinition? EventDefinition { get; set; } + + public Type[]? SupersedeTypes { get; set; } } } diff --git a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs index ee0d43a07a..44fb13016b 100644 --- a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs +++ b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs @@ -1,77 +1,84 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class RecycleBinEventArgs : CancellableEventArgs, IEquatable { - public class RecycleBinEventArgs : CancellableEventArgs, IEquatable + public RecycleBinEventArgs(Guid nodeObjectType, EventMessages eventMessages) + : base(true, eventMessages) => + NodeObjectType = nodeObjectType; + + public RecycleBinEventArgs(Guid nodeObjectType) + : base(true) => + NodeObjectType = nodeObjectType; + + /// + /// Gets the Id of the node object type of the items + /// being deleted from the Recycle Bin. + /// + public Guid NodeObjectType { get; } + + /// + /// Boolean indicating whether the Recycle Bin was emptied successfully + /// + public bool RecycleBinEmptiedSuccessfully { get; set; } + + /// + /// Boolean indicating whether this event was fired for the Content's Recycle Bin. + /// + public bool IsContentRecycleBin => NodeObjectType == Constants.ObjectTypes.Document; + + /// + /// Boolean indicating whether this event was fired for the Media's Recycle Bin. + /// + public bool IsMediaRecycleBin => NodeObjectType == Constants.ObjectTypes.Media; + + public static bool operator ==(RecycleBinEventArgs left, RecycleBinEventArgs right) => Equals(left, right); + + public bool Equals(RecycleBinEventArgs? other) { - public RecycleBinEventArgs(Guid nodeObjectType, EventMessages eventMessages) - : base(true, eventMessages) + if (ReferenceEquals(null, other)) { - NodeObjectType = nodeObjectType; + return false; } - public RecycleBinEventArgs(Guid nodeObjectType) - : base(true) + if (ReferenceEquals(this, other)) { - NodeObjectType = nodeObjectType; - + return true; } - /// - /// Gets the Id of the node object type of the items - /// being deleted from the Recycle Bin. - /// - public Guid NodeObjectType { get; } + return base.Equals(other) && NodeObjectType.Equals(other.NodeObjectType) && + RecycleBinEmptiedSuccessfully == other.RecycleBinEmptiedSuccessfully; + } - /// - /// Boolean indicating whether the Recycle Bin was emptied successfully - /// - public bool RecycleBinEmptiedSuccessfully { get; set; } - - /// - /// Boolean indicating whether this event was fired for the Content's Recycle Bin. - /// - public bool IsContentRecycleBin => NodeObjectType == Constants.ObjectTypes.Document; - - /// - /// Boolean indicating whether this event was fired for the Media's Recycle Bin. - /// - public bool IsMediaRecycleBin => NodeObjectType == Constants.ObjectTypes.Media; - - public bool Equals(RecycleBinEventArgs? other) + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && NodeObjectType.Equals(other.NodeObjectType) && RecycleBinEmptiedSuccessfully == other.RecycleBinEmptiedSuccessfully; + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((RecycleBinEventArgs) obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != GetType()) { - unchecked - { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ NodeObjectType.GetHashCode(); - hashCode = (hashCode * 397) ^ RecycleBinEmptiedSuccessfully.GetHashCode(); - return hashCode; - } + return false; } - public static bool operator ==(RecycleBinEventArgs left, RecycleBinEventArgs right) - { - return Equals(left, right); - } + return Equals((RecycleBinEventArgs)obj); + } - public static bool operator !=(RecycleBinEventArgs left, RecycleBinEventArgs right) + public override int GetHashCode() + { + unchecked { - return !Equals(left, right); + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ NodeObjectType.GetHashCode(); + hashCode = (hashCode * 397) ^ RecycleBinEmptiedSuccessfully.GetHashCode(); + return hashCode; } } + + public static bool operator !=(RecycleBinEventArgs left, RecycleBinEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Events/RefreshContentEventArgs.cs b/src/Umbraco.Core/Events/RefreshContentEventArgs.cs index c41043a039..00302e9f35 100644 --- a/src/Umbraco.Core/Events/RefreshContentEventArgs.cs +++ b/src/Umbraco.Core/Events/RefreshContentEventArgs.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Events -{ - //public class RefreshContentEventArgs : System.ComponentModel.CancelEventArgs { } -} +namespace Umbraco.Cms.Core.Events; + + +// public class RefreshContentEventArgs : System.ComponentModel.CancelEventArgs { } diff --git a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs index f37d8723a7..3817f93f6f 100644 --- a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs +++ b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs @@ -5,48 +5,49 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class RelateOnCopyNotificationHandler : INotificationHandler { - public class RelateOnCopyNotificationHandler : INotificationHandler + private readonly IAuditService _auditService; + private readonly IRelationService _relationService; + + public RelateOnCopyNotificationHandler(IRelationService relationService, IAuditService auditService) { - private readonly IRelationService _relationService; - private readonly IAuditService _auditService; + _relationService = relationService; + _auditService = auditService; + } - public RelateOnCopyNotificationHandler(IRelationService relationService, IAuditService auditService) + public void Handle(ContentCopiedNotification notification) + { + if (notification.RelateToOriginal == false) { - _relationService = relationService; - _auditService = auditService; + return; } - public void Handle(ContentCopiedNotification notification) + IRelationType? relationType = _relationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias); + + if (relationType == null) { - if (notification.RelateToOriginal == false) - { - return; - } + relationType = new RelationType( + Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, + Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, + true, + Constants.ObjectTypes.Document, + Constants.ObjectTypes.Document, + false); - var relationType = _relationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias); - - if (relationType == null) - { - relationType = new RelationType(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, - Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, - true, - Constants.ObjectTypes.Document, - Constants.ObjectTypes.Document, - false); - - _relationService.Save(relationType); - } - - var relation = new Relation(notification.Original.Id, notification.Copy.Id, relationType); - _relationService.Save(relation); - - _auditService.Add( - AuditType.Copy, - notification.Copy.WriterId, - notification.Copy.Id, ObjectTypes.GetName(UmbracoObjectTypes.Document) ?? string.Empty, - $"Copied content with Id: '{notification.Copy.Id}' related to original content with Id: '{notification.Original.Id}'"); + _relationService.Save(relationType); } + + var relation = new Relation(notification.Original.Id, notification.Copy.Id, relationType); + _relationService.Save(relation); + + _auditService.Add( + AuditType.Copy, + notification.Copy.WriterId, + notification.Copy.Id, + UmbracoObjectTypes.Document.GetName() ?? string.Empty, + $"Copied content with Id: '{notification.Copy.Id}' related to original content with Id: '{notification.Original.Id}'"); } } diff --git a/src/Umbraco.Core/Events/RolesEventArgs.cs b/src/Umbraco.Core/Events/RolesEventArgs.cs index a4fb6c3d18..a96de06713 100644 --- a/src/Umbraco.Core/Events/RolesEventArgs.cs +++ b/src/Umbraco.Core/Events/RolesEventArgs.cs @@ -1,16 +1,14 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class RolesEventArgs : EventArgs { - public class RolesEventArgs : EventArgs + public RolesEventArgs(int[] memberIds, string[] roles) { - public RolesEventArgs(int[] memberIds, string[] roles) - { - MemberIds = memberIds; - Roles = roles; - } - - public int[] MemberIds { get; set; } - public string[] Roles { get; set; } + MemberIds = memberIds; + Roles = roles; } + + public int[] MemberIds { get; set; } + + public string[] Roles { get; set; } } diff --git a/src/Umbraco.Core/Events/RollbackEventArgs.cs b/src/Umbraco.Core/Events/RollbackEventArgs.cs index 96b67ba769..d23ac75f9a 100644 --- a/src/Umbraco.Core/Events/RollbackEventArgs.cs +++ b/src/Umbraco.Core/Events/RollbackEventArgs.cs @@ -1,21 +1,19 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class RollbackEventArgs : CancellableObjectEventArgs { - public class RollbackEventArgs : CancellableObjectEventArgs + public RollbackEventArgs(TEntity eventObject, bool canCancel) + : base(eventObject, canCancel) { - public RollbackEventArgs(TEntity eventObject, bool canCancel) : base(eventObject, canCancel) - { - } - - public RollbackEventArgs(TEntity eventObject) : base(eventObject) - { - } - - /// - /// The entity being rolledback - /// - public TEntity? Entity - { - get { return EventObject; } - } } + + public RollbackEventArgs(TEntity eventObject) + : base(eventObject) + { + } + + /// + /// The entity being rolledback + /// + public TEntity? Entity => EventObject; } diff --git a/src/Umbraco.Core/Events/SaveEventArgs.cs b/src/Umbraco.Core/Events/SaveEventArgs.cs index 3424962a54..319a0726f2 100644 --- a/src/Umbraco.Core/Events/SaveEventArgs.cs +++ b/src/Umbraco.Core/Events/SaveEventArgs.cs @@ -1,117 +1,113 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +public class SaveEventArgs : CancellableEnumerableObjectEventArgs { - public class SaveEventArgs : CancellableEnumerableObjectEventArgs + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) { - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(eventObject, canCancel, messages, additionalData) - { - } - - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) - : base(eventObject, canCancel, eventMessages) - { - } - - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, EventMessages eventMessages) - : base(eventObject, eventMessages) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - /// - public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) - : base(new List { eventObject }, canCancel, messages, additionalData) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) - : base(new List { eventObject }, eventMessages) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - /// - public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) - : base(new List { eventObject }, canCancel, eventMessages) - { - } - - - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - /// - public SaveEventArgs(IEnumerable eventObject, bool canCancel) - : base(eventObject, canCancel) - { - } - - /// - /// Constructor accepting multiple entities that are used in the saving operation - /// - /// - public SaveEventArgs(IEnumerable eventObject) - : base(eventObject) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - public SaveEventArgs(TEntity eventObject) - : base(new List { eventObject }) - { - } - - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public SaveEventArgs(TEntity eventObject, bool canCancel) - : base(new List { eventObject }, canCancel) - { - } - - /// - /// Returns all entities that were saved during the operation - /// - public IEnumerable? SavedEntities => EventObject; } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) + : base(eventObject, canCancel, eventMessages) + { + } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, EventMessages eventMessages) + : base(eventObject, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(new List { eventObject }, canCancel, messages, additionalData) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) + : base(new List { eventObject }, eventMessages) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages eventMessages) + : base(new List { eventObject }, canCancel, eventMessages) + { + } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel) + : base(eventObject, canCancel) + { + } + + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + public SaveEventArgs(IEnumerable eventObject) + : base(eventObject) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + public SaveEventArgs(TEntity eventObject) + : base(new List { eventObject }) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel) + : base(new List { eventObject }, canCancel) + { + } + + /// + /// Returns all entities that were saved during the operation + /// + public IEnumerable? SavedEntities => EventObject; } diff --git a/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs b/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs index cdd8707a79..6681d321b7 100644 --- a/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs +++ b/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs @@ -1,135 +1,133 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class ScopedNotificationPublisher : IScopedNotificationPublisher { - public class ScopedNotificationPublisher : IScopedNotificationPublisher + private readonly IEventAggregator _eventAggregator; + private readonly object _locker = new(); + private readonly List _notificationOnScopeCompleted; + private bool _isSuppressed; + + public ScopedNotificationPublisher(IEventAggregator eventAggregator) { - private readonly IEventAggregator _eventAggregator; - private readonly List _notificationOnScopeCompleted; - private readonly object _locker = new object(); - private bool _isSuppressed = false; + _eventAggregator = eventAggregator; + _notificationOnScopeCompleted = new List(); + } - public ScopedNotificationPublisher(IEventAggregator eventAggregator) + public bool PublishCancelable(ICancelableNotification notification) + { + if (notification == null) { - _eventAggregator = eventAggregator; - _notificationOnScopeCompleted = new List(); + throw new ArgumentNullException(nameof(notification)); } - public bool PublishCancelable(ICancelableNotification notification) + if (_isSuppressed) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - if (_isSuppressed) - { - return false; - } - - _eventAggregator.Publish(notification); - return notification.Cancel; + return false; } - public async Task PublishCancelableAsync(ICancelableNotification notification) + _eventAggregator.Publish(notification); + return notification.Cancel; + } + + public async Task PublishCancelableAsync(ICancelableNotification notification) + { + if (notification == null) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - if (_isSuppressed) - { - return false; - } - - var task = _eventAggregator.PublishAsync(notification); - if (task is not null) - { - await task; - } - - return notification.Cancel; + throw new ArgumentNullException(nameof(notification)); } - public void Publish(INotification notification) + if (_isSuppressed) { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - if (_isSuppressed) - { - return; - } - - _notificationOnScopeCompleted.Add(notification); + return false; } - public void ScopeExit(bool completed) + Task task = _eventAggregator.PublishAsync(notification); + if (task is not null) { - try + await task; + } + + return notification.Cancel; + } + + public void Publish(INotification notification) + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + if (_isSuppressed) + { + return; + } + + _notificationOnScopeCompleted.Add(notification); + } + + public void ScopeExit(bool completed) + { + try + { + if (completed) { - if (completed) + foreach (INotification notification in _notificationOnScopeCompleted) { - foreach (INotification notification in _notificationOnScopeCompleted) + _eventAggregator.Publish(notification); + } + } + } + finally + { + _notificationOnScopeCompleted.Clear(); + } + } + + public IDisposable Suppress() + { + lock (_locker) + { + if (_isSuppressed) + { + throw new InvalidOperationException("Notifications are already suppressed"); + } + + return new Suppressor(this); + } + } + + private class Suppressor : IDisposable + { + private readonly ScopedNotificationPublisher _scopedNotificationPublisher; + private bool _disposedValue; + + public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) + { + _scopedNotificationPublisher = scopedNotificationPublisher; + _scopedNotificationPublisher._isSuppressed = true; + } + + public void Dispose() => Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + lock (_scopedNotificationPublisher._locker) { - _eventAggregator.Publish(notification); + _scopedNotificationPublisher._isSuppressed = false; } } - } - finally - { - _notificationOnScopeCompleted.Clear(); - } - } - public IDisposable Suppress() - { - lock(_locker) - { - if (_isSuppressed) - { - throw new InvalidOperationException("Notifications are already suppressed"); - } - return new Suppressor(this); + _disposedValue = true; } } - - private class Suppressor : IDisposable - { - private bool _disposedValue; - private readonly ScopedNotificationPublisher _scopedNotificationPublisher; - - public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) - { - _scopedNotificationPublisher = scopedNotificationPublisher; - _scopedNotificationPublisher._isSuppressed = true; - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - lock (_scopedNotificationPublisher._locker) - { - _scopedNotificationPublisher._isSuppressed = false; - } - } - _disposedValue = true; - } - } - public void Dispose() => Dispose(disposing: true); - } } } diff --git a/src/Umbraco.Core/Events/SendEmailEventArgs.cs b/src/Umbraco.Core/Events/SendEmailEventArgs.cs index c1e626c6c1..2e75d1b583 100644 --- a/src/Umbraco.Core/Events/SendEmailEventArgs.cs +++ b/src/Umbraco.Core/Events/SendEmailEventArgs.cs @@ -1,15 +1,10 @@ -using System; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Events -{ - public class SendEmailEventArgs : EventArgs - { - public EmailMessage Message { get; } +namespace Umbraco.Cms.Core.Events; - public SendEmailEventArgs(EmailMessage message) - { - Message = message; - } - } +public class SendEmailEventArgs : EventArgs +{ + public SendEmailEventArgs(EmailMessage message) => Message = message; + + public EmailMessage Message { get; } } diff --git a/src/Umbraco.Core/Events/SendToPublishEventArgs.cs b/src/Umbraco.Core/Events/SendToPublishEventArgs.cs index 9b4e078149..a72cd82012 100644 --- a/src/Umbraco.Core/Events/SendToPublishEventArgs.cs +++ b/src/Umbraco.Core/Events/SendToPublishEventArgs.cs @@ -1,21 +1,19 @@ -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class SendToPublishEventArgs : CancellableObjectEventArgs { - public class SendToPublishEventArgs : CancellableObjectEventArgs + public SendToPublishEventArgs(TEntity eventObject, bool canCancel) + : base(eventObject, canCancel) { - public SendToPublishEventArgs(TEntity eventObject, bool canCancel) : base(eventObject, canCancel) - { - } - - public SendToPublishEventArgs(TEntity eventObject) : base(eventObject) - { - } - - /// - /// The entity being sent to publish - /// - public TEntity? Entity - { - get { return EventObject; } - } } + + public SendToPublishEventArgs(TEntity eventObject) + : base(eventObject) + { + } + + /// + /// The entity being sent to publish + /// + public TEntity? Entity => EventObject; } diff --git a/src/Umbraco.Core/Events/SupersedeEventAttribute.cs b/src/Umbraco.Core/Events/SupersedeEventAttribute.cs index d733f0706a..21137968f0 100644 --- a/src/Umbraco.Core/Events/SupersedeEventAttribute.cs +++ b/src/Umbraco.Core/Events/SupersedeEventAttribute.cs @@ -1,20 +1,15 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events +/// +/// This is used to know if the event arg attributed should supersede another event arg type when +/// tracking events for the same entity. If one event args supersedes another then the event args that have been +/// superseded +/// will mean that the event will not be dispatched or the args will be filtered to exclude the entity. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class SupersedeEventAttribute : Attribute { - /// - /// This is used to know if the event arg attributed should supersede another event arg type when - /// tracking events for the same entity. If one event args supersedes another then the event args that have been superseded - /// will mean that the event will not be dispatched or the args will be filtered to exclude the entity. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class SupersedeEventAttribute : Attribute - { - public Type SupersededEventArgsType { get; private set; } + public SupersedeEventAttribute(Type supersededEventArgsType) => SupersededEventArgsType = supersededEventArgsType; - public SupersedeEventAttribute(Type supersededEventArgsType) - { - SupersededEventArgsType = supersededEventArgsType; - } - } + public Type SupersededEventArgsType { get; } } diff --git a/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs b/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs index 2c8dde89a2..8495da25b0 100644 --- a/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs +++ b/src/Umbraco.Core/Events/TransientEventMessagesFactory.cs @@ -1,18 +1,11 @@ -namespace Umbraco.Cms.Core.Events -{ - /// - /// A simple/default transient messages factory - /// - public class TransientEventMessagesFactory : IEventMessagesFactory - { - public EventMessages Get() - { - return new EventMessages(); - } +namespace Umbraco.Cms.Core.Events; - public EventMessages? GetOrDefault() - { - return null; - } - } +/// +/// A simple/default transient messages factory +/// +public class TransientEventMessagesFactory : IEventMessagesFactory +{ + public EventMessages Get() => new EventMessages(); + + public EventMessages? GetOrDefault() => null; } diff --git a/src/Umbraco.Core/Events/TypedEventHandler.cs b/src/Umbraco.Core/Events/TypedEventHandler.cs index 11301448e0..e359bd47f9 100644 --- a/src/Umbraco.Core/Events/TypedEventHandler.cs +++ b/src/Umbraco.Core/Events/TypedEventHandler.cs @@ -1,7 +1,4 @@ -using System; +namespace Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Events -{ - [Serializable] - public delegate void TypedEventHandler(TSender sender, TEventArgs e); -} +[Serializable] +public delegate void TypedEventHandler(TSender sender, TEventArgs e); diff --git a/src/Umbraco.Core/Events/UserGroupWithUsers.cs b/src/Umbraco.Core/Events/UserGroupWithUsers.cs index 17946a781f..f3a77e22e6 100644 --- a/src/Umbraco.Core/Events/UserGroupWithUsers.cs +++ b/src/Umbraco.Core/Events/UserGroupWithUsers.cs @@ -1,18 +1,19 @@ -using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class UserGroupWithUsers { - public class UserGroupWithUsers + public UserGroupWithUsers(IUserGroup userGroup, IUser[] addedUsers, IUser[] removedUsers) { - public UserGroupWithUsers(IUserGroup userGroup, IUser[] addedUsers, IUser[] removedUsers) - { - UserGroup = userGroup; - AddedUsers = addedUsers; - RemovedUsers = removedUsers; - } - - public IUserGroup UserGroup { get; } - public IUser[] AddedUsers { get; } - public IUser[] RemovedUsers { get; } + UserGroup = userGroup; + AddedUsers = addedUsers; + RemovedUsers = removedUsers; } + + public IUserGroup UserGroup { get; } + + public IUser[] AddedUsers { get; } + + public IUser[] RemovedUsers { get; } } diff --git a/src/Umbraco.Core/Events/UserNotificationsHandler.cs b/src/Umbraco.Core/Events/UserNotificationsHandler.cs index 96425e644f..042355630f 100644 --- a/src/Umbraco.Core/Events/UserNotificationsHandler.cs +++ b/src/Umbraco.Core/Events/UserNotificationsHandler.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Actions; @@ -18,221 +15,242 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public sealed class UserNotificationsHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { - public sealed class UserNotificationsHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + private readonly ActionCollection _actions; + private readonly IContentService _contentService; + private readonly Notifier _notifier; + + public UserNotificationsHandler(Notifier notifier, ActionCollection actions, IContentService contentService) { - private readonly Notifier _notifier; - private readonly ActionCollection _actions; - private readonly IContentService _contentService; + _notifier = notifier; + _actions = actions; + _contentService = contentService; + } - public UserNotificationsHandler(Notifier notifier, ActionCollection actions, IContentService contentService) + public void Handle(AssignedUserGroupPermissionsNotification notification) + { + IContent[]? entities = _contentService.GetByIds(notification.EntityPermissions.Select(e => e.EntityId)).ToArray(); + if (entities?.Any() == false) { - _notifier = notifier; - _actions = actions; - _contentService = contentService; + return; } - public void Handle(ContentSavedNotification notification) - { - var newEntities = new List(); - var updatedEntities = new List(); + _notifier.Notify(_actions.GetAction(), entities!); + } - //need to determine if this is updating or if it is new - foreach (var entity in notification.SavedEntities) + public void Handle(ContentCopiedNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.Original); + + public void Handle(ContentMovedNotification notification) + { + // notify about the move for all moved items + _notifier.Notify( + _actions.GetAction(), + notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); + + // for any items being moved from the recycle bin (restored), explicitly notify about that too + IContent[] restoredEntities = notification.MoveInfoCollection + .Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString)) + .Select(m => m.Entity) + .ToArray(); + if (restoredEntities.Any()) + { + _notifier.Notify(_actions.GetAction(), restoredEntities); + } + } + + public void Handle(ContentMovedToRecycleBinNotification notification) => _notifier.Notify( + _actions.GetAction(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); + + public void Handle(ContentPublishedNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.PublishedEntities.ToArray()); + + public void Handle(ContentRolledBackNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.Entity); + + public void Handle(ContentSavedNotification notification) + { + var newEntities = new List(); + var updatedEntities = new List(); + + // need to determine if this is updating or if it is new + foreach (IContent entity in notification.SavedEntities) + { + var dirty = (IRememberBeingDirty)entity; + if (dirty.WasPropertyDirty("Id")) { - var dirty = (IRememberBeingDirty)entity; - if (dirty.WasPropertyDirty("Id")) - { - //it's new - newEntities.Add(entity); - } - else - { - //it's updating - updatedEntities.Add(entity); - } + // it's new + newEntities.Add(entity); } - _notifier.Notify(_actions.GetAction(), newEntities.ToArray()); - _notifier.Notify(_actions.GetAction(), updatedEntities.ToArray()); - } - - public void Handle(ContentSortedNotification notification) - { - var parentId = notification.SortedEntities.Select(x => x.ParentId).Distinct().ToList(); - if (parentId.Count != 1) - return; // this shouldn't happen, for sorting all entities will have the same parent id - - // in this case there's nothing to report since if the root is sorted we can't report on a fake entity. - // this is how it was in v7, we can't report on root changes because you can't subscribe to root changes. - if (parentId[0] <= 0) - return; - - var parent = _contentService.GetById(parentId[0]); - if (parent == null) - return; // this shouldn't happen - - _notifier.Notify(_actions.GetAction(), new[] { parent }); - } - - public void Handle(ContentPublishedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.PublishedEntities.ToArray()); - - public void Handle(ContentMovedNotification notification) - { - // notify about the move for all moved items - _notifier.Notify(_actions.GetAction(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); - - // for any items being moved from the recycle bin (restored), explicitly notify about that too - var restoredEntities = notification.MoveInfoCollection - .Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString)) - .Select(m => m.Entity) - .ToArray(); - if (restoredEntities.Any()) + else { - _notifier.Notify(_actions.GetAction(), restoredEntities); + // it's updating + updatedEntities.Add(entity); } } - public void Handle(ContentMovedToRecycleBinNotification notification) => _notifier.Notify(_actions.GetAction(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray()); + _notifier.Notify(_actions.GetAction(), newEntities.ToArray()); + _notifier.Notify(_actions.GetAction(), updatedEntities.ToArray()); + } - public void Handle(ContentCopiedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Original); + public void Handle(ContentSentToPublishNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.Entity); - public void Handle(ContentRolledBackNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Entity); + public void Handle(ContentSortedNotification notification) + { + var parentId = notification.SortedEntities.Select(x => x.ParentId).Distinct().ToList(); + if (parentId.Count != 1) + { + return; // this shouldn't happen, for sorting all entities will have the same parent id + } - public void Handle(ContentSentToPublishNotification notification) => _notifier.Notify(_actions.GetAction(), notification.Entity); + // in this case there's nothing to report since if the root is sorted we can't report on a fake entity. + // this is how it was in v7, we can't report on root changes because you can't subscribe to root changes. + if (parentId[0] <= 0) + { + return; + } - public void Handle(ContentUnpublishedNotification notification) => _notifier.Notify(_actions.GetAction(), notification.UnpublishedEntities.ToArray()); + IContent? parent = _contentService.GetById(parentId[0]); + if (parent == null) + { + return; // this shouldn't happen + } + + _notifier.Notify(_actions.GetAction(), parent); + } + + public void Handle(ContentUnpublishedNotification notification) => + _notifier.Notify(_actions.GetAction(), notification.UnpublishedEntities.ToArray()); + + public void Handle(PublicAccessEntrySavedNotification notification) + { + IContent[] entities = _contentService.GetByIds(notification.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray(); + if (entities.Any() == false) + { + return; + } + + _notifier.Notify(_actions.GetAction(), entities); + } + + /// + /// This class is used to send the notifications + /// + public sealed class Notifier + { + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly INotificationService _notificationService; + private readonly ILocalizedTextService _textService; + private readonly IUserService _userService; + private GlobalSettings _globalSettings; /// - /// This class is used to send the notifications + /// Initializes a new instance of the class. /// - public sealed class Notifier + public Notifier( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IHostingEnvironment hostingEnvironment, + INotificationService notificationService, + IUserService userService, + ILocalizedTextService textService, + IOptionsMonitor globalSettings, + ILogger logger) { - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly INotificationService _notificationService; - private readonly IUserService _userService; - private readonly ILocalizedTextService _textService; - private GlobalSettings _globalSettings; - private readonly ILogger _logger; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _hostingEnvironment = hostingEnvironment; + _notificationService = notificationService; + _userService = userService; + _textService = textService; + _globalSettings = globalSettings.CurrentValue; + _logger = logger; - /// - /// Initializes a new instance of the class. - /// - public Notifier( - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IHostingEnvironment hostingEnvironment, - INotificationService notificationService, - IUserService userService, - ILocalizedTextService textService, - IOptionsMonitor globalSettings, - ILogger logger) + globalSettings.OnChange(x => _globalSettings = x); + } + + public void Notify(IAction? action, params IContent[] entities) + { + IUser? user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; + + // if there is no current user, then use the admin + if (user == null) { - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _hostingEnvironment = hostingEnvironment; - _notificationService = notificationService; - _userService = userService; - _textService = textService; - _globalSettings = globalSettings.CurrentValue; - _logger = logger; - - globalSettings.OnChange(x => _globalSettings = x); - } - - public void Notify(IAction? action, params IContent[] entities) - { - var user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser; - - //if there is no current user, then use the admin + _logger.LogDebug( + "There is no current Umbraco user logged in, the notifications will be sent from the administrator"); + user = _userService.GetUserById(Constants.Security.SuperUserId); if (user == null) { - _logger.LogDebug("There is no current Umbraco user logged in, the notifications will be sent from the administrator"); - user = _userService.GetUserById(Constants.Security.SuperUserId); - if (user == null) - { - _logger.LogWarning("Notifications can not be sent, no admin user with id {SuperUserId} could be resolved", Constants.Security.SuperUserId); - return; - } - } - - SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl); - } - - private void SendNotification(IUser sender, IEnumerable entities, IAction? action, Uri? siteUri) - { - if (sender == null) - throw new ArgumentNullException(nameof(sender)); - if (siteUri == null) - { - _logger.LogWarning("Notifications can not be sent, no site URL is set (might be during boot process?)"); + _logger.LogWarning( + "Notifications can not be sent, no admin user with id {SuperUserId} could be resolved", + Constants.Security.SuperUserId); return; } - - //group by the content type variation since the emails will be different - foreach (var contentVariantGroup in entities.GroupBy(x => x.ContentType.Variations)) - { - _notificationService.SendNotifications( - sender, - contentVariantGroup, - action?.Letter.ToString(CultureInfo.InvariantCulture), - _textService.Localize("actions", action?.Alias), - siteUri, - ((IUser user, NotificationEmailSubjectParams subject) x) - => _textService.Localize( - "notifications", "mailSubject", - x.user.GetUserCulture(_textService, _globalSettings), - new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }), - ((IUser user, NotificationEmailBodyParams body, bool isHtml) x) - => _textService.Localize( - "notifications", x.isHtml ? "mailBodyHtml" : "mailBody", - x.user.GetUserCulture(_textService, _globalSettings), - new[] - { - x.body.RecipientName, - x.body.Action, - x.body.ItemName, - x.body.EditedUser, - x.body.SiteUrl, - x.body.ItemId, - //format the summary depending on if it's variant or not - contentVariantGroup.Key == ContentVariation.Culture - ? (x.isHtml ? _textService.Localize("notifications", "mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications","mailBodyVariantSummary", new []{ x.body.Summary })) - : x.body.Summary, - x.body.ItemUrl - })); - } } + + SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl); } - public void Handle(AssignedUserGroupPermissionsNotification notification) + private void SendNotification(IUser sender, IEnumerable entities, IAction? action, Uri? siteUri) { - var entities = _contentService.GetByIds(notification.EntityPermissions.Select(e => e.EntityId))?.ToArray(); - if (entities?.Any() == false) + if (sender == null) { - return; + throw new ArgumentNullException(nameof(sender)); } - _notifier.Notify(_actions.GetAction(), entities!); - } - public void Handle(PublicAccessEntrySavedNotification notification) - { - var entities = _contentService.GetByIds(notification.SavedEntities.Select(e => e.ProtectedNodeId))?.ToArray(); - if (entities?.Any() == false) + if (siteUri == null) { + _logger.LogWarning("Notifications can not be sent, no site URL is set (might be during boot process?)"); return; } - _notifier.Notify(_actions.GetAction(), entities!); + + // group by the content type variation since the emails will be different + foreach (IGrouping contentVariantGroup in entities.GroupBy(x => + x.ContentType.Variations)) + { + _notificationService.SendNotifications( + sender, + contentVariantGroup, + action?.Letter.ToString(CultureInfo.InvariantCulture), + _textService.Localize("actions", action?.Alias), + siteUri, + x + => _textService.Localize( + "notifications", "mailSubject", x.user.GetUserCulture(_textService, _globalSettings), new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }), + x + => _textService.Localize( + "notifications", + x.isHtml ? "mailBodyHtml" : "mailBody", + x.user.GetUserCulture(_textService, _globalSettings), + new[] + { + x.body.RecipientName, x.body.Action, x.body.ItemName, x.body.EditedUser, x.body.SiteUrl, + x.body.ItemId, + + // format the summary depending on if it's variant or not + contentVariantGroup.Key == ContentVariation.Culture + ? x.isHtml + ? _textService.Localize("notifications", "mailBodyVariantHtmlSummary", new[] { x.body.Summary }) + : _textService.Localize("notifications", "mailBodyVariantSummary", new[] { x.body.Summary }) + : x.body.Summary, + x.body.ItemUrl, + })); + } } } } diff --git a/src/Umbraco.Core/Exceptions/AuthorizationException.cs b/src/Umbraco.Core/Exceptions/AuthorizationException.cs index fa2399fc5c..fd55a94b7b 100644 --- a/src/Umbraco.Core/Exceptions/AuthorizationException.cs +++ b/src/Umbraco.Core/Exceptions/AuthorizationException.cs @@ -1,45 +1,56 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// The exception that is thrown when authorization failed. +/// +/// +[Serializable] +public class AuthorizationException : Exception { /// - /// The exception that is thrown when authorization failed. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class AuthorizationException : Exception + public AuthorizationException() { - /// - /// Initializes a new instance of the class. - /// - public AuthorizationException() - { } + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public AuthorizationException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public AuthorizationException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public AuthorizationException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public AuthorizationException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected AuthorizationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected AuthorizationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Core/Exceptions/BootFailedException.cs b/src/Umbraco.Core/Exceptions/BootFailedException.cs index eeac07869d..5ade44a68f 100644 --- a/src/Umbraco.Core/Exceptions/BootFailedException.cs +++ b/src/Umbraco.Core/Exceptions/BootFailedException.cs @@ -1,83 +1,96 @@ -using System; using System.Runtime.Serialization; using System.Text; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// An exception that is thrown if the Umbraco application cannot boot. +/// +/// +[Serializable] +public class BootFailedException : Exception { /// - /// An exception that is thrown if the Umbraco application cannot boot. + /// Defines the default boot failed exception message. /// - /// - [Serializable] - public class BootFailedException : Exception + public const string DefaultMessage = "Boot failed: Umbraco cannot run. See Umbraco's log file for more details."; + + /// + /// Initializes a new instance of the class. + /// + public BootFailedException() { - /// - /// Defines the default boot failed exception message. - /// - public const string DefaultMessage = "Boot failed: Umbraco cannot run. See Umbraco's log file for more details."; + } - /// - /// Initializes a new instance of the class. - /// - public BootFailedException() - { } + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public BootFailedException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public BootFailedException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception which is the cause of this exception. + /// + /// The message that describes the error. + /// The inner exception, or null. + public BootFailedException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception which is the cause of this exception. - /// - /// The message that describes the error. - /// The inner exception, or null. - public BootFailedException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected BootFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected BootFailedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } - - /// - /// Rethrows a captured . - /// - /// The boot failed exception. - /// - /// - /// - /// The exception can be null, in which case a default message is used. - /// - public static void Rethrow(BootFailedException? bootFailedException) + /// + /// Rethrows a captured . + /// + /// The boot failed exception. + /// + /// + /// + /// The exception can be null, in which case a default message is used. + /// + public static void Rethrow(BootFailedException? bootFailedException) + { + if (bootFailedException == null) { - if (bootFailedException == null) - throw new BootFailedException(DefaultMessage); - - // see https://stackoverflow.com/questions/57383 - // would that be the correct way to do it? - //ExceptionDispatchInfo.Capture(bootFailedException).Throw(); - - Exception? e = bootFailedException; - var m = new StringBuilder(); - m.Append(DefaultMessage); - while (e != null) - { - m.Append($"\n\n-> {e.GetType().FullName}: {e.Message}"); - if (string.IsNullOrWhiteSpace(e.StackTrace) == false) - m.Append($"\n{e.StackTrace}"); - e = e.InnerException; - } - throw new BootFailedException(m.ToString()); + throw new BootFailedException(DefaultMessage); } + + // see https://stackoverflow.com/questions/57383 + // would that be the correct way to do it? + // ExceptionDispatchInfo.Capture(bootFailedException).Throw(); + Exception? e = bootFailedException; + var m = new StringBuilder(); + m.Append(DefaultMessage); + while (e != null) + { + m.Append($"\n\n-> {e.GetType().FullName}: {e.Message}"); + if (string.IsNullOrWhiteSpace(e.StackTrace) == false) + { + m.Append($"\n{e.StackTrace}"); + } + + e = e.InnerException; + } + + throw new BootFailedException(m.ToString()); } } diff --git a/src/Umbraco.Core/Exceptions/ConfigurationException.cs b/src/Umbraco.Core/Exceptions/ConfigurationException.cs index fe711a9823..89d8bfc01d 100644 --- a/src/Umbraco.Core/Exceptions/ConfigurationException.cs +++ b/src/Umbraco.Core/Exceptions/ConfigurationException.cs @@ -1,41 +1,47 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// An exception that is thrown if the configuration is wrong. +/// +/// +[Serializable] +public class ConfigurationException : Exception { /// - /// An exception that is thrown if the configuration is wrong. + /// Initializes a new instance of the class with a specified error message. /// - /// - [Serializable] - public class ConfigurationException : Exception + /// The message that describes the error. + public ConfigurationException(string message) + : base(message) { - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public ConfigurationException(string message) - : base(message) - { } + } - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception which is the cause of this exception. - /// - /// The message that describes the error. - /// The inner exception, or null. - public ConfigurationException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected ConfigurationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception which is the cause of this exception. + /// + /// The message that describes the error. + /// The inner exception, or null. + public ConfigurationException(string message, Exception innerException) + : base(message, innerException) + { + } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected ConfigurationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Core/Exceptions/DataOperationException.cs b/src/Umbraco.Core/Exceptions/DataOperationException.cs index 0b56cfb72c..9acc6ded38 100644 --- a/src/Umbraco.Core/Exceptions/DataOperationException.cs +++ b/src/Umbraco.Core/Exceptions/DataOperationException.cs @@ -1,98 +1,111 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// +/// +/// +[Serializable] +public class DataOperationException : Exception + where T : Enum { /// - /// + /// Initializes a new instance of the class. /// - /// - /// - [Serializable] - public class DataOperationException : Exception - where T : Enum + public DataOperationException() { - /// - /// Gets the operation. - /// - /// - /// The operation. - /// - /// - /// This object should be serializable to prevent a to be thrown. - /// - public T? Operation { get; private set; } + } - /// - /// Initializes a new instance of the class. - /// - public DataOperationException() - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public DataOperationException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public DataOperationException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public DataOperationException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public DataOperationException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The operation. + public DataOperationException(T operation) + : this(operation, "Data operation exception: " + operation) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The operation. - public DataOperationException(T operation) - : this(operation, "Data operation exception: " + operation) - { } + /// + /// Initializes a new instance of the class. + /// + /// The operation. + /// The message. + public DataOperationException(T operation, string message) + : base(message) => + Operation = operation; - /// - /// Initializes a new instance of the class. - /// - /// The operation. - /// The message. - public DataOperationException(T operation, string message) - : base(message) + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// info + protected DataOperationException(SerializationInfo info, StreamingContext context) + : base(info, context) => + Operation = (T)Enum.Parse(typeof(T), info.GetString(nameof(Operation)) ?? string.Empty); + + /// + /// Gets the operation. + /// + /// + /// The operation. + /// + /// + /// This object should be serializable to prevent a to be thrown. + /// + public T? Operation { get; private set; } + + /// + /// When overridden in a derived class, sets the with + /// information about the exception. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// info + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) { - Operation = operation; + throw new ArgumentNullException(nameof(info)); } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// info - protected DataOperationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - Operation = (T)Enum.Parse(typeof(T), info.GetString(nameof(Operation)) ?? string.Empty); - } + info.AddValue(nameof(Operation), Operation is not null ? Enum.GetName(typeof(T), Operation) : string.Empty); - /// - /// When overridden in a derived class, sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// info - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - info.AddValue(nameof(Operation), Operation is not null ? Enum.GetName(typeof(T), Operation) : string.Empty); - - base.GetObjectData(info, context); - } + base.GetObjectData(info, context); } } diff --git a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs index ba8c2b6106..9bc51d7b6e 100644 --- a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs +++ b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs @@ -1,166 +1,192 @@ -using System; using System.Runtime.Serialization; -using Umbraco.Extensions; using System.Text; +using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// The exception that is thrown when a composition is invalid. +/// +/// +[Serializable] +public class InvalidCompositionException : Exception { /// - /// The exception that is thrown when a composition is invalid. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class InvalidCompositionException : Exception + public InvalidCompositionException() { - /// - /// Gets the content type alias. - /// - /// - /// The content type alias. - /// - public string? ContentTypeAlias { get; } + } - /// - /// Gets the added composition alias. - /// - /// - /// The added composition alias. - /// - public string? AddedCompositionAlias { get; } + /// + /// Initializes a new instance of the class. + /// + /// The content type alias. + /// The property type aliases. + public InvalidCompositionException(string contentTypeAlias, string[] propertyTypeAliases) + : this(contentTypeAlias, null, propertyTypeAliases) + { + } - /// - /// Gets the property type aliases. - /// - /// - /// The property type aliases. - /// - public string[]? PropertyTypeAliases { get; } + /// + /// Initializes a new instance of the class. + /// + /// The content type alias. + /// The added composition alias. + /// The property type aliases. + public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases) + : this(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, new string[0]) + { + } - /// - /// Gets the property group aliases. - /// - /// - /// The property group aliases. - /// - public string[]? PropertyGroupAliases { get; } + /// + /// Initializes a new instance of the class. + /// + /// The content type alias. + /// The added composition alias. + /// The property type aliases. + /// The property group aliases. + public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases) + : this(FormatMessage(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, propertyGroupAliases)) + { + ContentTypeAlias = contentTypeAlias; + AddedCompositionAlias = addedCompositionAlias; + PropertyTypeAliases = propertyTypeAliases; + PropertyGroupAliases = propertyGroupAliases; + } - /// - /// Initializes a new instance of the class. - /// - public InvalidCompositionException() - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public InvalidCompositionException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The content type alias. - /// The property type aliases. - public InvalidCompositionException(string contentTypeAlias, string[] propertyTypeAliases) - : this(contentTypeAlias, null, propertyTypeAliases) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public InvalidCompositionException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The content type alias. - /// The added composition alias. - /// The property type aliases. - public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases) - : this(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, new string[0]) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected InvalidCompositionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + ContentTypeAlias = info.GetString(nameof(ContentTypeAlias)); + AddedCompositionAlias = info.GetString(nameof(AddedCompositionAlias)); + PropertyTypeAliases = (string[]?)info.GetValue(nameof(PropertyTypeAliases), typeof(string[])); + PropertyGroupAliases = (string[]?)info.GetValue(nameof(PropertyGroupAliases), typeof(string[])); + } - /// - /// Initializes a new instance of the class. - /// - /// The content type alias. - /// The added composition alias. - /// The property type aliases. - /// The property group aliases. - public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases) - : this(FormatMessage(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, propertyGroupAliases)) + /// + /// Gets the content type alias. + /// + /// + /// The content type alias. + /// + public string? ContentTypeAlias { get; } + + /// + /// Gets the added composition alias. + /// + /// + /// The added composition alias. + /// + public string? AddedCompositionAlias { get; } + + /// + /// Gets the property type aliases. + /// + /// + /// The property type aliases. + /// + public string[]? PropertyTypeAliases { get; } + + /// + /// Gets the property group aliases. + /// + /// + /// The property group aliases. + /// + public string[]? PropertyGroupAliases { get; } + + /// + /// When overridden in a derived class, sets the with + /// information about the exception. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// info + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) { - ContentTypeAlias = contentTypeAlias; - AddedCompositionAlias = addedCompositionAlias; - PropertyTypeAliases = propertyTypeAliases; - PropertyGroupAliases = propertyGroupAliases; + throw new ArgumentNullException(nameof(info)); } - private static string FormatMessage(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases) + info.AddValue(nameof(ContentTypeAlias), ContentTypeAlias); + info.AddValue(nameof(AddedCompositionAlias), AddedCompositionAlias); + info.AddValue(nameof(PropertyTypeAliases), PropertyTypeAliases); + info.AddValue(nameof(PropertyGroupAliases), PropertyGroupAliases); + + base.GetObjectData(info, context); + } + + private static string FormatMessage(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases) + { + var sb = new StringBuilder(); + + if (addedCompositionAlias.IsNullOrWhiteSpace()) { - var sb = new StringBuilder(); - - if (addedCompositionAlias.IsNullOrWhiteSpace()) - { - sb.AppendFormat("Content type with alias '{0}' has an invalid composition.", contentTypeAlias); - } - else - { - sb.AppendFormat("Content type with alias '{0}' was added as a composition to content type with alias '{1}', but there was a conflict.", addedCompositionAlias, contentTypeAlias); - } - - if (propertyTypeAliases.Length > 0) - { - sb.AppendFormat(" Property types must have a unique alias across all compositions, these aliases are duplicate: {0}.", string.Join(", ", propertyTypeAliases)); - } - - if (propertyGroupAliases.Length > 0) - { - sb.AppendFormat(" Property groups with the same alias must also have the same type across all compositions, these aliases have different types: {0}.", string.Join(", ", propertyGroupAliases)); - } - - return sb.ToString(); + sb.AppendFormat("Content type with alias '{0}' has an invalid composition.", contentTypeAlias); + } + else + { + sb.AppendFormat( + "Content type with alias '{0}' was added as a composition to content type with alias '{1}', but there was a conflict.", + addedCompositionAlias, + contentTypeAlias); } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public InvalidCompositionException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public InvalidCompositionException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected InvalidCompositionException(SerializationInfo info, StreamingContext context) - : base(info, context) + if (propertyTypeAliases.Length > 0) { - ContentTypeAlias = info.GetString(nameof(ContentTypeAlias)); - AddedCompositionAlias = info.GetString(nameof(AddedCompositionAlias)); - PropertyTypeAliases = (string[]?)info.GetValue(nameof(PropertyTypeAliases), typeof(string[])); - PropertyGroupAliases = (string[] ?)info.GetValue(nameof(PropertyGroupAliases), typeof(string[])); + sb.AppendFormat( + " Property types must have a unique alias across all compositions, these aliases are duplicate: {0}.", + string.Join(", ", propertyTypeAliases)); } - /// - /// When overridden in a derived class, sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// info - public override void GetObjectData(SerializationInfo info, StreamingContext context) + if (propertyGroupAliases.Length > 0) { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - info.AddValue(nameof(ContentTypeAlias), ContentTypeAlias); - info.AddValue(nameof(AddedCompositionAlias), AddedCompositionAlias); - info.AddValue(nameof(PropertyTypeAliases), PropertyTypeAliases); - info.AddValue(nameof(PropertyGroupAliases), PropertyGroupAliases); - - base.GetObjectData(info, context); + sb.AppendFormat( + " Property groups with the same alias must also have the same type across all compositions, these aliases have different types: {0}.", + string.Join(", ", propertyGroupAliases)); } + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs index 9ba1311e84..99ce96c273 100644 --- a/src/Umbraco.Core/Exceptions/PanicException.cs +++ b/src/Umbraco.Core/Exceptions/PanicException.cs @@ -1,45 +1,57 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// Represents an internal exception that in theory should never been thrown, it is only thrown in circumstances that +/// should never happen. +/// +/// +[Serializable] +public class PanicException : Exception { /// - /// Represents an internal exception that in theory should never been thrown, it is only thrown in circumstances that should never happen. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class PanicException : Exception + public PanicException() { - /// - /// Initializes a new instance of the class. - /// - public PanicException() - { } + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public PanicException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public PanicException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public PanicException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public PanicException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected PanicException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected PanicException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs b/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs index 2a2b97b23d..f65da50745 100644 --- a/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs +++ b/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs @@ -1,46 +1,53 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Exceptions +namespace Umbraco.Cms.Core.Exceptions; + +/// +/// An exception that is thrown if an unattended installation occurs. +/// +[Serializable] +public class UnattendedInstallException : Exception { /// - /// An exception that is thrown if an unattended installation occurs. + /// Initializes a new instance of the class. /// - [Serializable] - public class UnattendedInstallException : Exception + public UnattendedInstallException() { - /// - /// Initializes a new instance of the class. - /// - public UnattendedInstallException() - { - } + } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public UnattendedInstallException(string message) : base(message) - { - } + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public UnattendedInstallException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception which is the cause of this exception. - /// - /// The message that describes the error. - /// The inner exception, or null. - public UnattendedInstallException(string message, Exception innerException) : base(message, innerException) - { - } + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception which is the cause of this exception. + /// + /// The message that describes the error. + /// The inner exception, or null. + public UnattendedInstallException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected UnattendedInstallException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected UnattendedInstallException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Core/ExpressionHelper.cs b/src/Umbraco.Core/ExpressionHelper.cs index 1895364d17..79e03d7b93 100644 --- a/src/Umbraco.Core/ExpressionHelper.cs +++ b/src/Umbraco.Core/ExpressionHelper.cs @@ -1,372 +1,441 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using Umbraco.Cms.Core.Persistence; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// A set of helper methods for dealing with expressions +/// +/// +public static class ExpressionHelper { + private static readonly ConcurrentDictionary PropertyInfoCache = new(); + /// - /// A set of helper methods for dealing with expressions + /// Gets a object from an expression. /// + /// The type of the source. + /// The type of the property. + /// The source. + /// The property lambda. + /// /// - public static class ExpressionHelper - { - private static readonly ConcurrentDictionary PropertyInfoCache = new ConcurrentDictionary(); + public static PropertyInfo GetPropertyInfo( + this TSource source, + Expression> propertyLambda) => GetPropertyInfo(propertyLambda); - /// - /// Gets a object from an expression. - /// - /// The type of the source. - /// The type of the property. - /// The source. - /// The property lambda. - /// - /// - public static PropertyInfo GetPropertyInfo(this TSource source, Expression> propertyLambda) - { - return GetPropertyInfo(propertyLambda); - } - - /// - /// Gets a object from an expression. - /// - /// The type of the source. - /// The type of the property. - /// The property lambda. - /// - /// - public static PropertyInfo GetPropertyInfo(Expression> propertyLambda) - { - return PropertyInfoCache.GetOrAdd( - new LambdaExpressionCacheKey(propertyLambda), - x => - { - var type = typeof(TSource); - - var member = propertyLambda.Body as MemberExpression; - if (member == null) - { - if (propertyLambda.Body.GetType().Name == "UnaryExpression") - { - // The expression might be for some boxing, e.g. representing a value type like HiveId as an object - // in which case the expression will be Convert(x.MyProperty) - var unary = propertyLambda.Body as UnaryExpression; - if (unary != null) - { - var boxedMember = unary.Operand as MemberExpression; - if (boxedMember == null) - throw new ArgumentException("The type of property could not be inferred, try specifying the type parameters explicitly. This can happen if you have tried to access PropertyInfo where the property's return type is a value type, but the expression is trying to convert it to an object"); - else member = boxedMember; - } - } - else throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda)); - } - - - var propInfo = member!.Member as PropertyInfo; - if (propInfo == null) - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a field, not a property.", - propertyLambda)); - - if (type != propInfo.ReflectedType && - !type.IsSubclassOf(propInfo.ReflectedType!)) - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a property that is not from type {1}.", - propertyLambda, - type)); - - return propInfo; - }); - } - - public static (MemberInfo, string?) FindProperty(LambdaExpression lambda) - { - void Throw() + /// + /// Gets a object from an expression. + /// + /// The type of the source. + /// The type of the property. + /// The property lambda. + /// + /// + public static PropertyInfo + GetPropertyInfo(Expression> propertyLambda) => + PropertyInfoCache.GetOrAdd( + new LambdaExpressionCacheKey(propertyLambda), + x => { - throw new ArgumentException($"Expression '{lambda}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.", nameof(lambda)); - } + Type type = typeof(TSource); - Expression expr = lambda; - var loop = true; - string? alias = null; - while (loop) - { - switch (expr.NodeType) + var member = propertyLambda.Body as MemberExpression; + if (member == null) { - case ExpressionType.Convert: - expr = ((UnaryExpression) expr).Operand; - break; - case ExpressionType.Lambda: - expr = ((LambdaExpression) expr).Body; - break; - case ExpressionType.Call: - var callExpr = (MethodCallExpression) expr; - var method = callExpr.Method; - if (method.DeclaringType != typeof(SqlExtensionsStatics) || method.Name != "Alias" || !(callExpr.Arguments[1] is ConstantExpression aliasExpr)) - Throw(); - expr = callExpr.Arguments[0]; - alias = aliasExpr.Value?.ToString(); - break; - case ExpressionType.MemberAccess: - var memberExpr = (MemberExpression) expr; - if (memberExpr.Expression?.NodeType != ExpressionType.Parameter && memberExpr.Expression?.NodeType != ExpressionType.Convert) - Throw(); - return (memberExpr.Member, alias); - default: - loop = false; - break; + if (propertyLambda.Body.GetType().Name == "UnaryExpression") + { + // The expression might be for some boxing, e.g. representing a value type like HiveId as an object + // in which case the expression will be Convert(x.MyProperty) + if (propertyLambda.Body is UnaryExpression unary) + { + if (unary.Operand is not MemberExpression boxedMember) + { + throw new ArgumentException( + "The type of property could not be inferred, try specifying the type parameters explicitly. This can happen if you have tried to access PropertyInfo where the property's return type is a value type, but the expression is trying to convert it to an object"); + } + + member = boxedMember; + } + } + else + { + throw new ArgumentException( + string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda)); + } } - } - throw new Exception("Configuration for members is only supported for top-level individual members on a type."); + var propInfo = member!.Member as PropertyInfo; + if (propInfo == null) + { + throw new ArgumentException(string.Format( + "Expression '{0}' refers to a field, not a property.", + propertyLambda)); + } + + if (type != propInfo.ReflectedType && + !type.IsSubclassOf(propInfo.ReflectedType!)) + { + throw new ArgumentException(string.Format( + "Expression '{0}' refers to a property that is not from type {1}.", + propertyLambda, + type)); + } + + return propInfo; + }); + + public static (MemberInfo, string?) FindProperty(LambdaExpression lambda) + { + void Throw() + { + throw new ArgumentException( + $"Expression '{lambda}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.", + nameof(lambda)); } - public static IDictionary? GetMethodParams(Expression> fromExpression) + Expression expr = lambda; + var loop = true; + string? alias = null; + while (loop) { - if (fromExpression == null) return null; - var body = fromExpression.Body as MethodCallExpression; - if (body == null) - return new Dictionary(); - - var rVal = new Dictionary(); - var parameters = body.Method.GetParameters().Select(x => x.Name).Where(x => x is not null).ToArray(); - var i = 0; - foreach (var argument in body.Arguments) - { - var lambda = Expression.Lambda(argument, fromExpression.Parameters); - var d = lambda.Compile(); - var value = d.DynamicInvoke(new object[1]); - rVal.Add(parameters[i]!, value); - i++; - } - return rVal; - } - - /// - /// Gets a from an provided it refers to a method call. - /// - /// - /// From expression. - /// The or null if is null or cannot be converted to . - /// - public static MethodInfo? GetMethodInfo(Expression> fromExpression) - { - if (fromExpression == null) return null; - var body = fromExpression.Body as MethodCallExpression; - return body != null ? body.Method : null; - } - - /// - /// Gets the method info. - /// - /// The return type of the method. - /// From expression. - /// - public static MethodInfo? GetMethodInfo(Expression> fromExpression) - { - if (fromExpression == null) return null; - var body = fromExpression.Body as MethodCallExpression; - return body != null ? body.Method : null; - } - - /// - /// Gets the method info. - /// - /// The type of the 1. - /// The type of the 2. - /// From expression. - /// - public static MethodInfo? GetMethodInfo(Expression> fromExpression) - { - if (fromExpression == null) return null; - - MethodCallExpression? me; - switch (fromExpression.Body.NodeType) + switch (expr.NodeType) { case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - var ue = fromExpression.Body as UnaryExpression; - me = ((ue != null) ? ue.Operand : null) as MethodCallExpression; + expr = ((UnaryExpression)expr).Operand; break; + case ExpressionType.Lambda: + expr = ((LambdaExpression)expr).Body; + break; + case ExpressionType.Call: + var callExpr = (MethodCallExpression)expr; + MethodInfo method = callExpr.Method; + if (method.DeclaringType != typeof(SqlExtensionsStatics) || method.Name != "Alias" || + !(callExpr.Arguments[1] is ConstantExpression aliasExpr)) + { + Throw(); + } + + expr = callExpr.Arguments[0]; + alias = aliasExpr.Value?.ToString(); + break; + case ExpressionType.MemberAccess: + var memberExpr = (MemberExpression)expr; + if (memberExpr.Expression?.NodeType != ExpressionType.Parameter && + memberExpr.Expression?.NodeType != ExpressionType.Convert) + { + Throw(); + } + + return (memberExpr.Member, alias); default: - me = fromExpression.Body as MethodCallExpression; + loop = false; break; } - - return me != null ? me.Method : null; } - /// - /// Gets a from an provided it refers to a method call. - /// - /// The expression. - /// The or null if cannot be converted to . - /// - public static MethodInfo? GetMethod(Expression expression) + throw new Exception("Configuration for members is only supported for top-level individual members on a type."); + } + + public static IDictionary? GetMethodParams(Expression> fromExpression) + { + if (fromExpression == null) { - if (expression == null) return null; - return IsMethod(expression) ? (((MethodCallExpression)expression).Method) : null; + return null; } - /// - /// Gets a from an provided it refers to member access. - /// - /// - /// The type of the return. - /// From expression. - /// The or null if cannot be converted to . - /// - public static MemberInfo? GetMemberInfo(Expression> fromExpression) + if (fromExpression.Body is not MethodCallExpression body) { - if (fromExpression == null) return null; - - MemberExpression? me; - switch (fromExpression.Body.NodeType) - { - case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - var ue = fromExpression.Body as UnaryExpression; - me = ((ue != null) ? ue.Operand : null) as MemberExpression; - break; - default: - me = fromExpression.Body as MemberExpression; - break; - } - - return me != null ? me.Member : null; + return new Dictionary(); } - /// - /// Determines whether the MethodInfo is the same based on signature, not based on the equality operator or HashCode. - /// - /// The left. - /// The right. - /// - /// true if [is method signature equal to] [the specified left]; otherwise, false. - /// - /// - /// This is useful for comparing Expression methods that may contain different generic types - /// - public static bool IsMethodSignatureEqualTo(this MethodInfo left, MethodInfo right) + var rVal = new Dictionary(); + var parameters = body.Method.GetParameters().Select(x => x.Name).Where(x => x is not null).ToArray(); + var i = 0; + foreach (Expression argument in body.Arguments) + { + LambdaExpression lambda = Expression.Lambda(argument, fromExpression.Parameters); + Delegate d = lambda.Compile(); + var value = d.DynamicInvoke(new object[1]); + rVal.Add(parameters[i]!, value); + i++; + } + + return rVal; + } + + /// + /// Gets a from an provided it refers to a method call. + /// + /// + /// From expression. + /// + /// The or null if is null or cannot be converted to + /// . + /// + /// + public static MethodInfo? GetMethodInfo(Expression> fromExpression) + { + if (fromExpression == null) + { + return null; + } + + var body = fromExpression.Body as MethodCallExpression; + return body?.Method; + } + + /// + /// Gets the method info. + /// + /// The return type of the method. + /// From expression. + /// + public static MethodInfo? GetMethodInfo(Expression> fromExpression) + { + if (fromExpression == null) + { + return null; + } + + var body = fromExpression.Body as MethodCallExpression; + return body?.Method; + } + + /// + /// Gets the method info. + /// + /// The type of the 1. + /// The type of the 2. + /// From expression. + /// + public static MethodInfo? GetMethodInfo(Expression> fromExpression) + { + if (fromExpression == null) + { + return null; + } + + MethodCallExpression? me; + switch (fromExpression.Body.NodeType) + { + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + var ue = fromExpression.Body as UnaryExpression; + me = ue?.Operand as MethodCallExpression; + break; + default: + me = fromExpression.Body as MethodCallExpression; + break; + } + + return me?.Method; + } + + /// + /// Gets a from an provided it refers to a method call. + /// + /// The expression. + /// + /// The or null if cannot be converted to + /// . + /// + /// + public static MethodInfo? GetMethod(Expression expression) + { + if (expression == null) + { + return null; + } + + return IsMethod(expression) ? ((MethodCallExpression)expression).Method : null; + } + + /// + /// Gets a from an provided it refers to member + /// access. + /// + /// + /// The type of the return. + /// From expression. + /// + /// The or null if cannot be converted to + /// . + /// + /// + public static MemberInfo? GetMemberInfo(Expression> fromExpression) + { + if (fromExpression == null) + { + return null; + } + + MemberExpression? me; + switch (fromExpression.Body.NodeType) + { + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + var ue = fromExpression.Body as UnaryExpression; + me = ue?.Operand as MemberExpression; + break; + default: + me = fromExpression.Body as MemberExpression; + break; + } + + return me?.Member; + } + + /// + /// Determines whether the MethodInfo is the same based on signature, not based on the equality operator or HashCode. + /// + /// The left. + /// The right. + /// + /// true if [is method signature equal to] [the specified left]; otherwise, false. + /// + /// + /// This is useful for comparing Expression methods that may contain different generic types + /// + public static bool IsMethodSignatureEqualTo(this MethodInfo left, MethodInfo right) + { + if (left.Equals(right)) { - if (left.Equals(right)) - return true; - if (left.DeclaringType != right.DeclaringType) - return false; - if (left.Name != right.Name) - return false; - var leftParams = left.GetParameters(); - var rightParams = right.GetParameters(); - if (leftParams.Length != rightParams.Length) - return false; - for (int i = 0; i < leftParams.Length; i++) - { - //if they are delegate parameters, then assume they match as they could be anything - if (typeof(Delegate).IsAssignableFrom(leftParams[i].ParameterType) && typeof(Delegate).IsAssignableFrom(rightParams[i].ParameterType)) - continue; - //if they are not delegates, then compare the types - if (leftParams[i].ParameterType != rightParams[i].ParameterType) - return false; - } - if (left.ReturnType != right.ReturnType) - return false; return true; } - /// - /// Gets a from an provided it refers to member access. - /// - /// The expression. - /// - /// - public static MemberInfo? GetMember(Expression expression) + if (left.DeclaringType != right.DeclaringType) { - if (expression == null) return null; - return IsMember(expression) ? (((MemberExpression)expression).Member) : null; + return false; } - /// - /// Gets a from a - /// - /// From method group. - /// - /// - public static MethodInfo GetStaticMethodInfo(Delegate fromMethodGroup) + if (left.Name != right.Name) { - if (fromMethodGroup == null) throw new ArgumentNullException("fromMethodGroup"); - - - return fromMethodGroup.Method; + return false; } - ///// - ///// Formats an unhandled item for representing the expression as a string. - ///// - ///// - ///// The unhandled item. - ///// - ///// - //public static string FormatUnhandledItem(T unhandledItem) where T : class - //{ - // if (unhandledItem == null) throw new ArgumentNullException("unhandledItem"); - - - // var itemAsExpression = unhandledItem as Expression; - // return itemAsExpression != null - // ? FormattingExpressionTreeVisitor.Format(itemAsExpression) - // : unhandledItem.ToString(); - //} - - /// - /// Determines whether the specified expression is a method. - /// - /// The expression. - /// true if the specified expression is method; otherwise, false. - /// - public static bool IsMethod(Expression expression) + ParameterInfo[] leftParams = left.GetParameters(); + ParameterInfo[] rightParams = right.GetParameters(); + if (leftParams.Length != rightParams.Length) { - return expression is MethodCallExpression; + return false; } - - /// - /// Determines whether the specified expression is a member. - /// - /// The expression. - /// true if the specified expression is member; otherwise, false. - /// - public static bool IsMember(Expression expression) + for (var i = 0; i < leftParams.Length; i++) { - return expression is MemberExpression; + // if they are delegate parameters, then assume they match as they could be anything + if (typeof(Delegate).IsAssignableFrom(leftParams[i].ParameterType) && + typeof(Delegate).IsAssignableFrom(rightParams[i].ParameterType)) + { + continue; + } + + // if they are not delegates, then compare the types + if (leftParams[i].ParameterType != rightParams[i].ParameterType) + { + return false; + } } - /// - /// Determines whether the specified expression is a constant. - /// - /// The expression. - /// true if the specified expression is constant; otherwise, false. - /// - public static bool IsConstant(Expression expression) + if (left.ReturnType != right.ReturnType) { - return expression is ConstantExpression; + return false; } - /// - /// Gets the first value from the supplied arguments of an expression, for those arguments that can be cast to . - /// - /// The arguments. - /// - /// - public static object? GetFirstValueFromArguments(IEnumerable arguments) + return true; + } + + /// + /// Gets a from an provided it refers to member access. + /// + /// The expression. + /// + /// + public static MemberInfo? GetMember(Expression expression) + { + if (expression == null) { - if (arguments == null) return false; - return - arguments.Where(x => x is ConstantExpression).Cast - ().Select(x => x.Value).DefaultIfEmpty(null).FirstOrDefault(); + return null; } + + return IsMember(expression) ? ((MemberExpression)expression).Member : null; + } + + /// + /// Gets a from a + /// + /// From method group. + /// + /// + public static MethodInfo GetStaticMethodInfo(Delegate fromMethodGroup) + { + if (fromMethodGroup == null) + { + throw new ArgumentNullException("fromMethodGroup"); + } + + return fromMethodGroup.Method; + } + + ///// + ///// Formats an unhandled item for representing the expression as a string. + ///// + ///// + ///// The unhandled item. + ///// + ///// + // public static string FormatUnhandledItem(T unhandledItem) where T : class + // { + // if (unhandledItem == null) throw new ArgumentNullException("unhandledItem"); + + // var itemAsExpression = unhandledItem as Expression; + // return itemAsExpression != null + // ? FormattingExpressionTreeVisitor.Format(itemAsExpression) + // : unhandledItem.ToString(); + // } + + /// + /// Determines whether the specified expression is a method. + /// + /// The expression. + /// true if the specified expression is method; otherwise, false. + /// + public static bool IsMethod(Expression expression) => expression is MethodCallExpression; + + /// + /// Determines whether the specified expression is a member. + /// + /// The expression. + /// true if the specified expression is member; otherwise, false. + /// + public static bool IsMember(Expression expression) => expression is MemberExpression; + + /// + /// Determines whether the specified expression is a constant. + /// + /// The expression. + /// true if the specified expression is constant; otherwise, false. + /// + public static bool IsConstant(Expression expression) => expression is ConstantExpression; + + /// + /// Gets the first value from the supplied arguments of an expression, for those arguments that can be cast to + /// . + /// + /// The arguments. + /// + /// + public static object? GetFirstValueFromArguments(IEnumerable arguments) + { + if (arguments == null) + { + return false; + } + + return + arguments.Where(x => x is ConstantExpression).Cast + ().Select(x => x.Value).DefaultIfEmpty(null).FirstOrDefault(); } } diff --git a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs index aea0f847ab..45ae9ceafe 100644 --- a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs +++ b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs @@ -1,106 +1,107 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; using System.Reflection; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class AssemblyExtensions { - public static class AssemblyExtensions + private static string _rootDir = string.Empty; + + /// + /// Utility method that returns the path to the root of the application, by getting the path to where the assembly + /// where this + /// method is included is present, then traversing until it's past the /bin directory. Ie. this makes it work + /// even if the assembly is in a /bin/debug or /bin/release folder + /// + /// + public static string GetRootDirectorySafe(this Assembly executingAssembly) { - private static string _rootDir = ""; - - /// - /// Utility method that returns the path to the root of the application, by getting the path to where the assembly where this - /// method is included is present, then traversing until it's past the /bin directory. Ie. this makes it work - /// even if the assembly is in a /bin/debug or /bin/release folder - /// - /// - public static string GetRootDirectorySafe(this Assembly executingAssembly) + if (string.IsNullOrEmpty(_rootDir) == false) { - if (string.IsNullOrEmpty(_rootDir) == false) - { - return _rootDir; - } - var codeBase = executingAssembly.Location; - var uri = new Uri(codeBase); - var path = uri.LocalPath; - var baseDirectory = Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(baseDirectory)) - throw new Exception("No root directory could be resolved. Please ensure that your Umbraco solution is correctly configured."); - - _rootDir = baseDirectory.Contains("bin") - ? baseDirectory.Substring(0, baseDirectory.LastIndexOf("bin", StringComparison.OrdinalIgnoreCase) - 1) - : baseDirectory; - return _rootDir; } - /// - /// Returns the file used to load the assembly - /// - /// - /// - public static FileInfo GetAssemblyFile(this Assembly assembly) + var codeBase = executingAssembly.Location; + var uri = new Uri(codeBase); + var path = uri.LocalPath; + var baseDirectory = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(baseDirectory)) + { + throw new Exception( + "No root directory could be resolved. Please ensure that your Umbraco solution is correctly configured."); + } + + _rootDir = baseDirectory.Contains("bin") + ? baseDirectory[..(baseDirectory.LastIndexOf("bin", StringComparison.OrdinalIgnoreCase) - 1)] + : baseDirectory; + + return _rootDir; + } + + /// + /// Returns the file used to load the assembly + /// + /// + /// + public static FileInfo GetAssemblyFile(this Assembly assembly) + { + var codeBase = assembly.Location; + var uri = new Uri(codeBase); + var path = uri.LocalPath; + return new FileInfo(path); + } + + /// + /// Returns true if the assembly is the App_Code assembly + /// + /// + /// + public static bool IsAppCodeAssembly(this Assembly assembly) + { + if (assembly.FullName!.StartsWith("App_Code")) + { + try + { + Assembly.Load("App_Code"); + return true; + } + catch (FileNotFoundException) + { + // this will occur if it cannot load the assembly + return false; + } + } + + return false; + } + + /// + /// Returns true if the assembly is the compiled global asax. + /// + /// + /// + public static bool IsGlobalAsaxAssembly(this Assembly assembly) => + + // only way I can figure out how to test is by the name + assembly.FullName!.StartsWith("App_global.asax"); + + /// + /// Returns the file used to load the assembly + /// + /// + /// + public static FileInfo? GetAssemblyFile(this AssemblyName assemblyName) + { + var codeBase = assemblyName.CodeBase; + if (!string.IsNullOrEmpty(codeBase)) { - var codeBase = assembly.Location; var uri = new Uri(codeBase); var path = uri.LocalPath; return new FileInfo(path); } - /// - /// Returns true if the assembly is the App_Code assembly - /// - /// - /// - public static bool IsAppCodeAssembly(this Assembly assembly) - { - if (assembly.FullName!.StartsWith("App_Code")) - { - try - { - Assembly.Load("App_Code"); - return true; - } - catch (FileNotFoundException) - { - //this will occur if it cannot load the assembly - return false; - } - } - return false; - } - - /// - /// Returns true if the assembly is the compiled global asax. - /// - /// - /// - public static bool IsGlobalAsaxAssembly(this Assembly assembly) - { - //only way I can figure out how to test is by the name - return assembly.FullName!.StartsWith("App_global.asax"); - } - - /// - /// Returns the file used to load the assembly - /// - /// - /// - public static FileInfo? GetAssemblyFile(this AssemblyName assemblyName) - { - var codeBase = assemblyName.CodeBase; - if (!string.IsNullOrEmpty(codeBase)) - { - var uri = new Uri(codeBase); - var path = uri.LocalPath; - return new FileInfo(path); - } - - return null; - } - + return null; } } diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index e3d6f7f4fd..a604b3e017 100644 --- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs @@ -1,378 +1,395 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Security.Claims; using System.Security.Principal; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ClaimsIdentityExtensions { - public static class ClaimsIdentityExtensions + /// + /// Returns the required claim types for a back office identity + /// + /// + /// This does not include the role claim type or allowed apps type since that is a collection and in theory could be + /// empty + /// + public static IEnumerable RequiredBackOfficeClaimTypes => new[] { - public static T? GetUserId(this IIdentity identity) + ClaimTypes.NameIdentifier, // id + ClaimTypes.Name, // username + ClaimTypes.GivenName, + + // Constants.Security.StartContentNodeIdClaimType, These seem to be able to be null... + // Constants.Security.StartMediaNodeIdClaimType, + ClaimTypes.Locality, Constants.Security.SecurityStampClaimType, + }; + + public static T? GetUserId(this IIdentity identity) + { + var strId = identity.GetUserId(); + Attempt converted = strId.TryConvertTo(); + return converted.Result ?? default; + } + + /// + /// Returns the user id from the of either the claim type + /// or "sub" + /// + /// + /// + /// The string value of the user id if found otherwise null + /// + public static string? GetUserId(this IIdentity identity) + { + if (identity == null) { - var strId = identity.GetUserId(); - var converted = strId.TryConvertTo(); - return converted.Result ?? default; + throw new ArgumentNullException(nameof(identity)); } - /// - /// Returns the user id from the of either the claim type or "sub" - /// - /// - /// - /// The string value of the user id if found otherwise null - /// - public static string? GetUserId(this IIdentity identity) + string? userId = null; + if (identity is ClaimsIdentity claimsIdentity) { - if (identity == null) throw new ArgumentNullException(nameof(identity)); - - string? userId = null; - if (identity is ClaimsIdentity claimsIdentity) - { - userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier) - ?? claimsIdentity.FindFirstValue("sub"); - } - - return userId; + userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier) + ?? claimsIdentity.FindFirstValue("sub"); } - /// - /// Returns the user name from the of either the claim type or "preferred_username" - /// - /// - /// - /// The string value of the user name if found otherwise null - /// - public static string? GetUserName(this IIdentity identity) + return userId; + } + + /// + /// Returns the user name from the of either the claim type or + /// "preferred_username" + /// + /// + /// + /// The string value of the user name if found otherwise null + /// + public static string? GetUserName(this IIdentity identity) + { + if (identity == null) { - if (identity == null) throw new ArgumentNullException(nameof(identity)); - - string? username = null; - if (identity is ClaimsIdentity claimsIdentity) - { - username = claimsIdentity.FindFirstValue(ClaimTypes.Name) - ?? claimsIdentity.FindFirstValue("preferred_username"); - } - - return username; + throw new ArgumentNullException(nameof(identity)); } - public static string? GetEmail(this IIdentity identity) + string? username = null; + if (identity is ClaimsIdentity claimsIdentity) { - if (identity == null) throw new ArgumentNullException(nameof(identity)); - - string? email = null; - if (identity is ClaimsIdentity claimsIdentity) - { - email = claimsIdentity.FindFirstValue(ClaimTypes.Email); - } - - return email; + username = claimsIdentity.FindFirstValue(ClaimTypes.Name) + ?? claimsIdentity.FindFirstValue("preferred_username"); } - /// - /// Returns the first claim value found in the for the given claimType - /// - /// - /// - /// - /// The string value of the claim if found otherwise null - /// - public static string? FindFirstValue(this ClaimsIdentity identity, string claimType) - { - if (identity == null) throw new ArgumentNullException(nameof(identity)); + return username; + } - return identity.FindFirst(claimType)?.Value; + public static string? GetEmail(this IIdentity identity) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); } - /// - /// Returns the required claim types for a back office identity - /// - /// - /// This does not include the role claim type or allowed apps type since that is a collection and in theory could be empty - /// - public static IEnumerable RequiredBackOfficeClaimTypes => new[] + string? email = null; + if (identity is ClaimsIdentity claimsIdentity) { - ClaimTypes.NameIdentifier, //id - ClaimTypes.Name, //username - ClaimTypes.GivenName, - // Constants.Security.StartContentNodeIdClaimType, These seem to be able to be null... - // Constants.Security.StartMediaNodeIdClaimType, - ClaimTypes.Locality, - Constants.Security.SecurityStampClaimType - }; + email = claimsIdentity.FindFirstValue(ClaimTypes.Email); + } - /// - /// Verify that a ClaimsIdentity has all the required claim types - /// - /// - /// Verified identity wrapped in a ClaimsIdentity with BackOfficeAuthentication type - /// True if ClaimsIdentity - public static bool VerifyBackOfficeIdentity(this ClaimsIdentity identity, [MaybeNullWhen(false)] out ClaimsIdentity verifiedIdentity) + return email; + } + + /// + /// Returns the first claim value found in the for the given claimType + /// + /// + /// + /// + /// The string value of the claim if found otherwise null + /// + public static string? FindFirstValue(this ClaimsIdentity identity, string claimType) + { + if (identity == null) { - if (identity is null) + throw new ArgumentNullException(nameof(identity)); + } + + return identity.FindFirst(claimType)?.Value; + } + + /// + /// Verify that a ClaimsIdentity has all the required claim types + /// + /// + /// Verified identity wrapped in a ClaimsIdentity with BackOfficeAuthentication type + /// True if ClaimsIdentity + public static bool VerifyBackOfficeIdentity( + this ClaimsIdentity identity, + [MaybeNullWhen(false)] out ClaimsIdentity verifiedIdentity) + { + if (identity is null) + { + verifiedIdentity = null; + return false; + } + + // Validate that all required claims exist + foreach (var claimType in RequiredBackOfficeClaimTypes) + { + if (identity.HasClaim(x => x.Type == claimType) == false || + identity.HasClaim(x => x.Type == claimType && x.Value.IsNullOrWhiteSpace())) { verifiedIdentity = null; return false; } - - // Validate that all required claims exist - foreach (var claimType in RequiredBackOfficeClaimTypes) - { - if (identity.HasClaim(x => x.Type == claimType) == false || - identity.HasClaim(x => x.Type == claimType && x.Value.IsNullOrWhiteSpace())) - { - verifiedIdentity = null; - return false; - } - } - - verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType ? identity : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType); - return true; } - /// - /// Add the required claims to be a BackOffice ClaimsIdentity - /// - /// this - /// The users Id - /// Username - /// Real name - /// Start content nodes - /// Start media nodes - /// The locality of the user - /// Security stamp - /// Allowed apps - /// Roles - public static void AddRequiredClaims(this ClaimsIdentity identity, string userId, string username, - string realName, IEnumerable? startContentNodes, IEnumerable? startMediaNodes, string culture, - string securityStamp, IEnumerable allowedApps, IEnumerable roles) + verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType + ? identity + : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType); + return true; + } + + /// + /// Add the required claims to be a BackOffice ClaimsIdentity + /// + /// this + /// The users Id + /// Username + /// Real name + /// Start content nodes + /// Start media nodes + /// The locality of the user + /// Security stamp + /// Allowed apps + /// Roles + public static void AddRequiredClaims(this ClaimsIdentity identity, string userId, string username, string realName, IEnumerable? startContentNodes, IEnumerable? startMediaNodes, string culture, string securityStamp, IEnumerable allowedApps, IEnumerable roles) + { + // This is the id that 'identity' uses to check for the user id + if (identity.HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) { - //This is the id that 'identity' uses to check for the user id - if (identity.HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) + identity.AddClaim(new Claim( + ClaimTypes.NameIdentifier, + userId, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + if (identity.HasClaim(x => x.Type == ClaimTypes.Name) == false) + { + identity.AddClaim(new Claim( + ClaimTypes.Name, + username, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + if (identity.HasClaim(x => x.Type == ClaimTypes.GivenName) == false) + { + identity.AddClaim(new Claim( + ClaimTypes.GivenName, + realName, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && + startContentNodes != null) + { + foreach (var startContentNode in startContentNodes) { identity.AddClaim(new Claim( - ClaimTypes.NameIdentifier, - userId, - ClaimValueTypes.String, + Constants.Security.StartContentNodeIdClaimType, + startContentNode.ToInvariantString(), + ClaimValueTypes.Integer32, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } - - if (identity.HasClaim(x => x.Type == ClaimTypes.Name) == false) - { - identity.AddClaim(new Claim( - ClaimTypes.Name, - username, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - - if (identity.HasClaim(x => x.Type == ClaimTypes.GivenName) == false) - { - identity.AddClaim(new Claim( - ClaimTypes.GivenName, - realName, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - - if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && - startContentNodes != null) - { - foreach (var startContentNode in startContentNodes) - { - identity.AddClaim(new Claim( - Constants.Security.StartContentNodeIdClaimType, - startContentNode.ToInvariantString(), - ClaimValueTypes.Integer32, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - } - - if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && - startMediaNodes != null) - { - foreach (var startMediaNode in startMediaNodes) - { - identity.AddClaim(new Claim( - Constants.Security.StartMediaNodeIdClaimType, - startMediaNode.ToInvariantString(), - ClaimValueTypes.Integer32, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - } - - if (identity.HasClaim(x => x.Type == ClaimTypes.Locality) == false) - { - identity.AddClaim(new Claim( - ClaimTypes.Locality, - culture, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - - // The security stamp claim is also required - if (identity.HasClaim(x => x.Type == Constants.Security.SecurityStampClaimType) == false) - { - identity.AddClaim(new Claim( - Constants.Security.SecurityStampClaimType, - securityStamp, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - - // Add each app as a separate claim - if (identity.HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && - allowedApps != null) - { - foreach (var application in allowedApps) - { - identity.AddClaim(new Claim( - Constants.Security.AllowedApplicationsClaimType, - application, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - } - - // Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might - // not be made with that factory if it was created with a different ticket so perform the check - if (identity.HasClaim(x => x.Type == ClaimsIdentity.DefaultRoleClaimType) == false && roles != null) - { - // Manually add them - foreach (var roleName in roles) - { - identity.AddClaim(new Claim( - identity.RoleClaimType, - roleName, - ClaimValueTypes.String, - Constants.Security.BackOfficeAuthenticationType, - Constants.Security.BackOfficeAuthenticationType, - identity)); - } - } } - /// - /// Get the start content nodes from a ClaimsIdentity - /// - /// - /// Array of start content nodes - public static int[] GetStartContentNodes(this ClaimsIdentity identity) => - identity.FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType) - .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : default) - .Where(x => x != default).ToArray(); - - /// - /// Get the start media nodes from a ClaimsIdentity - /// - /// - /// Array of start media nodes - public static int[] GetStartMediaNodes(this ClaimsIdentity identity) => - identity.FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) - .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : default) - .Where(x => x != default).ToArray(); - - /// - /// Get the allowed applications from a ClaimsIdentity - /// - /// - /// - public static string[] GetAllowedApplications(this ClaimsIdentity identity) => identity - .FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray(); - - /// - /// Get the user ID from a ClaimsIdentity - /// - /// - /// User ID as integer - public static int? GetId(this ClaimsIdentity identity) + if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && + startMediaNodes != null) { - var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier); - if (firstValue is not null) + foreach (var startMediaNode in startMediaNodes) { - return int.Parse(firstValue, CultureInfo.InvariantCulture); + identity.AddClaim(new Claim( + Constants.Security.StartMediaNodeIdClaimType, + startMediaNode.ToInvariantString(), + ClaimValueTypes.Integer32, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); } - - return null; } - /// - /// Get the real name belonging to the user from a ClaimsIdentity - /// - /// - /// Real name of the user - public static string? GetRealName(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.GivenName); - - /// - /// Get the username of the user from a ClaimsIdentity - /// - /// - /// Username of the user - public static string? GetUsername(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Name); - - /// - /// Get the culture string from a ClaimsIdentity - /// - /// - /// Culture string - public static string? GetCultureString(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Locality); - - /// - /// Get the security stamp from a ClaimsIdentity - /// - /// - /// Security stamp - public static string? GetSecurityStamp(this ClaimsIdentity identity) => identity.FindFirstValue(Constants.Security.SecurityStampClaimType); - - /// - /// Get the roles assigned to a user from a ClaimsIdentity - /// - /// - /// Array of roles - public static string[] GetRoles(this ClaimsIdentity identity) => identity - .FindAll(x => x.Type == ClaimsIdentity.DefaultRoleClaimType).Select(role => role.Value).ToArray(); - - - /// - /// Adds or updates and existing claim. - /// - public static void AddOrUpdateClaim(this ClaimsIdentity identity, Claim? claim) + if (identity.HasClaim(x => x.Type == ClaimTypes.Locality) == false) { - if (identity == null) + identity.AddClaim(new Claim( + ClaimTypes.Locality, + culture, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + // The security stamp claim is also required + if (identity.HasClaim(x => x.Type == Constants.Security.SecurityStampClaimType) == false) + { + identity.AddClaim(new Claim( + Constants.Security.SecurityStampClaimType, + securityStamp, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); + } + + // Add each app as a separate claim + if (identity.HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null) + { + foreach (var application in allowedApps) { - throw new ArgumentNullException(nameof(identity)); + identity.AddClaim(new Claim( + Constants.Security.AllowedApplicationsClaimType, + application, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); } + } - if (claim is not null) + // Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might + // not be made with that factory if it was created with a different ticket so perform the check + if (identity.HasClaim(x => x.Type == ClaimsIdentity.DefaultRoleClaimType) == false && roles != null) + { + // Manually add them + foreach (var roleName in roles) { - Claim? existingClaim = identity.Claims.FirstOrDefault(x => x.Type == claim.Type); - identity.TryRemoveClaim(existingClaim); - - identity.AddClaim(claim); + identity.AddClaim(new Claim( + identity.RoleClaimType, + roleName, + ClaimValueTypes.String, + Constants.Security.BackOfficeAuthenticationType, + Constants.Security.BackOfficeAuthenticationType, + identity)); } } } + + /// + /// Get the start content nodes from a ClaimsIdentity + /// + /// + /// Array of start content nodes + public static int[] GetStartContentNodes(this ClaimsIdentity identity) => + identity.FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType) + .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? i + : default) + .Where(x => x != default).ToArray(); + + /// + /// Get the start media nodes from a ClaimsIdentity + /// + /// + /// Array of start media nodes + public static int[] GetStartMediaNodes(this ClaimsIdentity identity) => + identity.FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) + .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? i + : default) + .Where(x => x != default).ToArray(); + + /// + /// Get the allowed applications from a ClaimsIdentity + /// + /// + /// + public static string[] GetAllowedApplications(this ClaimsIdentity identity) => identity + .FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray(); + + /// + /// Get the user ID from a ClaimsIdentity + /// + /// + /// User ID as integer + public static int? GetId(this ClaimsIdentity identity) + { + var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier); + if (firstValue is not null) + { + return int.Parse(firstValue, CultureInfo.InvariantCulture); + } + + return null; + } + + /// + /// Get the real name belonging to the user from a ClaimsIdentity + /// + /// + /// Real name of the user + public static string? GetRealName(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.GivenName); + + /// + /// Get the username of the user from a ClaimsIdentity + /// + /// + /// Username of the user + public static string? GetUsername(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Name); + + /// + /// Get the culture string from a ClaimsIdentity + /// + /// + /// Culture string + public static string? GetCultureString(this ClaimsIdentity identity) => + identity.FindFirstValue(ClaimTypes.Locality); + + /// + /// Get the security stamp from a ClaimsIdentity + /// + /// + /// Security stamp + public static string? GetSecurityStamp(this ClaimsIdentity identity) => + identity.FindFirstValue(Constants.Security.SecurityStampClaimType); + + /// + /// Get the roles assigned to a user from a ClaimsIdentity + /// + /// + /// Array of roles + public static string[] GetRoles(this ClaimsIdentity identity) => identity + .FindAll(x => x.Type == ClaimsIdentity.DefaultRoleClaimType).Select(role => role.Value).ToArray(); + + /// + /// Adds or updates and existing claim. + /// + public static void AddOrUpdateClaim(this ClaimsIdentity identity, Claim? claim) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (claim is not null) + { + Claim? existingClaim = identity.Claims.FirstOrDefault(x => x.Type == claim.Type); + identity.TryRemoveClaim(existingClaim); + + identity.AddClaim(claim); + } + } } diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index 0bd1e36d9e..df0e58d878 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -1,11 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Xml.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; @@ -15,395 +11,399 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ContentExtensions { - public static class ContentExtensions + /// + /// Returns the path to a media item stored in a property if the property editor is + /// + /// + /// + /// + /// + /// + /// + /// True if the file path can be resolved and the property is + public static bool TryGetMediaPath( + this IContentBase content, + string propertyTypeAlias, + MediaUrlGeneratorCollection mediaUrlGenerators, + out string? mediaFilePath, + string? culture = null, + string? segment = null) { - /// - /// Returns the path to a media item stored in a property if the property editor is - /// - /// - /// - /// - /// - /// - /// - /// True if the file path can be resolved and the property is - public static bool TryGetMediaPath( - this IContentBase content, - string propertyTypeAlias, - MediaUrlGeneratorCollection mediaUrlGenerators, - out string? mediaFilePath, - string? culture = null, - string? segment = null) + if (!content.Properties.TryGetValue(propertyTypeAlias, out IProperty? property)) { - if (!content.Properties.TryGetValue(propertyTypeAlias, out IProperty? property)) - { - mediaFilePath = null; - return false; - } - - if (!mediaUrlGenerators.TryGetMediaPath( - property?.PropertyType?.PropertyEditorAlias, - property?.GetValue(culture, segment), - out mediaFilePath)) - { - return false; - } - - return true; - } - - public static bool IsAnyUserPropertyDirty(this IContentBase entity) - { - return entity.Properties.Any(x => x.IsDirty()); - } - - public static bool WasAnyUserPropertyDirty(this IContentBase entity) - { - return entity.Properties.Any(x => x.WasDirty()); - } - - - public 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.IsPropertyDirty(nameof(entity.Path)) - && entity.IsPropertyDirty(nameof(entity.Level)) - && entity.IsPropertyDirty(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 - /// - /// - /// - /// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it. - /// - /// - public static void SanitizeEntityPropertiesForXmlStorage(this IContentBase entity) - { - entity.Name = entity.Name?.ToValidXmlString(); - foreach (var property in entity.Properties) - { - foreach (var propertyValue in property.Values) - { - if (propertyValue.EditedValue is string editString) - propertyValue.EditedValue = editString.ToValidXmlString(); - if (propertyValue.PublishedValue is string publishedString) - propertyValue.PublishedValue = publishedString.ToValidXmlString(); - } - } - } - - /// - /// Checks if the IContentBase has children - /// - /// - /// - /// - /// - /// This is a bit of a hack because we need to type check! - /// - internal static bool? HasChildren(IContentBase content, ServiceContext services) - { - if (content is IContent) - { - return services.ContentService?.HasChildren(content.Id); - } - if (content is IMedia) - { - return services.MediaService?.HasChildren(content.Id); - } + mediaFilePath = null; return false; } - - /// - /// Returns all properties based on the editorAlias - /// - /// - /// - /// - public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias) - => content.Properties.Where(x => x.PropertyType?.PropertyEditorAlias == editorAlias); - - - #region IContent - - /// - /// Gets the current status of the Content - /// - public static ContentStatus GetStatus(this IContent content, ContentScheduleCollection contentSchedule, string? culture = null) + if (!mediaUrlGenerators.TryGetMediaPath( + property?.PropertyType?.PropertyEditorAlias, + property?.GetValue(culture, segment), + out mediaFilePath)) { - if (content.Trashed) - return ContentStatus.Trashed; - - if (!content.ContentType.VariesByCulture()) - culture = string.Empty; - else if (culture.IsNullOrWhiteSpace()) - throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); - - var expires = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Expire); - if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) - return ContentStatus.Expired; - - var release = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Release); - if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) - return ContentStatus.AwaitingRelease; - - if (content.Published) - return ContentStatus.Published; - - return ContentStatus.Unpublished; + return false; } - /// - /// Gets a collection containing the ids of all ancestors. - /// - /// to retrieve ancestors for - /// An Enumerable list of integer ids - public static IEnumerable? GetAncestorIds(this IContent content) => - content.Path?.Split(Constants.CharArrays.Comma) - .Where(x => x != Constants.System.RootString && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(s => - int.Parse(s, CultureInfo.InvariantCulture)); + return true; + } - #endregion + public static bool IsAnyUserPropertyDirty(this IContentBase entity) => entity.Properties.Any(x => x.IsDirty()); + public static bool WasAnyUserPropertyDirty(this IContentBase entity) => entity.Properties.Any(x => x.WasDirty()); - /// - /// Gets the for the Creator of this content item. - /// - public static IProfile? GetCreatorProfile(this IContentBase content, IUserService userService) + public 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.IsPropertyDirty(nameof(entity.Path)) + && entity.IsPropertyDirty(nameof(entity.Level)) + && entity.IsPropertyDirty(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 + /// + /// + /// + /// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it. + /// + /// + public static void SanitizeEntityPropertiesForXmlStorage(this IContentBase entity) + { + entity.Name = entity.Name?.ToValidXmlString(); + foreach (IProperty property in entity.Properties) { - return userService.GetProfileById(content.CreatorId); - } - /// - /// Gets the for the Writer of this content. - /// - public static IProfile? GetWriterProfile(this IContent content, IUserService userService) - { - return userService.GetProfileById(content.WriterId); - } - - /// - /// Gets the for the Writer of this content. - /// - public static IProfile? GetWriterProfile(this IMedia content, IUserService userService) - { - return userService.GetProfileById(content.WriterId); - } - - - #region User/Profile methods - - /// - /// Gets the for the Creator of this media item. - /// - public static IProfile? GetCreatorProfile(this IMedia media, IUserService userService) - { - return userService.GetProfileById(media.CreatorId); - } - - - #endregion - - - /// - /// Returns properties that do not belong to a group - /// - /// - /// - public static IEnumerable GetNonGroupedProperties(this IContentBase content) - { - return content.Properties - .Where(x => x.PropertyType?.PropertyGroupId == null) - .OrderBy(x => x.PropertyType?.SortOrder); - } - - /// - /// Returns the Property object for the given property group - /// - /// - /// - /// - public static IEnumerable GetPropertiesForGroup(this IContentBase content, PropertyGroup propertyGroup) - { - //get the properties for the current tab - return content.Properties - .Where(property => propertyGroup.PropertyTypes is not null && propertyGroup.PropertyTypes - .Select(propertyType => propertyType.Id) - .Contains(property.PropertyTypeId)); - } - - - #region SetValue for setting file contents - - /// - /// Sets the posted file value of a property. - /// - public static void SetValue( - this IContentBase content, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - string propertyTypeAlias, - string filename, - Stream filestream, - string? culture = null, - string? segment = null) - { - if (filename == null || filestream == null) - return; - - filename = shortStringHelper.CleanStringForSafeFileName(filename); - if (string.IsNullOrWhiteSpace(filename)) - return; - filename = filename.ToLower(); - - SetUploadFile(content, mediaFileManager, mediaUrlGenerators, contentTypeBaseServiceProvider, propertyTypeAlias, filename, filestream, culture, segment); - } - - private static void SetUploadFile( - this IContentBase content, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - string propertyTypeAlias, - string filename, - Stream filestream, - string? culture = null, - string? segment = null) - { - var property = GetProperty(content, contentTypeBaseServiceProvider, propertyTypeAlias); - - // Fixes https://github.com/umbraco/Umbraco-CMS/issues/3937 - Assigning a new file to an - // existing IMedia with extension SetValue causes exception 'Illegal characters in path' - string? oldpath = null; - - if (content.TryGetMediaPath(property.Alias, mediaUrlGenerators, out string? mediaFilePath, culture, segment)) + foreach (IPropertyValue propertyValue in property.Values) { - oldpath = mediaFileManager.FileSystem.GetRelativePath(mediaFilePath!); + if (propertyValue.EditedValue is string editString) + { + propertyValue.EditedValue = editString.ToValidXmlString(); + } + + if (propertyValue.PublishedValue is string publishedString) + { + propertyValue.PublishedValue = publishedString.ToValidXmlString(); + } } + } + } - var filepath = mediaFileManager.StoreFile(content, property.PropertyType, filename, filestream, oldpath); + /// + /// Returns all properties based on the editorAlias + /// + /// + /// + /// + public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias) + => content.Properties.Where(x => x.PropertyType?.PropertyEditorAlias == editorAlias); - // NOTE: Here we are just setting the value to a string which means that any file based editor - // will need to handle the raw string value and save it to it's correct (i.e. JSON) - // format. I'm unsure how this works today with image cropper but it does (maybe events?) - property.SetValue(mediaFileManager.FileSystem.GetUrl(filepath), culture, segment); + /// + /// Checks if the IContentBase has children + /// + /// + /// + /// + /// + /// This is a bit of a hack because we need to type check! + /// + internal static bool? HasChildren(IContentBase content, ServiceContext services) + { + if (content is IContent) + { + return services.ContentService?.HasChildren(content.Id); } - // gets or creates a property for a content item. - private static IProperty GetProperty(IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias) + if (content is IMedia) { - var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - if (property != null) - return property; + return services.MediaService?.HasChildren(content.Id); + } - var contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); - var propertyType = contentType?.CompositionPropertyTypes - .FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); - if (propertyType == null) - throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); + return false; + } - property = new Property(propertyType); - content.Properties.Add(property); + /// + /// Gets the for the Creator of this content item. + /// + public static IProfile? GetCreatorProfile(this IContentBase content, IUserService userService) => + userService.GetProfileById(content.CreatorId); + + /// + /// Gets the for the Writer of this content. + /// + public static IProfile? GetWriterProfile(this IContent content, IUserService userService) => + userService.GetProfileById(content.WriterId); + + /// + /// Gets the for the Writer of this content. + /// + public static IProfile? GetWriterProfile(this IMedia content, IUserService userService) => + userService.GetProfileById(content.WriterId); + + #region User/Profile methods + + /// + /// Gets the for the Creator of this media item. + /// + public static IProfile? GetCreatorProfile(this IMedia media, IUserService userService) => + userService.GetProfileById(media.CreatorId); + + #endregion + + /// + /// Returns properties that do not belong to a group + /// + /// + /// + public static IEnumerable GetNonGroupedProperties(this IContentBase content) => + content.Properties + .Where(x => x.PropertyType?.PropertyGroupId == null) + .OrderBy(x => x.PropertyType?.SortOrder); + + /// + /// Returns the Property object for the given property group + /// + /// + /// + /// + public static IEnumerable + GetPropertiesForGroup(this IContentBase content, PropertyGroup propertyGroup) => + + // get the properties for the current tab + content.Properties + .Where(property => propertyGroup.PropertyTypes is not null && propertyGroup.PropertyTypes + .Select(propertyType => propertyType.Id) + .Contains(property.PropertyTypeId)); + + #region Dirty + + public static IEnumerable GetDirtyUserProperties(this IContentBase entity) => + entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias); + + #endregion + + /// + /// Creates the full xml representation for the object and all of it's descendants + /// + /// to generate xml for + /// + /// Xml representation of the passed in + public static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer) => + serializer.Serialize(content, false, true); + + /// + /// Creates the xml representation for the object + /// + /// to generate xml for + /// + /// Xml representation of the passed in + public static XElement ToXml(this IContent content, IEntityXmlSerializer serializer) => + serializer.Serialize(content, false); + + /// + /// Creates the xml representation for the object + /// + /// to generate xml for + /// + /// Xml representation of the passed in + public static XElement ToXml(this IMedia media, IEntityXmlSerializer serializer) => serializer.Serialize(media); + + /// + /// Creates the xml representation for the object + /// + /// to generate xml for + /// + /// Xml representation of the passed in + public static XElement ToXml(this IMember member, IEntityXmlSerializer serializer) => serializer.Serialize(member); + + #region IContent + + /// + /// Gets the current status of the Content + /// + public static ContentStatus GetStatus(this IContent content, ContentScheduleCollection contentSchedule, string? culture = null) + { + if (content.Trashed) + { + return ContentStatus.Trashed; + } + + if (!content.ContentType.VariesByCulture()) + { + culture = string.Empty; + } + else if (culture.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty"); + } + + IEnumerable expires = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Expire); + if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) + { + return ContentStatus.Expired; + } + + IEnumerable release = contentSchedule.GetSchedule(culture!, ContentScheduleAction.Release); + if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) + { + return ContentStatus.AwaitingRelease; + } + + if (content.Published) + { + return ContentStatus.Published; + } + + return ContentStatus.Unpublished; + } + + /// + /// Gets a collection containing the ids of all ancestors. + /// + /// to retrieve ancestors for + /// An Enumerable list of integer ids + public static IEnumerable? GetAncestorIds(this IContent content) => + content.Path?.Split(Constants.CharArrays.Comma) + .Where(x => x != Constants.System.RootString && x != content.Id.ToString(CultureInfo.InvariantCulture)) + .Select(s => + int.Parse(s, CultureInfo.InvariantCulture)); + + #endregion + + #region SetValue for setting file contents + + /// + /// Sets the posted file value of a property. + /// + public static void SetValue( + this IContentBase content, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias, + string filename, + Stream filestream, + string? culture = null, + string? segment = null) + { + if (filename == null || filestream == null) + { + return; + } + + filename = shortStringHelper.CleanStringForSafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) + { + return; + } + + filename = filename.ToLower(); + + SetUploadFile(content, mediaFileManager, mediaUrlGenerators, contentTypeBaseServiceProvider, propertyTypeAlias, filename, filestream, culture, segment); + } + + /// + /// Stores a file. + /// + /// A content item. + /// The property alias. + /// The name of the file. + /// A stream containing the file data. + /// The original file path, if any. + /// The path to the file, relative to the media filesystem. + /// + /// + /// Does NOT set the property value, so one should probably store the file and then do + /// something alike: property.Value = MediaHelper.FileSystem.GetUrl(filepath). + /// + /// + /// The original file path is used, in the old media file path scheme, to try and reuse + /// the "folder number" that was assigned to the previous file referenced by the property, + /// if any. + /// + /// + public static string StoreFile( + this IContentBase content, + MediaFileManager mediaFileManager, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias, + string filename, + Stream filestream, + string filepath) + { + IContentTypeComposition? contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); + IPropertyType? propertyType = contentType? + .CompositionPropertyTypes.FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); + if (propertyType == null) + { + throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); + } + + return mediaFileManager.StoreFile(content, propertyType, filename, filestream, filepath); + } + + private static void SetUploadFile( + this IContentBase content, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias, + string filename, + Stream filestream, + string? culture = null, + string? segment = null) + { + IProperty property = GetProperty(content, contentTypeBaseServiceProvider, propertyTypeAlias); + + // Fixes https://github.com/umbraco/Umbraco-CMS/issues/3937 - Assigning a new file to an + // existing IMedia with extension SetValue causes exception 'Illegal characters in path' + string? oldpath = null; + + if (content.TryGetMediaPath(property.Alias, mediaUrlGenerators, out var mediaFilePath, culture, segment)) + { + oldpath = mediaFileManager.FileSystem.GetRelativePath(mediaFilePath!); + } + + var filepath = mediaFileManager.StoreFile(content, property.PropertyType, filename, filestream, oldpath); + + // NOTE: Here we are just setting the value to a string which means that any file based editor + // will need to handle the raw string value and save it to it's correct (i.e. JSON) + // format. I'm unsure how this works today with image cropper but it does (maybe events?) + property.SetValue(mediaFileManager.FileSystem.GetUrl(filepath), culture, segment); + } + + // gets or creates a property for a content item. + private static IProperty GetProperty( + IContentBase content, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias) + { + IProperty? property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (property != null) + { return property; } - /// - /// Stores a file. - /// - /// A content item. - /// The property alias. - /// The name of the file. - /// A stream containing the file data. - /// The original file path, if any. - /// The path to the file, relative to the media filesystem. - /// - /// Does NOT set the property value, so one should probably store the file and then do - /// something alike: property.Value = MediaHelper.FileSystem.GetUrl(filepath). - /// The original file path is used, in the old media file path scheme, to try and reuse - /// the "folder number" that was assigned to the previous file referenced by the property, - /// if any. - /// - public static string StoreFile(this IContentBase content, MediaFileManager mediaFileManager, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, Stream filestream, string filepath) + IContentTypeComposition? contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); + IPropertyType? propertyType = contentType?.CompositionPropertyTypes + .FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); + if (propertyType == null) { - var contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); - var propertyType = contentType? - .CompositionPropertyTypes.FirstOrDefault(x => x.Alias?.InvariantEquals(propertyTypeAlias) ?? false); - if (propertyType == null) - throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); - return mediaFileManager.StoreFile(content, propertyType, filename, filestream, filepath); + throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); } - #endregion - - - #region Dirty - - public static IEnumerable GetDirtyUserProperties(this IContentBase entity) - { - return entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias); - } - - - - #endregion - - - /// - /// Creates the full xml representation for the object and all of it's descendants - /// - /// to generate xml for - /// - /// Xml representation of the passed in - public static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer) - { - return serializer.Serialize(content, false, true); - } - - /// - /// Creates the xml representation for the object - /// - /// to generate xml for - /// - /// Xml representation of the passed in - public static XElement ToXml(this IContent content, IEntityXmlSerializer serializer) - { - return serializer.Serialize(content, false, false); - } - - - /// - /// Creates the xml representation for the object - /// - /// to generate xml for - /// - /// Xml representation of the passed in - public static XElement ToXml(this IMedia media, IEntityXmlSerializer serializer) - { - return serializer.Serialize(media); - } - - /// - /// Creates the xml representation for the object - /// - /// to generate xml for - /// - /// Xml representation of the passed in - public static XElement ToXml(this IMember member, IEntityXmlSerializer serializer) - { - return serializer.Serialize(member); - } + property = new Property(propertyType); + content.Properties.Add(property); + return property; } + + #endregion } diff --git a/src/Umbraco.Core/Extensions/ContentVariationExtensions.cs b/src/Umbraco.Core/Extensions/ContentVariationExtensions.cs index 4469683acb..2654bf0e1e 100644 --- a/src/Umbraco.Core/Extensions/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentVariationExtensions.cs @@ -1,346 +1,381 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for content variations. +/// +public static class ContentVariationExtensions { /// - /// Provides extension methods for content variations. + /// Determines whether the content type is invariant. /// - public static class ContentVariationExtensions + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// + public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the content type is invariant. + /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// + public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the content type is invariant. + /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// + public static bool VariesByNothing(this IPublishedContentType contentType) => + contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the property type is invariant. + /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// + public static bool VariesByNothing(this IPropertyType propertyType) => propertyType.Variations.VariesByNothing(); + + /// + /// Determines whether the property type is invariant. + /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// + public static bool VariesByNothing(this IPublishedPropertyType propertyType) => + propertyType.Variations.VariesByNothing(); + + /// + /// Determines whether a variation is invariant. + /// + /// The variation. + /// + /// A value indicating whether the variation is invariant. + /// + public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; + + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this IPublishedContentType contentType) => + contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by culture. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// + public static bool VariesByCulture(this IPropertyType propertyType) => propertyType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by culture. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// + public static bool VariesByCulture(this IPublishedPropertyType propertyType) => + propertyType.Variations.VariesByCulture(); + + /// + /// Determines whether a variation varies by culture. + /// + /// The variation. + /// + /// A value indicating whether the variation varies by culture. + /// + public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; + + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this IPublishedContentType contentType) => + contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// + public static bool VariesBySegment(this IPropertyType propertyType) => propertyType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// + public static bool VariesBySegment(this IPublishedPropertyType propertyType) => + propertyType.Variations.VariesBySegment(); + + /// + /// Determines whether a variation varies by segment. + /// + /// The variation. + /// + /// A value indicating whether the variation varies by segment. + /// + public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => + contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => + contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => + contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IPropertyType propertyType) => + propertyType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => + propertyType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether a variation varies by culture and segment. + /// + /// The variation. + /// + /// A value indicating whether the variation varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ContentVariation variation) => + (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; + + /// + /// Sets or removes the content type variation depending on the specified value. + /// + /// The content type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// This method does not support setting the variation to nothing. + /// + public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => + contentType.Variations = contentType.Variations.SetFlag(variation, value); + + /// + /// Sets or removes the property type variation depending on the specified value. + /// + /// The property type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// This method does not support setting the variation to nothing. + /// + public static void SetVariesBy(this IPropertyType propertyType, ContentVariation variation, bool value = true) => + propertyType.Variations = propertyType.Variations.SetFlag(variation, value); + + /// + /// Returns the variations with the variation set or removed depending on the specified value. + /// + /// The existing variations. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// The variations with the variation set or removed. + /// + /// + /// This method does not support setting the variation to nothing. + /// + public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true) => + value + ? variations | variation // Set flag using bitwise logical OR + : variations & + ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + + /// + /// Validates that a combination of culture and segment is valid for the variation. + /// + /// The variation. + /// The culture. + /// The segment. + /// A value indicating whether to perform exact validation. + /// A value indicating whether to support wildcards. + /// + /// A value indicating whether to throw a when the + /// combination is invalid. + /// + /// + /// true if the combination is valid; otherwise false. + /// + /// + /// Occurs when the combination is invalid, and + /// is true. + /// + /// + /// + /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is + /// Culture, then + /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: + /// if the variation is + /// Culture, an invariant combination is ok. + /// + /// + /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one + /// content type. + /// + /// Both and can be "*" to indicate "all of them". + /// + public static bool ValidateVariation(this ContentVariation variation, string? culture, string? segment, bool exact, bool wildcards, bool throwIfInvalid) { - /// - /// Determines whether the content type is invariant. - /// - /// The content type. - /// - /// A value indicating whether the content type is invariant. - /// - public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing(); + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); - /// - /// Determines whether the content type is invariant. - /// - /// The content type. - /// - /// A value indicating whether the content type is invariant. - /// - public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); - - /// - /// Determines whether the content type is invariant. - /// - /// The content type. - /// - /// A value indicating whether the content type is invariant. - /// - public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing(); - - /// - /// Determines whether the property type is invariant. - /// - /// The property type. - /// - /// A value indicating whether the property type is invariant. - /// - public static bool VariesByNothing(this IPropertyType propertyType) => propertyType.Variations.VariesByNothing(); - - /// - /// Determines whether the property type is invariant. - /// - /// The property type. - /// - /// A value indicating whether the property type is invariant. - /// - public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); - - /// - /// Determines whether a variation is invariant. - /// - /// The variation. - /// - /// A value indicating whether the variation is invariant. - /// - public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; - - /// - /// Determines whether the content type varies by culture. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture. - /// - public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the content type varies by culture. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture. - /// - public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the content type varies by culture. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture. - /// - public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the property type varies by culture. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture. - /// - public static bool VariesByCulture(this IPropertyType propertyType) => propertyType.Variations.VariesByCulture(); - - /// - /// Determines whether the property type varies by culture. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture. - /// - public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); - - /// - /// Determines whether a variation varies by culture. - /// - /// The variation. - /// - /// A value indicating whether the variation varies by culture. - /// - public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the property type varies by segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by segment. - /// - public static bool VariesBySegment(this IPropertyType propertyType) => propertyType.Variations.VariesBySegment(); - - /// - /// Determines whether the property type varies by segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by segment. - /// - public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); - - /// - /// Determines whether a variation varies by segment. - /// - /// The variation. - /// - /// A value indicating whether the variation varies by segment. - /// - public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the property type varies by culture and segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the property type varies by culture and segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether a variation varies by culture and segment. - /// - /// The variation. - /// - /// A value indicating whether the variation varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; - - /// - /// Sets or removes the content type variation depending on the specified value. - /// - /// The content type. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// This method does not support setting the variation to nothing. - /// - public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetFlag(variation, value); - - /// - /// Sets or removes the property type variation depending on the specified value. - /// - /// The property type. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// This method does not support setting the variation to nothing. - /// - public static void SetVariesBy(this IPropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetFlag(variation, value); - - /// - /// Returns the variations with the variation set or removed depending on the specified value. - /// - /// The existing variations. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// The variations with the variation set or removed. - /// - /// - /// This method does not support setting the variation to nothing. - /// - public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true) + // if wildcards are disabled, do not allow "*" + if (!wildcards && (culture == "*" || segment == "*")) { - return value - ? variations | variation // Set flag using bitwise logical OR - : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + if (throwIfInvalid) + { + throw new NotSupportedException("Variation wildcards are not supported."); + } + + return false; } - /// - /// Validates that a combination of culture and segment is valid for the variation. - /// - /// The variation. - /// The culture. - /// The segment. - /// A value indicating whether to perform exact validation. - /// A value indicating whether to support wildcards. - /// A value indicating whether to throw a when the combination is invalid. - /// - /// true if the combination is valid; otherwise false. - /// - /// Occurs when the combination is invalid, and is true. - /// - /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then - /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is - /// Culture, an invariant combination is ok. - /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type. - /// Both and can be "*" to indicate "all of them". - /// - public static bool ValidateVariation(this ContentVariation variation, string? culture, string? segment, bool exact, bool wildcards, bool throwIfInvalid) + if (variation.VariesByCulture()) { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - // if wildcards are disabled, do not allow "*" - if (!wildcards && (culture == "*" || segment == "*")) + // varies by culture + // in exact mode, the culture cannot be null + if (exact && culture == null) { if (throwIfInvalid) - throw new NotSupportedException($"Variation wildcards are not supported."); - return false; - } - - if (variation.VariesByCulture()) - { - // varies by culture - // in exact mode, the culture cannot be null - if (exact && culture == null) { - if (throwIfInvalid) - throw new NotSupportedException($"Culture may not be null because culture variation is enabled."); - - return false; + throw new NotSupportedException("Culture may not be null because culture variation is enabled."); } - } - else - { - // does not vary by culture - // the culture cannot have a value - // unless wildcards and it's "*" - if (culture != null && !(wildcards && culture == "*")) - { - if (throwIfInvalid) - throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled."); - - return false; - } - } - - // if it does not vary by segment - // the segment cannot have a value - // segment may always be null, even when the ContentVariation.Segment flag is set for this variation, - // therefore the exact parameter is not used in segment validation. - if (!variation.VariesBySegment() && segment != null && !(wildcards && segment == "*")) - { - if (throwIfInvalid) - throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled."); return false; } - - return true; } + else + { + // does not vary by culture + // the culture cannot have a value + // unless wildcards and it's "*" + if (culture != null && !(wildcards && culture == "*")) + { + if (throwIfInvalid) + { + throw new NotSupportedException( + $"Culture \"{culture}\" is invalid because culture variation is disabled."); + } + + return false; + } + } + + // if it does not vary by segment + // the segment cannot have a value + // segment may always be null, even when the ContentVariation.Segment flag is set for this variation, + // therefore the exact parameter is not used in segment validation. + if (!variation.VariesBySegment() && segment != null && !(wildcards && segment == "*")) + { + if (throwIfInvalid) + { + throw new NotSupportedException( + $"Segment \"{segment}\" is invalid because segment variation is disabled."); + } + + return false; + } + + return true; } } diff --git a/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs b/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs index 8dfec45c7e..1af0b8c47a 100644 --- a/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs +++ b/src/Umbraco.Core/Extensions/CoreCacheHelperExtensions.cs @@ -1,24 +1,21 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Cache; -namespace Umbraco.Extensions -{ - /// - /// Extension methods for the cache helper - /// - public static class CoreCacheHelperExtensions - { - public const string PartialViewCacheKey = "Umbraco.Web.PartialViewCacheKey"; +namespace Umbraco.Extensions; - /// - /// Clears the cache for partial views - /// - /// - public static void ClearPartialViewCache(this AppCaches appCaches) - { - appCaches.RuntimeCache.ClearByKey(PartialViewCacheKey); - } - } +/// +/// Extension methods for the cache helper +/// +public static class CoreCacheHelperExtensions +{ + public const string PartialViewCacheKey = "Umbraco.Web.PartialViewCacheKey"; + + /// + /// Clears the cache for partial views + /// + /// + public static void ClearPartialViewCache(this AppCaches appCaches) => + appCaches.RuntimeCache.ClearByKey(PartialViewCacheKey); } diff --git a/src/Umbraco.Core/Extensions/DataTableExtensions.cs b/src/Umbraco.Core/Extensions/DataTableExtensions.cs index 4594709407..10fa51deaf 100644 --- a/src/Umbraco.Core/Extensions/DataTableExtensions.cs +++ b/src/Umbraco.Core/Extensions/DataTableExtensions.cs @@ -1,114 +1,112 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Static and extension methods for the DataTable object +/// +public static class DataTableExtensions { /// - /// Static and extension methods for the DataTable object + /// Creates a DataTable with the specified alias and columns and uses a callback to populate the headers. /// - public static class DataTableExtensions + /// + /// + /// + /// + /// + /// This has been migrated from the Node class and uses proper locking now. It is now used by the Node class and the + /// DynamicPublishedContent extensions for legacy reasons. + /// + public static DataTable GenerateDataTable( + string tableAlias, + Func>> getHeaders, + Func>, IEnumerable>>>> + rowData) { - /// - /// Creates a DataTable with the specified alias and columns and uses a callback to populate the headers. - /// - /// - /// - /// - /// - /// - /// This has been migrated from the Node class and uses proper locking now. It is now used by the Node class and the - /// DynamicPublishedContent extensions for legacy reasons. - /// - public static DataTable GenerateDataTable( - string tableAlias, - Func>> getHeaders, - Func>, IEnumerable>>>> rowData) + var dt = new DataTable(tableAlias); + + // get all row data + Tuple>, IEnumerable>>[] tableData = + rowData().ToArray(); + + // get all headers + IDictionary propertyHeaders = GetPropertyHeaders(tableAlias, getHeaders); + foreach (KeyValuePair h in propertyHeaders) { - var dt = new DataTable(tableAlias); - - //get all row data - var tableData = rowData().ToArray(); - - //get all headers - var propertyHeaders = GetPropertyHeaders(tableAlias, getHeaders); - foreach(var h in propertyHeaders) - { - dt.Columns.Add(new DataColumn(h.Value)); - } - - //add row data - foreach(var r in tableData) - { - dt.PopulateRow( - propertyHeaders, - r.Item1, - r.Item2); - } - - return dt; + dt.Columns.Add(new DataColumn(h.Value)); } - /// - /// Helper method to return this ugly object - /// - /// - /// - /// This is for legacy code, I didn't want to go creating custom classes for these - /// - public static List>, IEnumerable>>> CreateTableData() + // add row data + foreach (Tuple>, IEnumerable>> r in + tableData) { - return new List>, IEnumerable>>>(); + dt.PopulateRow( + propertyHeaders, + r.Item1, + r.Item2); } - /// - /// Helper method to deal with these ugly objects - /// - /// - /// - /// - /// - /// This is for legacy code, I didn't want to go creating custom classes for these - /// - public static void AddRowData( - List>, IEnumerable>>> rowData, - IEnumerable> standardVals, - IEnumerable> userVals) + return dt; + } + + /// + /// Helper method to return this ugly object + /// + /// + /// + /// This is for legacy code, I didn't want to go creating custom classes for these + /// + public static List>, IEnumerable>>> + CreateTableData() => + new List>, IEnumerable>>>(); + + /// + /// Helper method to deal with these ugly objects + /// + /// + /// + /// + /// + /// This is for legacy code, I didn't want to go creating custom classes for these + /// + public static void AddRowData( + List>, IEnumerable>>> rowData, + IEnumerable> standardVals, + IEnumerable> userVals) => + rowData.Add(new Tuple>, IEnumerable>>( + standardVals, + userVals)); + + private static IDictionary GetPropertyHeaders( + string alias, + Func>> getHeaders) + { + IEnumerable> headers = getHeaders(alias); + var def = headers.ToDictionary(pt => pt.Key, pt => pt.Value); + return def; + } + + private static void PopulateRow( + this DataTable dt, + IDictionary aliasesToNames, + IEnumerable> standardVals, + IEnumerable> userPropertyVals) + { + DataRow dr = dt.NewRow(); + foreach (KeyValuePair r in standardVals) { - rowData.Add(new System.Tuple>, IEnumerable>>( - standardVals, - userVals - )); + dr[r.Key] = r.Value; } - private static IDictionary GetPropertyHeaders(string alias, Func>> getHeaders) + foreach (KeyValuePair p in userPropertyVals.Where(p => p.Value != null)) { - var headers = getHeaders(alias); - var def = headers.ToDictionary(pt => pt.Key, pt => pt.Value); - return def; - } - - private static void PopulateRow( - this DataTable dt, - IDictionary aliasesToNames, - IEnumerable> standardVals, - IEnumerable> userPropertyVals) - { - var dr = dt.NewRow(); - foreach (var r in standardVals) - { - dr[r.Key] = r.Value; - } - foreach (var p in userPropertyVals.Where(p => p.Value != null)) - { - dr[aliasesToNames[p.Key]] = p.Value; - } - dt.Rows.Add(dr); + dr[aliasesToNames[p.Key]] = p.Value; } + dt.Rows.Add(dr); } } diff --git a/src/Umbraco.Core/Extensions/DateTimeExtensions.cs b/src/Umbraco.Core/Extensions/DateTimeExtensions.cs index e500cf86b0..35c9f600e5 100644 --- a/src/Umbraco.Core/Extensions/DateTimeExtensions.cs +++ b/src/Umbraco.Core/Extensions/DateTimeExtensions.cs @@ -1,46 +1,57 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Globalization; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class DateTimeExtensions { - public static class DateTimeExtensions + public enum DateTruncate { - /// - /// Returns the DateTime as an ISO formatted string that is globally expectable - /// - /// - /// - public static string ToIsoString(this DateTime dt) + Year, + Month, + Day, + Hour, + Minute, + Second, + } + + /// + /// Returns the DateTime as an ISO formatted string that is globally expectable + /// + /// + /// + public static string ToIsoString(this DateTime dt) => + dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + + public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) + { + if (truncateTo == DateTruncate.Year) { - return dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + return new DateTime(dt.Year, 1, 1); } - public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) + if (truncateTo == DateTruncate.Month) { - if (truncateTo == DateTruncate.Year) - return new DateTime(dt.Year, 1, 1); - if (truncateTo == DateTruncate.Month) - return new DateTime(dt.Year, dt.Month, 1); - if (truncateTo == DateTruncate.Day) - return new DateTime(dt.Year, dt.Month, dt.Day); - if (truncateTo == DateTruncate.Hour) - return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0); - if (truncateTo == DateTruncate.Minute) - return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0); - return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second); + return new DateTime(dt.Year, dt.Month, 1); } - public enum DateTruncate + if (truncateTo == DateTruncate.Day) { - Year, - Month, - Day, - Hour, - Minute, - Second + return new DateTime(dt.Year, dt.Month, dt.Day); } + + if (truncateTo == DateTruncate.Hour) + { + return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0); + } + + if (truncateTo == DateTruncate.Minute) + { + return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0); + } + + return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second); } } diff --git a/src/Umbraco.Core/Extensions/DecimalExtensions.cs b/src/Umbraco.Core/Extensions/DecimalExtensions.cs index fa62805841..6e70544d0e 100644 --- a/src/Umbraco.Core/Extensions/DecimalExtensions.cs +++ b/src/Umbraco.Core/Extensions/DecimalExtensions.cs @@ -1,26 +1,25 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for System.Decimal. +/// +/// +/// See System.Decimal on MSDN and also +/// http://stackoverflow.com/questions/4298719/parse-decimal-and-filter-extra-0-on-the-right/4298787#4298787. +/// +public static class DecimalExtensions { /// - /// Provides extension methods for System.Decimal. + /// Gets the normalized value. /// - /// See System.Decimal on MSDN and also - /// http://stackoverflow.com/questions/4298719/parse-decimal-and-filter-extra-0-on-the-right/4298787#4298787. + /// The value to normalize. + /// The normalized value. + /// + /// Normalizing changes the scaling factor and removes trailing zeros, + /// so 1.2500m comes out as 1.25m. /// - public static class DecimalExtensions - { - /// - /// Gets the normalized value. - /// - /// The value to normalize. - /// The normalized value. - /// Normalizing changes the scaling factor and removes trailing zeros, - /// so 1.2500m comes out as 1.25m. - public static decimal Normalize(this decimal value) - { - return value / 1.000000000000000000000000000000000m; - } - } + public static decimal Normalize(this decimal value) => value / 1.000000000000000000000000000000000m; } diff --git a/src/Umbraco.Core/Extensions/DelegateExtensions.cs b/src/Umbraco.Core/Extensions/DelegateExtensions.cs index 4cbcdd5d6a..621ef46438 100644 --- a/src/Umbraco.Core/Extensions/DelegateExtensions.cs +++ b/src/Umbraco.Core/Extensions/DelegateExtensions.cs @@ -1,48 +1,57 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Diagnostics; -using System.Threading; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class DelegateExtensions { - public static class DelegateExtensions + public static Attempt RetryUntilSuccessOrTimeout(this Func> task, TimeSpan timeout, TimeSpan pause) { - public static Attempt RetryUntilSuccessOrTimeout(this Func> task, TimeSpan timeout, TimeSpan pause) + if (pause.TotalMilliseconds < 0) { - if (pause.TotalMilliseconds < 0) - { - throw new ArgumentException("pause must be >= 0 milliseconds"); - } - var stopwatch = Stopwatch.StartNew(); - do - { - var result = task(); - if (result.Success) { return result; } - Thread.Sleep((int)pause.TotalMilliseconds); - } - while (stopwatch.Elapsed < timeout); - return Attempt.Fail(); + throw new ArgumentException("pause must be >= 0 milliseconds"); } - public static Attempt RetryUntilSuccessOrMaxAttempts(this Func> task, int totalAttempts, TimeSpan pause) + var stopwatch = Stopwatch.StartNew(); + do { - if (pause.TotalMilliseconds < 0) + Attempt result = task(); + if (result.Success) { - throw new ArgumentException("pause must be >= 0 milliseconds"); + return result; } - int attempts = 0; - do - { - attempts++; - var result = task(attempts); - if (result.Success) { return result; } - Thread.Sleep((int)pause.TotalMilliseconds); - } - while (attempts < totalAttempts); - return Attempt.Fail(); + + Thread.Sleep((int)pause.TotalMilliseconds); } + while (stopwatch.Elapsed < timeout); + + return Attempt.Fail(); + } + + public static Attempt RetryUntilSuccessOrMaxAttempts(this Func> task, int totalAttempts, TimeSpan pause) + { + if (pause.TotalMilliseconds < 0) + { + throw new ArgumentException("pause must be >= 0 milliseconds"); + } + + var attempts = 0; + do + { + attempts++; + Attempt result = task(attempts); + if (result.Success) + { + return result; + } + + Thread.Sleep((int)pause.TotalMilliseconds); + } + while (attempts < totalAttempts); + + return Attempt.Fail(); } } diff --git a/src/Umbraco.Core/Extensions/DictionaryExtensions.cs b/src/Umbraco.Core/Extensions/DictionaryExtensions.cs index 906f12282e..3bbd3bdcb9 100644 --- a/src/Umbraco.Core/Extensions/DictionaryExtensions.cs +++ b/src/Umbraco.Core/Extensions/DictionaryExtensions.cs @@ -1,306 +1,320 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using System.Net; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core; -namespace Umbraco.Extensions -{ - /// - /// Extension methods for Dictionary & ConcurrentDictionary - /// - public static class DictionaryExtensions - { +namespace Umbraco.Extensions; - /// - /// Method to Get a value by the key. If the key doesn't exist it will create a new TVal object for the key and return it. - /// - /// - /// - /// - /// - /// - public static TVal GetOrCreate(this IDictionary dict, TKey key) - where TVal : class, new() +/// +/// Extension methods for Dictionary & ConcurrentDictionary +/// +public static class DictionaryExtensions +{ + /// + /// Method to Get a value by the key. If the key doesn't exist it will create a new TVal object for the key and return + /// it. + /// + /// + /// + /// + /// + /// + public static TVal GetOrCreate(this IDictionary dict, TKey key) + where TVal : class, new() + { + if (dict.ContainsKey(key) == false) { - if (dict.ContainsKey(key) == false) - { - dict.Add(key, new TVal()); - } - return dict[key]; + dict.Add(key, new TVal()); } - /// - /// Updates an item with the specified key with the specified value - /// - /// - /// - /// - /// - /// - /// - /// - /// Taken from: http://stackoverflow.com/questions/12240219/is-there-a-way-to-use-concurrentdictionary-tryupdate-with-a-lambda-expression - /// - /// If there is an item in the dictionary with the key, it will keep trying to update it until it can - /// - public static bool TryUpdate(this ConcurrentDictionary dict, TKey key, Func updateFactory) - where TKey : notnull + return dict[key]; + } + + /// + /// Updates an item with the specified key with the specified value + /// + /// + /// + /// + /// + /// + /// + /// + /// Taken from: + /// http://stackoverflow.com/questions/12240219/is-there-a-way-to-use-concurrentdictionary-tryupdate-with-a-lambda-expression + /// If there is an item in the dictionary with the key, it will keep trying to update it until it can + /// + public static bool TryUpdate(this ConcurrentDictionary dict, TKey key, Func updateFactory) + where TKey : notnull + { + while (dict.TryGetValue(key, out TValue? curValue)) { - TValue? curValue; - while (dict.TryGetValue(key, out curValue)) + if (dict.TryUpdate(key, updateFactory(curValue), curValue)) { - if (dict.TryUpdate(key, updateFactory(curValue), curValue)) - return true; - //if we're looping either the key was removed by another thread, or another thread - //changed the value, so we start again. + return true; } + + // if we're looping either the key was removed by another thread, or another thread + // changed the value, so we start again. + } + + return false; + } + + /// + /// Updates an item with the specified key with the specified value + /// + /// + /// + /// + /// + /// + /// + /// + /// Taken from: + /// http://stackoverflow.com/questions/12240219/is-there-a-way-to-use-concurrentdictionary-tryupdate-with-a-lambda-expression + /// WARNING: If the value changes after we've retrieved it, then the item will not be updated + /// + public static bool TryUpdateOptimisitic(this ConcurrentDictionary dict, TKey key, Func updateFactory) + where TKey : notnull + { + if (!dict.TryGetValue(key, out TValue? curValue)) + { return false; } - /// - /// Updates an item with the specified key with the specified value - /// - /// - /// - /// - /// - /// - /// - /// - /// Taken from: http://stackoverflow.com/questions/12240219/is-there-a-way-to-use-concurrentdictionary-tryupdate-with-a-lambda-expression - /// - /// WARNING: If the value changes after we've retrieved it, then the item will not be updated - /// - public static bool TryUpdateOptimisitic(this ConcurrentDictionary dict, TKey key, Func updateFactory) - where TKey : notnull + dict.TryUpdate(key, updateFactory(curValue), curValue); + return true; // note we return true whether we succeed or not, see explanation below. + } + + /// + /// Converts a dictionary to another type by only using direct casting + /// + /// + /// + /// + /// + public static IDictionary ConvertTo(this IDictionary d) + where TKeyOut : notnull + { + var result = new Dictionary(); + foreach (DictionaryEntry v in d) { - TValue? curValue; - if (!dict.TryGetValue(key, out curValue)) - return false; - dict.TryUpdate(key, updateFactory(curValue), curValue); - return true;//note we return true whether we succeed or not, see explanation below. + result.Add((TKeyOut)v.Key, (TValOut)v.Value!); } - /// - /// Converts a dictionary to another type by only using direct casting - /// - /// - /// - /// - /// - public static IDictionary ConvertTo(this IDictionary d) - where TKeyOut : notnull + return result; + } + + /// + /// Converts a dictionary to another type using the specified converters + /// + /// + /// + /// + /// + /// + /// + public static IDictionary ConvertTo( + this IDictionary d, + Func keyConverter, + Func valConverter) + where TKeyOut : notnull + { + var result = new Dictionary(); + foreach (DictionaryEntry v in d) { - var result = new Dictionary(); - foreach (DictionaryEntry v in d) - { - result.Add((TKeyOut)v.Key, (TValOut)v.Value!); - } - return result; + result.Add(keyConverter(v.Key), valConverter(v.Value!)); } - /// - /// Converts a dictionary to another type using the specified converters - /// - /// - /// - /// - /// - /// - /// - public static IDictionary ConvertTo(this IDictionary d, Func keyConverter, Func valConverter) - where TKeyOut : notnull + return result; + } + + /// + /// Converts a dictionary to a NameValueCollection + /// + /// + /// + public static NameValueCollection ToNameValueCollection(this IDictionary d) + { + var n = new NameValueCollection(); + foreach (KeyValuePair i in d) { - var result = new Dictionary(); - foreach (DictionaryEntry v in d) - { - result.Add(keyConverter(v.Key), valConverter(v.Value!)); - } - return result; + n.Add(i.Key, i.Value); } - /// - /// Converts a dictionary to a NameValueCollection - /// - /// - /// - public static NameValueCollection ToNameValueCollection(this IDictionary d) + return n; + } + + /// + /// Merges all key/values from the sources dictionaries into the destination dictionary + /// + /// + /// + /// + /// The source dictionary to merge other dictionaries into + /// + /// By default all values will be retained in the destination if the same keys exist in the sources but + /// this can changed if overwrite = true, then any key/value found in any of the sources will overwritten in the + /// destination. Note that + /// it will just use the last found key/value if this is true. + /// + /// The other dictionaries to merge values from + public static void MergeLeft(this T destination, IEnumerable> sources, bool overwrite = false) + where T : IDictionary + { + foreach (KeyValuePair p in sources.SelectMany(src => src) + .Where(p => overwrite || destination.ContainsKey(p.Key) == false)) { - var n = new NameValueCollection(); - foreach (var i in d) - { - n.Add(i.Key, i.Value); - } - return n; - } - - - /// - /// Merges all key/values from the sources dictionaries into the destination dictionary - /// - /// - /// - /// - /// The source dictionary to merge other dictionaries into - /// - /// By default all values will be retained in the destination if the same keys exist in the sources but - /// this can changed if overwrite = true, then any key/value found in any of the sources will overwritten in the destination. Note that - /// it will just use the last found key/value if this is true. - /// - /// The other dictionaries to merge values from - public static void MergeLeft(this T destination, IEnumerable> sources, bool overwrite = false) - where T : IDictionary - { - foreach (var p in sources.SelectMany(src => src).Where(p => overwrite || destination.ContainsKey(p.Key) == false)) - { - destination[p.Key] = p.Value; - } - } - - /// - /// Merges all key/values from the sources dictionaries into the destination dictionary - /// - /// - /// - /// - /// The source dictionary to merge other dictionaries into - /// - /// By default all values will be retained in the destination if the same keys exist in the sources but - /// this can changed if overwrite = true, then any key/value found in any of the sources will overwritten in the destination. Note that - /// it will just use the last found key/value if this is true. - /// - /// The other dictionary to merge values from - public static void MergeLeft(this T destination, IDictionary source, bool overwrite = false) - where T : IDictionary - { - destination.MergeLeft(new[] {source}, overwrite); - } - - /// - /// Returns the value of the key value based on the key, if the key is not found, a null value is returned - /// - /// The type of the key. - /// The type of the val. - /// The d. - /// The key. - /// The default value. - /// - public static TVal? GetValue(this IDictionary d, TKey key, TVal? defaultValue = default(TVal)) - { - if (d.ContainsKey(key)) - { - return d[key]; - } - return defaultValue; - } - - /// - /// Returns the value of the key value based on the key as it's string value, if the key is not found, then an empty string is returned - /// - /// - /// - /// - public static string? GetValueAsString(this IDictionary d, TKey key) - => d.ContainsKey(key) ? d[key]!.ToString() : string.Empty; - - /// - /// Returns the value of the key value based on the key as it's string value, if the key is not found or is an empty string, then the provided default value is returned - /// - /// - /// - /// - /// - public static string? GetValueAsString(this IDictionary d, TKey key, string defaultValue) - { - if (d.ContainsKey(key)) - { - var value = d[key]!.ToString(); - if (value != string.Empty) - return value; - } - - return defaultValue; - } - - /// contains key ignore case. - /// The dictionary. - /// The key. - /// Value Type - /// The contains key ignore case. - public static bool ContainsKeyIgnoreCase(this IDictionary dictionary, string key) - { - return dictionary.Keys.InvariantContains(key); - } - - /// - /// Converts a dictionary object to a query string representation such as: - /// firstname=shannon&lastname=deminick - /// - /// - /// - public static string ToQueryString(this IDictionary d) - { - if (!d.Any()) return ""; - - var builder = new StringBuilder(); - foreach (var i in d) - { - builder.Append(String.Format("{0}={1}&", WebUtility.UrlEncode(i.Key), i.Value == null ? string.Empty : WebUtility.UrlEncode(i.Value.ToString()))); - } - return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); - } - - /// The get entry ignore case. - /// The dictionary. - /// The key. - /// The type - /// The entry - public static TValue? GetValueIgnoreCase(this IDictionary dictionary, string key) - => dictionary!.GetValueIgnoreCase(key, default(TValue)); - - /// The get entry ignore case. - /// The dictionary. - /// The key. - /// The default value. - /// The type - /// The entry - public static TValue GetValueIgnoreCase(this IDictionary dictionary, string? key, TValue - defaultValue) - { - key = dictionary.Keys.FirstOrDefault(i => i.InvariantEquals(key)); - - return key.IsNullOrWhiteSpace() == false - ? dictionary[key!] - : defaultValue; - } - - public static async Task> ToDictionaryAsync( - this IEnumerable enumerable, - Func syncKeySelector, - Func> asyncValueSelector) - where TKey : notnull - { - Dictionary dictionary = new Dictionary(); - - foreach (var item in enumerable) - { - var key = syncKeySelector(item); - - var value = await asyncValueSelector(item); - - dictionary.Add(key,value); - } - - return dictionary; + destination[p.Key] = p.Value; } } + + /// + /// Merges all key/values from the sources dictionaries into the destination dictionary + /// + /// + /// + /// + /// The source dictionary to merge other dictionaries into + /// + /// By default all values will be retained in the destination if the same keys exist in the sources but + /// this can changed if overwrite = true, then any key/value found in any of the sources will overwritten in the + /// destination. Note that + /// it will just use the last found key/value if this is true. + /// + /// The other dictionary to merge values from + public static void MergeLeft(this T destination, IDictionary source, bool overwrite = false) + where T : IDictionary => + destination.MergeLeft(new[] { source }, overwrite); + + /// + /// Returns the value of the key value based on the key, if the key is not found, a null value is returned + /// + /// The type of the key. + /// The type of the val. + /// The d. + /// The key. + /// The default value. + /// + public static TVal? GetValue(this IDictionary d, TKey key, TVal? defaultValue = default) + { + if (d.ContainsKey(key)) + { + return d[key]; + } + + return defaultValue; + } + + /// + /// Returns the value of the key value based on the key as it's string value, if the key is not found, then an empty + /// string is returned + /// + /// + /// + /// + public static string? GetValueAsString(this IDictionary d, TKey key) + => d.ContainsKey(key) ? d[key]!.ToString() : string.Empty; + + /// + /// Returns the value of the key value based on the key as it's string value, if the key is not found or is an empty + /// string, then the provided default value is returned + /// + /// + /// + /// + /// + public static string? GetValueAsString(this IDictionary d, TKey key, string defaultValue) + { + if (d.ContainsKey(key)) + { + var value = d[key]!.ToString(); + if (value != string.Empty) + { + return value; + } + } + + return defaultValue; + } + + /// contains key ignore case. + /// The dictionary. + /// The key. + /// Value Type + /// The contains key ignore case. + public static bool ContainsKeyIgnoreCase(this IDictionary dictionary, string key) => + dictionary.Keys.InvariantContains(key); + + /// + /// Converts a dictionary object to a query string representation such as: + /// firstname=shannon&lastname=deminick + /// + /// + /// + public static string ToQueryString(this IDictionary d) + { + if (!d.Any()) + { + return string.Empty; + } + + var builder = new StringBuilder(); + foreach (KeyValuePair i in d) + { + builder.Append(string.Format("{0}={1}&", WebUtility.UrlEncode(i.Key), i.Value == null ? string.Empty : WebUtility.UrlEncode(i.Value.ToString()))); + } + + return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand); + } + + /// The get entry ignore case. + /// The dictionary. + /// The key. + /// The type + /// The entry + public static TValue? GetValueIgnoreCase(this IDictionary dictionary, string key) + => dictionary!.GetValueIgnoreCase(key, default); + + /// The get entry ignore case. + /// The dictionary. + /// The key. + /// The default value. + /// The type + /// The entry + public static TValue GetValueIgnoreCase(this IDictionary dictionary, string? key, TValue + defaultValue) + { + key = dictionary.Keys.FirstOrDefault(i => i.InvariantEquals(key)); + + return key.IsNullOrWhiteSpace() == false + ? dictionary[key!] + : defaultValue; + } + + public static async Task> ToDictionaryAsync( + this IEnumerable enumerable, + Func syncKeySelector, + Func> asyncValueSelector) + where TKey : notnull + { + var dictionary = new Dictionary(); + + foreach (TInput item in enumerable) + { + TKey key = syncKeySelector(item); + + TValue value = await asyncValueSelector(item); + + dictionary.Add(key, value); + } + + return dictionary; + } } diff --git a/src/Umbraco.Core/Extensions/EnumExtensions.cs b/src/Umbraco.Core/Extensions/EnumExtensions.cs index e13467ef32..3aa124d2f3 100644 --- a/src/Umbraco.Core/Extensions/EnumExtensions.cs +++ b/src/Umbraco.Core/Extensions/EnumExtensions.cs @@ -1,47 +1,42 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +/// +/// Provides extension methods to . +/// +public static class EnumExtensions { /// - /// Provides extension methods to . + /// Determines whether all the flags/bits are set within the enum value. /// - public static class EnumExtensions + /// The enum type. + /// The enum value. + /// The flags. + /// + /// true if all the flags/bits are set within the enum value; otherwise, false. + /// + [Obsolete("Use Enum.HasFlag() or bitwise operations (if performance is important) instead.")] + public static bool HasFlagAll(this T value, T flags) + where T : Enum => + value.HasFlag(flags); + + /// + /// Determines whether any of the flags/bits are set within the enum value. + /// + /// The enum type. + /// The value. + /// The flags. + /// + /// true if any of the flags/bits are set within the enum value; otherwise, false. + /// + public static bool HasFlagAny(this T value, T flags) + where T : Enum { - /// - /// Determines whether all the flags/bits are set within the enum value. - /// - /// The enum type. - /// The enum value. - /// The flags. - /// - /// true if all the flags/bits are set within the enum value; otherwise, false. - /// - [Obsolete("Use Enum.HasFlag() or bitwise operations (if performance is important) instead.")] - public static bool HasFlagAll(this T value, T flags) - where T : Enum - { - return value.HasFlag(flags); - } + var v = Convert.ToUInt64(value); + var f = Convert.ToUInt64(flags); - /// - /// Determines whether any of the flags/bits are set within the enum value. - /// - /// The enum type. - /// The value. - /// The flags. - /// - /// true if any of the flags/bits are set within the enum value; otherwise, false. - /// - public static bool HasFlagAny(this T value, T flags) - where T : Enum - { - var v = Convert.ToUInt64(value); - var f = Convert.ToUInt64(flags); - - return (v & f) > 0; - } + return (v & f) > 0; } } diff --git a/src/Umbraco.Core/Extensions/EnumerableExtensions.cs b/src/Umbraco.Core/Extensions/EnumerableExtensions.cs index 28f4844f00..6628dc4f3d 100644 --- a/src/Umbraco.Core/Extensions/EnumerableExtensions.cs +++ b/src/Umbraco.Core/Extensions/EnumerableExtensions.cs @@ -1,351 +1,399 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extensions for enumerable sources +/// +public static class EnumerableExtensions { + public static bool IsCollectionEmpty(this IReadOnlyCollection? list) => list == null || list.Count == 0; + /// - /// Extensions for enumerable sources + /// Wraps this object instance into an IEnumerable{T} consisting of a single item. /// - public static class EnumerableExtensions + /// Type of the object. + /// The instance that will be wrapped. + /// An IEnumerable{T} consisting of a single item. + public static IEnumerable Yield(this T item) { - public static bool IsCollectionEmpty(this IReadOnlyCollection? list) => list == null || list.Count == 0; + // see EnumeratorBenchmarks - this is faster, and allocates less, than returning an array + yield return item; + } - internal static bool HasDuplicates(this IEnumerable items, bool includeNull) + internal static bool HasDuplicates(this IEnumerable items, bool includeNull) + { + var hs = new HashSet(); + foreach (T item in items) { - var hs = new HashSet(); - foreach (var item in items) + if ((item != null || includeNull) && !hs.Add(item)) { - if ((item != null || includeNull) && !hs.Add(item)) - return true; - } - return false; - } - - - /// - /// Wraps this object instance into an IEnumerable{T} consisting of a single item. - /// - /// Type of the object. - /// The instance that will be wrapped. - /// An IEnumerable{T} consisting of a single item. - public static IEnumerable Yield(this T item) - { - // see EnumeratorBenchmarks - this is faster, and allocates less, than returning an array - yield return item; - } - - public static IEnumerable> InGroupsOf(this IEnumerable? source, int groupSize) - { - if (source == null) - throw new ArgumentNullException("source"); - if (groupSize <= 0) - throw new ArgumentException("Must be greater than zero.", "groupSize"); - - - // following code derived from MoreLinq and does not allocate bazillions of tuples - - T[]? temp = null; - var count = 0; - - foreach (var item in source) - { - if (temp == null) temp = new T[groupSize]; - temp[count++] = item; - if (count != groupSize) continue; - yield return temp/*.Select(x => x)*/; - temp = null; - count = 0; - } - - if (temp != null && count > 0) - yield return temp.Take(count); - } - - public static IEnumerable SelectByGroups(this IEnumerable source, Func, IEnumerable> selector, int groupSize) - { - // don't want to use a SelectMany(x => x) here - isn't this better? - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var resultGroup in source.InGroupsOf(groupSize).Select(selector)) - foreach (var result in resultGroup) - yield return result; - } - - /// - /// Returns a sequence of length whose elements are the result of invoking . - /// - /// - /// The factory. - /// The count. - /// - public static IEnumerable Range(Func factory, int count) - { - for (int i = 1; i <= count; i++) - { - yield return factory.Invoke(i - 1); + return true; } } - /// The if not null. - /// The items. - /// The action. - /// The type - public static void IfNotNull(this IEnumerable items, Action action) where TItem : class + return false; + } + + public static IEnumerable> InGroupsOf(this IEnumerable? source, int groupSize) + { + if (source == null) { - if (items != null) + throw new ArgumentNullException("source"); + } + + if (groupSize <= 0) + { + throw new ArgumentException("Must be greater than zero.", "groupSize"); + } + + // following code derived from MoreLinq and does not allocate bazillions of tuples + T[]? temp = null; + var count = 0; + + foreach (T item in source) + { + if (temp == null) { - foreach (TItem item in items) + temp = new T[groupSize]; + } + + temp[count++] = item; + if (count != groupSize) + { + continue; + } + + yield return temp /*.Select(x => x)*/; + temp = null; + count = 0; + } + + if (temp != null && count > 0) + { + yield return temp.Take(count); + } + } + + public static IEnumerable SelectByGroups( + this IEnumerable source, + Func, IEnumerable> selector, + int groupSize) + { + // don't want to use a SelectMany(x => x) here - isn't this better? + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (IEnumerable resultGroup in source.InGroupsOf(groupSize).Select(selector)) + { + foreach (TResult result in resultGroup) + { + yield return result; + } + } + } + + /// + /// Returns a sequence of length whose elements are the result of invoking + /// . + /// + /// + /// The factory. + /// The count. + /// + public static IEnumerable Range(Func factory, int count) + { + for (var i = 1; i <= count; i++) + { + yield return factory.Invoke(i - 1); + } + } + + /// The if not null. + /// The items. + /// The action. + /// The type + public static void IfNotNull(this IEnumerable items, Action action) + where TItem : class + { + if (items != null) + { + foreach (TItem item in items) + { + item.IfNotNull(action); + } + } + } + + /// + /// Returns true if all items in the other collection exist in this collection + /// + /// + /// + /// + /// + public static bool ContainsAll(this IEnumerable source, IEnumerable other) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (other == null) + { + throw new ArgumentNullException("other"); + } + + return other.Except(source).Any() == false; + } + + /// + /// Returns true if the source contains any of the items in the other list + /// + /// + /// + /// + /// + public static bool ContainsAny(this IEnumerable source, IEnumerable other) => + other.Any(source.Contains); + + /// + /// Removes all matching items from an . + /// + /// + /// The list. + /// The predicate. + /// + public static void RemoveAll(this IList list, Func predicate) + { + for (var i = 0; i < list.Count; i++) + { + if (predicate(list[i])) + { + list.RemoveAt(i--); + } + } + } + + /// + /// Removes all matching items from an . + /// + /// + /// The list. + /// The predicate. + /// + public static void RemoveAll(this ICollection list, Func predicate) + { + T[] matches = list.Where(predicate).ToArray(); + foreach (T match in matches) + { + list.Remove(match); + } + } + + public static IEnumerable SelectRecursive( + this IEnumerable source, + Func> recursiveSelector, + int maxRecusionDepth = 100) + { + var stack = new Stack>(); + stack.Push(source.GetEnumerator()); + + try + { + while (stack.Count > 0) + { + if (stack.Count > maxRecusionDepth) { - item.IfNotNull(action); + throw new InvalidOperationException("Maximum recursion depth reached of " + maxRecusionDepth); } - } - } - - /// - /// Returns true if all items in the other collection exist in this collection - /// - /// - /// - /// - /// - public static bool ContainsAll(this IEnumerable source, IEnumerable other) - { - if (source == null) throw new ArgumentNullException("source"); - if (other == null) throw new ArgumentNullException("other"); - - return other.Except(source).Any() == false; - } - - /// - /// Returns true if the source contains any of the items in the other list - /// - /// - /// - /// - /// - public static bool ContainsAny(this IEnumerable source, IEnumerable other) - { - return other.Any(source.Contains); - } - - /// - /// Removes all matching items from an . - /// - /// - /// The list. - /// The predicate. - /// - public static void RemoveAll(this IList list, Func predicate) - { - for (var i = 0; i < list.Count; i++) - { - if (predicate(list[i])) + if (stack.Peek().MoveNext()) { - list.RemoveAt(i--); + TSource current = stack.Peek().Current; + + yield return current; + + stack.Push(recursiveSelector(current).GetEnumerator()); } - } - } - - /// - /// Removes all matching items from an . - /// - /// - /// The list. - /// The predicate. - /// - public static void RemoveAll(this ICollection list, Func predicate) - { - var matches = list.Where(predicate).ToArray(); - foreach (var match in matches) - { - list.Remove(match); - } - } - - public static IEnumerable SelectRecursive( - this IEnumerable source, - Func> recursiveSelector, int maxRecusionDepth = 100) - { - var stack = new Stack>(); - stack.Push(source.GetEnumerator()); - - try - { - while (stack.Count > 0) - { - if (stack.Count > maxRecusionDepth) - throw new InvalidOperationException("Maximum recursion depth reached of " + maxRecusionDepth); - - if (stack.Peek().MoveNext()) - { - var current = stack.Peek().Current; - - yield return current; - - stack.Push(recursiveSelector(current).GetEnumerator()); - } - else - { - stack.Pop().Dispose(); - } - } - } - finally - { - while (stack.Count > 0) + else { stack.Pop().Dispose(); } } } - - /// - /// Filters a sequence of values to ignore those which are null. - /// - /// - /// The coll. - /// - /// - public static IEnumerable WhereNotNull(this IEnumerable coll) where T : class + finally { - return coll.Where(x => x != null)!; - } - - public static IEnumerable ForAllThatAre(this IEnumerable sequence, Action projection) - where TActual : class - { - return sequence.Select( - x => - { - if (x is TActual casted) - { - projection.Invoke(casted); - } - return x; - }); - } - - /// - /// Finds the index of the first item matching an expression in an enumerable. - /// - /// The type of the enumerated objects. - /// The enumerable to search. - /// The expression to test the items against. - /// The index of the first matching item, or -1. - public static int FindIndex(this IEnumerable items, Func predicate) - { - return FindIndex(items, 0, predicate); - } - - /// - /// Finds the index of the first item matching an expression in an enumerable. - /// - /// The type of the enumerated objects. - /// The enumerable to search. - /// The index to start at. - /// The expression to test the items against. - /// The index of the first matching item, or -1. - public static int FindIndex(this IEnumerable items, int startIndex, Func predicate) - { - if (items == null) throw new ArgumentNullException("items"); - if (predicate == null) throw new ArgumentNullException("predicate"); - if (startIndex < 0) throw new ArgumentOutOfRangeException("startIndex"); - - var index = startIndex; - if (index > 0) - items = items.Skip(index); - - foreach (var item in items) + while (stack.Count > 0) { - if (predicate(item)) return index; - index++; + stack.Pop().Dispose(); } - - return -1; - } - - ///Finds the index of the first occurrence of an item in an enumerable. - ///The enumerable to search. - ///The item to find. - ///The index of the first matching item, or -1 if the item was not found. - public static int IndexOf(this IEnumerable items, T item) - { - return items.FindIndex(i => EqualityComparer.Default.Equals(item, i)); - } - - /// - /// Determines if 2 lists have equal elements within them regardless of how they are sorted - /// - /// - /// - /// - /// - /// - /// The logic for this is taken from: - /// http://stackoverflow.com/questions/4576723/test-whether-two-ienumerablet-have-the-same-values-with-the-same-frequencies - /// - /// There's a few answers, this one seems the best for it's simplicity and based on the comment of Eamon - /// - public static bool UnsortedSequenceEqual(this IEnumerable? source, IEnumerable? other) - { - if (source == null && other == null) return true; - if (source == null || other == null) return false; - - var list1Groups = source.ToLookup(i => i); - var list2Groups = other.ToLookup(i => i); - return list1Groups.Count == list2Groups.Count - && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); - } - - /// - /// Transforms an enumerable. - /// - /// - /// - /// - /// - public static IEnumerable Transform(this IEnumerable source, Func, IEnumerable> transform) - { - return transform(source); - } - - /// - /// Gets a null IEnumerable as an empty IEnumerable. - /// - /// - /// - /// - public static IEnumerable EmptyNull(this IEnumerable? items) - { - return items ?? Enumerable.Empty(); - } - - // the .OfType() filter is nice when there's only one type - // this is to support filtering with multiple types - public static IEnumerable OfTypes(this IEnumerable contents, params Type[] types) - { - return contents.Where(x => types.Contains(x?.GetType())); - } - - public static IEnumerable SkipLast(this IEnumerable source) - { - using (var e = source.GetEnumerator()) - { - if (e.MoveNext() == false) yield break; - - for (var value = e.Current; e.MoveNext(); value = e.Current) - yield return value; - } - } - - public static IOrderedEnumerable OrderBy(this IEnumerable source, Func keySelector, Direction sortOrder) - { - return sortOrder == Direction.Ascending ? source.OrderBy(keySelector) : source.OrderByDescending(keySelector); } } + + /// + /// Filters a sequence of values to ignore those which are null. + /// + /// + /// The coll. + /// + /// + public static IEnumerable WhereNotNull(this IEnumerable coll) + where T : class + => + coll.Where(x => x != null)!; + + public static IEnumerable ForAllThatAre( + this IEnumerable sequence, + Action projection) + where TActual : class => + sequence.Select( + x => + { + if (x is TActual casted) + { + projection.Invoke(casted); + } + + return x; + }); + + /// + /// Finds the index of the first item matching an expression in an enumerable. + /// + /// The type of the enumerated objects. + /// The enumerable to search. + /// The expression to test the items against. + /// The index of the first matching item, or -1. + public static int FindIndex(this IEnumerable items, Func predicate) => + FindIndex(items, 0, predicate); + + /// + /// Finds the index of the first item matching an expression in an enumerable. + /// + /// The type of the enumerated objects. + /// The enumerable to search. + /// The index to start at. + /// The expression to test the items against. + /// The index of the first matching item, or -1. + public static int FindIndex(this IEnumerable items, int startIndex, Func predicate) + { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + if (predicate == null) + { + throw new ArgumentNullException("predicate"); + } + + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException("startIndex"); + } + + var index = startIndex; + if (index > 0) + { + items = items.Skip(index); + } + + foreach (T item in items) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; + } + + /// Finds the index of the first occurrence of an item in an enumerable. + /// The enumerable to search. + /// The item to find. + /// The index of the first matching item, or -1 if the item was not found. + public static int IndexOf(this IEnumerable items, T item) => + items.FindIndex(i => EqualityComparer.Default.Equals(item, i)); + + /// + /// Determines if 2 lists have equal elements within them regardless of how they are sorted + /// + /// + /// + /// + /// + /// + /// The logic for this is taken from: + /// http://stackoverflow.com/questions/4576723/test-whether-two-ienumerablet-have-the-same-values-with-the-same-frequencies + /// There's a few answers, this one seems the best for it's simplicity and based on the comment of Eamon + /// + public static bool UnsortedSequenceEqual(this IEnumerable? source, IEnumerable? other) + { + if (source == null && other == null) + { + return true; + } + + if (source == null || other == null) + { + return false; + } + + ILookup list1Groups = source.ToLookup(i => i); + ILookup list2Groups = other.ToLookup(i => i); + return list1Groups.Count == list2Groups.Count + && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); + } + + /// + /// Transforms an enumerable. + /// + /// + /// + /// + /// + public static IEnumerable Transform( + this IEnumerable source, + Func, IEnumerable> transform) => transform(source); + + /// + /// Gets a null IEnumerable as an empty IEnumerable. + /// + /// + /// + /// + public static IEnumerable EmptyNull(this IEnumerable? items) => items ?? Enumerable.Empty(); + + // the .OfType() filter is nice when there's only one type + // this is to support filtering with multiple types + public static IEnumerable OfTypes(this IEnumerable contents, params Type[] types) => + contents.Where(x => types.Contains(x?.GetType())); + + public static IEnumerable SkipLast(this IEnumerable source) + { + using (IEnumerator e = source.GetEnumerator()) + { + if (e.MoveNext() == false) + { + yield break; + } + + for (T value = e.Current; e.MoveNext(); value = e.Current) + { + yield return value; + } + } + } + + public static IOrderedEnumerable OrderBy( + this IEnumerable source, + Func keySelector, + Direction sortOrder) => sortOrder == Direction.Ascending + ? source.OrderBy(keySelector) + : source.OrderByDescending(keySelector); } diff --git a/src/Umbraco.Core/Extensions/ExpressionExtensions.cs b/src/Umbraco.Core/Extensions/ExpressionExtensions.cs index d76f39a8de..12476c9506 100644 --- a/src/Umbraco.Core/Extensions/ExpressionExtensions.cs +++ b/src/Umbraco.Core/Extensions/ExpressionExtensions.cs @@ -1,27 +1,25 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Linq.Expressions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +internal static class ExpressionExtensions { - internal static class ExpressionExtensions + public static Expression> True() => f => true; + + public static Expression> False() => f => false; + + public static Expression> Or(this Expression> left, Expression> right) { - public static Expression> True() { return f => true; } + InvocationExpression invokedExpr = Expression.Invoke(right, left.Parameters); + return Expression.Lambda>(Expression.OrElse(left.Body, invokedExpr), left.Parameters); + } - public static Expression> False() { return f => false; } - - public static Expression> Or(this Expression> left, Expression> right) - { - var invokedExpr = Expression.Invoke(right, left.Parameters); - return Expression.Lambda>(Expression.OrElse(left.Body, invokedExpr), left.Parameters); - } - - public static Expression> And(this Expression> left, Expression> right) - { - var invokedExpr = Expression.Invoke(right, left.Parameters); - return Expression.Lambda> (Expression.AndAlso(left.Body, invokedExpr), left.Parameters); - } + public static Expression> And(this Expression> left, Expression> right) + { + InvocationExpression invokedExpr = Expression.Invoke(right, left.Parameters); + return Expression.Lambda>(Expression.AndAlso(left.Body, invokedExpr), left.Parameters); } } diff --git a/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs b/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs index 944c9360b4..f1b19569ff 100644 --- a/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs +++ b/src/Umbraco.Core/Extensions/HostEnvironmentExtensions.cs @@ -1,53 +1,51 @@ -using System; -using System.IO; using Microsoft.Extensions.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Extensions +namespace Umbraco.Cms.Core.Extensions; + +/// +/// Contains extension methods for the interface. +/// +public static class HostEnvironmentExtensions { + private static string? _temporaryApplicationId; + /// - /// Contains extension methods for the interface. + /// Maps a virtual path to a physical path to the application's content root. /// - public static class HostEnvironmentExtensions + /// + /// Generally the content root is the parent directory of the web root directory. + /// + public static string MapPathContentRoot(this IHostEnvironment hostEnvironment, string path) { - private static string? s_temporaryApplicationId; + var root = hostEnvironment.ContentRootPath; - /// - /// Maps a virtual path to a physical path to the application's content root. - /// - /// - /// Generally the content root is the parent directory of the web root directory. - /// - public static string MapPathContentRoot(this IHostEnvironment hostEnvironment, string path) + var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); + + // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX + // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did, + // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not + // absolute in the file system. This error will help us find and fix improper uses, and should be removed once + // all those uses have been found and fixed + if (newPath.StartsWith(root)) { - var root = hostEnvironment.ContentRootPath; - - var newPath = path.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); - - // TODO: This is a temporary error because we switched from IOHelper.MapPath to HostingEnvironment.MapPathXXX - // IOHelper would check if the path passed in started with the root, and not prepend the root again if it did, - // however if you are requesting a path be mapped, it should always assume the path is relative to the root, not - // absolute in the file system. This error will help us find and fix improper uses, and should be removed once - // all those uses have been found and fixed - if (newPath.StartsWith(root)) - { - throw new ArgumentException("The path appears to already be fully qualified. Please remove the call to MapPathContentRoot"); - } - - return Path.Combine(root, newPath.TrimStart(Constants.CharArrays.TildeForwardSlashBackSlash)); + throw new ArgumentException( + "The path appears to already be fully qualified. Please remove the call to MapPathContentRoot"); } - /// - /// Gets a temporary application id for use before the ioc container is built. - /// - public static string GetTemporaryApplicationId(this IHostEnvironment hostEnvironment) - { - if (s_temporaryApplicationId != null) - { - return s_temporaryApplicationId; - } + return Path.Combine(root, newPath.TrimStart(Constants.CharArrays.TildeForwardSlashBackSlash)); + } - return s_temporaryApplicationId = hostEnvironment.ContentRootPath.GenerateHash(); + /// + /// Gets a temporary application id for use before the ioc container is built. + /// + public static string GetTemporaryApplicationId(this IHostEnvironment hostEnvironment) + { + if (_temporaryApplicationId != null) + { + return _temporaryApplicationId; } + + return _temporaryApplicationId = hostEnvironment.ContentRootPath.GenerateHash(); } } diff --git a/src/Umbraco.Core/Extensions/IfExtensions.cs b/src/Umbraco.Core/Extensions/IfExtensions.cs index b4ef60ea57..1ab908b445 100644 --- a/src/Umbraco.Core/Extensions/IfExtensions.cs +++ b/src/Umbraco.Core/Extensions/IfExtensions.cs @@ -1,60 +1,58 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +/// +/// Extension methods for 'If' checking like checking If something is null or not null +/// +public static class IfExtensions { - /// - /// Extension methods for 'If' checking like checking If something is null or not null - /// - public static class IfExtensions + /// The if not null. + /// The item. + /// The action. + /// The type + public static void IfNotNull(this TItem item, Action action) + where TItem : class { - /// The if not null. - /// The item. - /// The action. - /// The type - public static void IfNotNull(this TItem item, Action action) where TItem : class + if (item != null) { - if (item != null) - { - action(item); - } + action(item); } - - /// The if true. - /// The predicate. - /// The action. - public static void IfTrue(this bool predicate, Action action) - { - if (predicate) - { - action(); - } - } - - /// - /// Checks if the item is not null, and if so returns an action on that item, or a default value - /// - /// the result type - /// The type - /// The item. - /// The action. - /// The default value. - /// - public static TResult? IfNotNull(this TItem? item, Func action, TResult? defaultValue = default(TResult)) - where TItem : class - => item != null ? action(item) : defaultValue; - - /// - /// Checks if the value is null, if it is it returns the value specified, otherwise returns the non-null value - /// - /// - /// - /// - /// - public static TItem IfNull(this TItem? item, Func action) - where TItem : class - => item ?? action(item!); } + + /// The if true. + /// The predicate. + /// The action. + public static void IfTrue(this bool predicate, Action action) + { + if (predicate) + { + action(); + } + } + + /// + /// Checks if the item is not null, and if so returns an action on that item, or a default value + /// + /// the result type + /// The type + /// The item. + /// The action. + /// The default value. + /// + public static TResult? IfNotNull(this TItem? item, Func action, TResult? defaultValue = default) + where TItem : class + => item != null ? action(item) : defaultValue; + + /// + /// Checks if the value is null, if it is it returns the value specified, otherwise returns the non-null value + /// + /// + /// + /// + /// + public static TItem IfNull(this TItem? item, Func action) + where TItem : class + => item ?? action(item!); } diff --git a/src/Umbraco.Core/Extensions/IntExtensions.cs b/src/Umbraco.Core/Extensions/IntExtensions.cs index 4f79baa3f5..d347993dd0 100644 --- a/src/Umbraco.Core/Extensions/IntExtensions.cs +++ b/src/Umbraco.Core/Extensions/IntExtensions.cs @@ -1,35 +1,34 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +public static class IntExtensions { - public static class IntExtensions + /// + /// Does something 'x' amount of times + /// + /// + /// + public static void Times(this int n, Action action) { - /// - /// Does something 'x' amount of times - /// - /// - /// - public static void Times(this int n, Action action) + for (var i = 0; i < n; i++) { - for (int i = 0; i < n; i++) - { - action(i); - } - } - - /// - /// Creates a Guid based on an integer value - /// - /// value to convert - /// - public static Guid ToGuid(this int value) - { - byte[] bytes = new byte[16]; - BitConverter.GetBytes(value).CopyTo(bytes, 0); - return new Guid(bytes); + action(i); } } + + /// + /// Creates a Guid based on an integer value + /// + /// value to convert + /// + /// + /// + public static Guid ToGuid(this int value) + { + var bytes = new byte[16]; + BitConverter.GetBytes(value).CopyTo(bytes, 0); + return new Guid(bytes); + } } diff --git a/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs b/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs index 73927f7a41..7189c4cc15 100644 --- a/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs +++ b/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs @@ -1,23 +1,20 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +/// +/// Provides extension methods for the struct. +/// +public static class KeyValuePairExtensions { /// - /// Provides extension methods for the struct. + /// Implements key/value pair deconstruction. /// - public static class KeyValuePairExtensions + /// Allows for foreach ((var k, var v) in ...). + public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { - /// - /// Implements key/value pair deconstruction. - /// - /// Allows for foreach ((var k, var v) in ...). - public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) - { - key = kvp.Key; - value = kvp.Value; - } + key = kvp.Key; + value = kvp.Value; } } diff --git a/src/Umbraco.Core/Extensions/MediaTypeExtensions.cs b/src/Umbraco.Core/Extensions/MediaTypeExtensions.cs index 2c46271964..f9aec08a61 100644 --- a/src/Umbraco.Core/Extensions/MediaTypeExtensions.cs +++ b/src/Umbraco.Core/Extensions/MediaTypeExtensions.cs @@ -1,16 +1,15 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class MediaTypeExtensions { - public static class MediaTypeExtensions - { - public static bool IsSystemMediaType(this IMediaType mediaType) => - mediaType.Alias == Constants.Conventions.MediaTypes.File - || mediaType.Alias == Constants.Conventions.MediaTypes.Folder - || mediaType.Alias == Constants.Conventions.MediaTypes.Image; - } + public static bool IsSystemMediaType(this IMediaType mediaType) => + mediaType.Alias == Constants.Conventions.MediaTypes.File + || mediaType.Alias == Constants.Conventions.MediaTypes.Folder + || mediaType.Alias == Constants.Conventions.MediaTypes.Image; } diff --git a/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs b/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs index a07abfbd96..f8fdcdc83f 100644 --- a/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs +++ b/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs @@ -1,43 +1,39 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; +using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class NameValueCollectionExtensions { - public static class NameValueCollectionExtensions + public static IEnumerable> AsEnumerable(this NameValueCollection nvc) { - public static IEnumerable> AsEnumerable(this NameValueCollection nvc) + foreach (var key in nvc.AllKeys) { - foreach (string? key in nvc.AllKeys) - { - yield return new KeyValuePair(key, nvc[key]); - } - } - - public static bool ContainsKey(this NameValueCollection collection, string key) - { - return collection.Keys.Cast().Any(k => (string) k == key); - } - - public static T? GetValue(this NameValueCollection collection, string key, T defaultIfNotFound) - { - if (collection.ContainsKey(key) == false) - { - return defaultIfNotFound; - } - - var val = collection[key]; - if (val == null) - { - return defaultIfNotFound; - } - - var result = val.TryConvertTo(); - - return result.Success ? result.Result : defaultIfNotFound; + yield return new KeyValuePair(key, nvc[key]); } } + + public static bool ContainsKey(this NameValueCollection collection, string key) => + collection.Keys.Cast().Any(k => (string)k == key); + + public static T? GetValue(this NameValueCollection collection, string key, T defaultIfNotFound) + { + if (collection.ContainsKey(key) == false) + { + return defaultIfNotFound; + } + + var val = collection[key]; + if (val == null) + { + return defaultIfNotFound; + } + + Attempt result = val.TryConvertTo(); + + return result.Success ? result.Result : defaultIfNotFound; + } } diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index 1ba7e0fc4d..6dc220446b 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -1,13 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.ComponentModel; -using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; @@ -15,767 +11,856 @@ using System.Xml; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Collections; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides object extension methods. +/// +public static class ObjectExtensions { + private static readonly ConcurrentDictionary NullableGenericCache = new(); + private static readonly ConcurrentDictionary InputTypeConverterCache = new(); + + private static readonly ConcurrentDictionary DestinationTypeConverterCache = + new(); + + private static readonly ConcurrentDictionary AssignableTypeCache = new(); + private static readonly ConcurrentDictionary BoolConvertCache = new(); + + private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; + private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new(); + + // private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); + /// - /// Provides object extension methods. /// - public static class ObjectExtensions + /// + /// + /// + public static IEnumerable AsEnumerableOfOne(this T input) => Enumerable.Repeat(input, 1); + + /// + /// + /// + public static void DisposeIfDisposable(this object input) { - private static readonly ConcurrentDictionary NullableGenericCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary InputTypeConverterCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary DestinationTypeConverterCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary AssignableTypeCache = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary BoolConvertCache = new ConcurrentDictionary(); - - private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; - private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter(); - - //private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); - - /// - /// - /// - /// - /// - /// - public static IEnumerable AsEnumerableOfOne(this T input) + if (input is IDisposable disposable) { - return Enumerable.Repeat(input, 1); + disposable.Dispose(); } + } - /// - /// - /// - /// - public static void DisposeIfDisposable(this object input) + /// + /// Provides a shortcut way of safely casting an input when you cannot guarantee the is + /// an instance type (i.e., when the C# AS keyword is not applicable). + /// + /// + /// The input. + /// + public static T? SafeCast(this object input) + { + if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) { - if (input is IDisposable disposable) - disposable.Dispose(); - } - - /// - /// Provides a shortcut way of safely casting an input when you cannot guarantee the is - /// an instance type (i.e., when the C# AS keyword is not applicable). - /// - /// - /// The input. - /// - public static T? SafeCast(this object input) - { - if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default; - if (input is T variable) return variable; return default; } - /// - /// Attempts to convert the input object to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The type to convert to - /// The input. - /// The - public static Attempt TryConvertTo(this object? input) + if (input is T variable) { - Attempt result = TryConvertTo(input, typeof(T)); - - if (result.Success) - { - return Attempt.Succeed((T?)result.Result); - } - - if (input == null) - { - if (typeof(T).IsValueType) - { - // fail, cannot convert null to a value type - return Attempt.Fail(); - } - else - { - // sure, null can be any object - return Attempt.Succeed((T)input!); - } - } - - // just try to cast - try - { - return Attempt.Succeed((T)input); - } - catch (Exception e) - { - return Attempt.Fail(e); - } + return variable; } - /// - /// Attempts to convert the input object to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The input. - /// The type to convert to - /// The - public static Attempt TryConvertTo(this object? input, Type target) + return default; + } + + /// + /// Attempts to convert the input object to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The type to convert to + /// The input. + /// The + public static Attempt TryConvertTo(this object? input) + { + Attempt result = TryConvertTo(input, typeof(T)); + + if (result.Success) { - if (target == null) + return Attempt.Succeed((T?)result.Result); + } + + if (input == null) + { + if (typeof(T).IsValueType) { - return Attempt.Fail(); + // fail, cannot convert null to a value type + return Attempt.Fail(); } - try + // sure, null can be any object + return Attempt.Succeed((T)input!); + } + + // just try to cast + try + { + return Attempt.Succeed((T)input); + } + catch (Exception e) + { + return Attempt.Fail(e); + } + } + + /// + /// Attempts to convert the input object to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The input. + /// The type to convert to + /// The + public static Attempt TryConvertTo(this object? input, Type target) + { + if (target == null) + { + return Attempt.Fail(); + } + + try + { + if (input == null) { - if (input == null) - { - // Nullable is ok - if (target.IsGenericType && GetCachedGenericNullableType(target) != null) - { - return Attempt.Succeed(null); - } - - // Reference types are ok - return Attempt.If(target.IsValueType == false, null); - } - - var inputType = input.GetType(); - - // Easy - if (target == typeof(object) || inputType == target) - { - return Attempt.Succeed(input); - } - - // Check for string so that overloaders of ToString() can take advantage of the conversion. - if (target == typeof(string)) - { - return Attempt.Succeed(input.ToString()); - } - - // If we've got a nullable of something, we try to convert directly to that thing. - // We cache the destination type and underlying nullable types - // Any other generic types need to fall through - if (target.IsGenericType) - { - var underlying = GetCachedGenericNullableType(target); - if (underlying != null) - { - // Special case for empty strings for bools/dates which should return null if an empty string. - if (input is string inputString) - { - // TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null? - if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool))) - { - return Attempt.Succeed(null); - } - } - - // Recursively call into this method with the inner (not-nullable) type and handle the outcome - var inner = input.TryConvertTo(underlying); - - // And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception - if (inner.Success) - { - input = inner.Result; // Now fall on through... - } - else - { - return Attempt.Fail(inner.Exception); - } - } - } - else - { - // target is not a generic type - - if (input is string inputString) - { - // Try convert from string, returns an Attempt if the string could be - // processed (either succeeded or failed), else null if we need to try - // other methods - var result = TryConvertToFromString(inputString, target); - if (result.HasValue) - { - return result.Value; - } - } - - // TODO: Do a check for destination type being IEnumerable and source type implementing IEnumerable with - // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it. - if (GetCachedCanAssign(input, inputType, target)) - { - return Attempt.Succeed(Convert.ChangeType(input, target)); - } - } - - if (target == typeof(bool)) - { - if (GetCachedCanConvertToBoolean(inputType)) - { - return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input!)); - } - } - - var inputConverter = GetCachedSourceTypeConverter(inputType, target); - if (inputConverter != null) - { - return Attempt.Succeed(inputConverter.ConvertTo(input, target)); - } - - var outputConverter = GetCachedTargetTypeConverter(inputType, target); - if (outputConverter != null) - { - return Attempt.Succeed(outputConverter.ConvertFrom(input!)); - } - + // Nullable is ok if (target.IsGenericType && GetCachedGenericNullableType(target) != null) { - // cannot Convert.ChangeType as that does not work with nullable - // input has already been converted to the underlying type - just - // return input, there's an implicit conversion from T to T? anyways - return Attempt.Succeed(input); + return Attempt.Succeed(null); } - // Re-check convertibles since we altered the input through recursion - if (input is IConvertible convertible2) + // Reference types are ok + return Attempt.If(target.IsValueType == false, null); + } + + Type inputType = input.GetType(); + + // Easy + if (target == typeof(object) || inputType == target) + { + return Attempt.Succeed(input); + } + + // Check for string so that overloaders of ToString() can take advantage of the conversion. + if (target == typeof(string)) + { + return Attempt.Succeed(input.ToString()); + } + + // If we've got a nullable of something, we try to convert directly to that thing. + // We cache the destination type and underlying nullable types + // Any other generic types need to fall through + if (target.IsGenericType) + { + Type? underlying = GetCachedGenericNullableType(target); + if (underlying != null) { - return Attempt.Succeed(Convert.ChangeType(convertible2, target)); + // Special case for empty strings for bools/dates which should return null if an empty string. + if (input is string inputString) + { + // TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null? + if (string.IsNullOrEmpty(inputString) && + (underlying == typeof(DateTime) || underlying == typeof(bool))) + { + return Attempt.Succeed(null); + } + } + + // Recursively call into this method with the inner (not-nullable) type and handle the outcome + Attempt inner = input.TryConvertTo(underlying); + + // And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception + if (inner.Success) + { + input = inner.Result; // Now fall on through... + } + else + { + return Attempt.Fail(inner.Exception); + } } } - catch (Exception e) + else { - return Attempt.Fail(e); + // target is not a generic type + if (input is string inputString) + { + // Try convert from string, returns an Attempt if the string could be + // processed (either succeeded or failed), else null if we need to try + // other methods + Attempt? result = TryConvertToFromString(inputString, target); + if (result.HasValue) + { + return result.Value; + } + } + + // TODO: Do a check for destination type being IEnumerable and source type implementing IEnumerable with + // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it. + if (GetCachedCanAssign(input, inputType, target)) + { + return Attempt.Succeed(Convert.ChangeType(input, target)); + } + } + + if (target == typeof(bool)) + { + if (GetCachedCanConvertToBoolean(inputType)) + { + return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input!)); + } + } + + TypeConverter? inputConverter = GetCachedSourceTypeConverter(inputType, target); + if (inputConverter != null) + { + return Attempt.Succeed(inputConverter.ConvertTo(input, target)); + } + + TypeConverter? outputConverter = GetCachedTargetTypeConverter(inputType, target); + if (outputConverter != null) + { + return Attempt.Succeed(outputConverter.ConvertFrom(input!)); + } + + if (target.IsGenericType && GetCachedGenericNullableType(target) != null) + { + // cannot Convert.ChangeType as that does not work with nullable + // input has already been converted to the underlying type - just + // return input, there's an implicit conversion from T to T? anyways + return Attempt.Succeed(input); + } + + // Re-check convertibles since we altered the input through recursion + if (input is IConvertible convertible2) + { + return Attempt.Succeed(Convert.ChangeType(convertible2, target)); + } + } + catch (Exception e) + { + return Attempt.Fail(e); + } + + return Attempt.Fail(); + } + + // public enum PropertyNamesCaseType + // { + // CamelCase, + // CaseInsensitive + // } + + ///// + ///// Convert an object to a JSON string with camelCase formatting + ///// + ///// + ///// + // public static string ToJsonString(this object obj) + // { + // return obj.ToJsonString(PropertyNamesCaseType.CamelCase); + // } + + ///// + ///// Convert an object to a JSON string with the specified formatting + ///// + ///// The obj. + ///// Type of the property names case. + ///// + // public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType) + // { + // var type = obj.GetType(); + // var dateTimeStyle = "yyyy-MM-dd HH:mm:ss"; + + // if (type.IsPrimitive || typeof(string).IsAssignableFrom(type)) + // { + // return obj.ToString(); + // } + + // if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type)) + // { + // return Convert.ToDateTime(obj).ToString(dateTimeStyle); + // } + + // var serializer = new JsonSerializer(); + + // switch (propertyNamesCaseType) + // { + // case PropertyNamesCaseType.CamelCase: + // serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); + // break; + // } + + // var dateTimeConverter = new IsoDateTimeConverter + // { + // DateTimeStyles = System.Globalization.DateTimeStyles.None, + // DateTimeFormat = dateTimeStyle + // }; + + // if (typeof(IDictionary).IsAssignableFrom(type)) + // { + // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + // if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type))) + // { + // return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); + // } + + /// + /// Converts an object into a dictionary + /// + /// + /// + /// + /// + /// + /// + public static IDictionary? ToDictionary( + this T o, + params Expression>[] ignoreProperties) => o?.ToDictionary(ignoreProperties + .Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); + + internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) + { + // TODO: Localize this exception + if (isDisposed) + { + throw new ObjectDisposedException(objectname); + } + } + + /// + /// Attempts to convert the input string to the output type. + /// + /// This code is an optimized version of the original Umbraco method + /// The input. + /// The type to convert to + /// The + private static Attempt? TryConvertToFromString(this string input, Type target) + { + // Easy + if (target == typeof(string)) + { + return Attempt.Succeed(input); + } + + // Null, empty, whitespaces + if (string.IsNullOrWhiteSpace(input)) + { + if (target == typeof(bool)) + { + // null/empty = bool false + return Attempt.Succeed(false); + } + + if (target == typeof(DateTime)) + { + // null/empty = min DateTime value + return Attempt.Succeed(DateTime.MinValue); + } + + // Cannot decide here, + // Any of the types below will fail parsing and will return a failed attempt + // but anything else will not be processed and will return null + // so even though the string is null/empty we have to proceed. + } + + // Look for type conversions in the expected order of frequency of use. + // + // By using a mixture of ordered if statements and switches we can optimize both for + // fast conditional checking for most frequently used types and the branching + // that does not depend on previous values available to switch statements. + if (target.IsPrimitive) + { + if (target == typeof(int)) + { + if (int.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Because decimal 100.01m will happily convert to integer 100, it + // makes sense that string "100.01" *also* converts to integer 100. + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2)); + } + + if (target == typeof(long)) + { + if (long.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Same as int + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); + } + + // TODO: Should we do the decimal trick for short, byte, unsigned? + if (target == typeof(bool)) + { + if (bool.TryParse(input, out var value)) + { + return Attempt.Succeed(value); + } + + // Don't declare failure so the CustomBooleanTypeConverter can try + return null; + } + + // Calling this method directly is faster than any attempt to cache it. + switch (Type.GetTypeCode(target)) + { + case TypeCode.Int16: + return Attempt.If(short.TryParse(input, out var value), value); + + case TypeCode.Double: + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(double.TryParse(input2, out var valueD), valueD); + + case TypeCode.Single: + var input3 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(float.TryParse(input3, out var valueF), valueF); + + case TypeCode.Char: + return Attempt.If(char.TryParse(input, out var valueC), valueC); + + case TypeCode.Byte: + return Attempt.If(byte.TryParse(input, out var valueB), valueB); + + case TypeCode.SByte: + return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); + + case TypeCode.UInt32: + return Attempt.If(uint.TryParse(input, out var valueU), valueU); + + case TypeCode.UInt16: + return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); + + case TypeCode.UInt64: + return Attempt.If(ulong.TryParse(input, out var valueUl), valueUl); + } + } + else if (target == typeof(Guid)) + { + return Attempt.If(Guid.TryParse(input, out Guid value), value); + } + else if (target == typeof(DateTime)) + { + if (DateTime.TryParse(input, out DateTime value)) + { + switch (value.Kind) + { + case DateTimeKind.Unspecified: + case DateTimeKind.Utc: + return Attempt.Succeed(value); + + case DateTimeKind.Local: + return Attempt.Succeed(value.ToUniversalTime()); + + default: + throw new ArgumentOutOfRangeException(); + } } return Attempt.Fail(); } - - /// - /// Attempts to convert the input string to the output type. - /// - /// This code is an optimized version of the original Umbraco method - /// The input. - /// The type to convert to - /// The - private static Attempt? TryConvertToFromString(this string input, Type target) + else if (target == typeof(DateTimeOffset)) { - // Easy - if (target == typeof(string)) + return Attempt.If(DateTimeOffset.TryParse(input, out DateTimeOffset value), value); + } + else if (target == typeof(TimeSpan)) + { + return Attempt.If(TimeSpan.TryParse(input, out TimeSpan value), value); + } + else if (target == typeof(decimal)) + { + var input2 = NormalizeNumberDecimalSeparator(input); + return Attempt.If(decimal.TryParse(input2, out var value), value); + } + else if (input != null && target == typeof(Version)) + { + return Attempt.If(Version.TryParse(input, out Version? value), value); + } + + // E_NOTIMPL IPAddress, BigInteger + return null; // we can't decide... + } + + /// + /// Turns object into dictionary + /// + /// + /// Properties to ignore + /// + public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) + { + if (o != null) + { + PropertyDescriptorCollection props = TypeDescriptor.GetProperties(o); + var d = new Dictionary(); + foreach (PropertyDescriptor prop in props.Cast() + .Where(x => ignoreProperties.Contains(x.Name) == false)) { - return Attempt.Succeed(input); - } - - // Null, empty, whitespaces - if (string.IsNullOrWhiteSpace(input)) - { - if (target == typeof(bool)) + var val = prop.GetValue(o); + if (val != null) { - // null/empty = bool false - return Attempt.Succeed(false); - } - - if (target == typeof(DateTime)) - { - // null/empty = min DateTime value - return Attempt.Succeed(DateTime.MinValue); - } - - // Cannot decide here, - // Any of the types below will fail parsing and will return a failed attempt - // but anything else will not be processed and will return null - // so even though the string is null/empty we have to proceed. - } - - // Look for type conversions in the expected order of frequency of use. - // - // By using a mixture of ordered if statements and switches we can optimize both for - // fast conditional checking for most frequently used types and the branching - // that does not depend on previous values available to switch statements. - if (target.IsPrimitive) - { - if (target == typeof(int)) - { - if (int.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Because decimal 100.01m will happily convert to integer 100, it - // makes sense that string "100.01" *also* converts to integer 100. - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2)); - } - - if (target == typeof(long)) - { - if (long.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Same as int - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); - } - - // TODO: Should we do the decimal trick for short, byte, unsigned? - - if (target == typeof(bool)) - { - if (bool.TryParse(input, out var value)) - { - return Attempt.Succeed(value); - } - - // Don't declare failure so the CustomBooleanTypeConverter can try - return null; - } - - // Calling this method directly is faster than any attempt to cache it. - switch (Type.GetTypeCode(target)) - { - case TypeCode.Int16: - return Attempt.If(short.TryParse(input, out var value), value); - - case TypeCode.Double: - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(double.TryParse(input2, out var valueD), valueD); - - case TypeCode.Single: - var input3 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(float.TryParse(input3, out var valueF), valueF); - - case TypeCode.Char: - return Attempt.If(char.TryParse(input, out var valueC), valueC); - - case TypeCode.Byte: - return Attempt.If(byte.TryParse(input, out var valueB), valueB); - - case TypeCode.SByte: - return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); - - case TypeCode.UInt32: - return Attempt.If(uint.TryParse(input, out var valueU), valueU); - - case TypeCode.UInt16: - return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); - - case TypeCode.UInt64: - return Attempt.If(ulong.TryParse(input, out var valueUl), valueUl); + d.Add(prop.Name, (TVal)val); } } - else if (target == typeof(Guid)) - { - return Attempt.If(Guid.TryParse(input, out var value), value); - } - else if (target == typeof(DateTime)) - { - if (DateTime.TryParse(input, out var value)) - { - switch (value.Kind) - { - case DateTimeKind.Unspecified: - case DateTimeKind.Utc: - return Attempt.Succeed(value); - case DateTimeKind.Local: - return Attempt.Succeed(value.ToUniversalTime()); - - default: - throw new ArgumentOutOfRangeException(); - } - } - - return Attempt.Fail(); - } - else if (target == typeof(DateTimeOffset)) - { - return Attempt.If(DateTimeOffset.TryParse(input, out var value), value); - } - else if (target == typeof(TimeSpan)) - { - return Attempt.If(TimeSpan.TryParse(input, out var value), value); - } - else if (target == typeof(decimal)) - { - var input2 = NormalizeNumberDecimalSeparator(input); - return Attempt.If(decimal.TryParse(input2, out var value), value); - } - else if (input != null && target == typeof(Version)) - { - return Attempt.If(Version.TryParse(input, out var value), value); - } - - // E_NOTIMPL IPAddress, BigInteger - return null; // we can't decide... - } - internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) - { - // TODO: Localize this exception - if (isDisposed) - throw new ObjectDisposedException(objectname); + return d; } - //public enum PropertyNamesCaseType - //{ - // CamelCase, - // CaseInsensitive - //} + return new Dictionary(); + } - ///// - ///// Convert an object to a JSON string with camelCase formatting - ///// - ///// - ///// - //public static string ToJsonString(this object obj) - //{ - // return obj.ToJsonString(PropertyNamesCaseType.CamelCase); - //} - - ///// - ///// Convert an object to a JSON string with the specified formatting - ///// - ///// The obj. - ///// Type of the property names case. - ///// - //public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType) - //{ - // var type = obj.GetType(); - // var dateTimeStyle = "yyyy-MM-dd HH:mm:ss"; - - // if (type.IsPrimitive || typeof(string).IsAssignableFrom(type)) - // { - // return obj.ToString(); - // } - - // if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type)) - // { - // return Convert.ToDateTime(obj).ToString(dateTimeStyle); - // } - - // var serializer = new JsonSerializer(); - - // switch (propertyNamesCaseType) - // { - // case PropertyNamesCaseType.CamelCase: - // serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); - // break; - // } - - // var dateTimeConverter = new IsoDateTimeConverter - // { - // DateTimeStyles = System.Globalization.DateTimeStyles.None, - // DateTimeFormat = dateTimeStyle - // }; - - // if (typeof(IDictionary).IsAssignableFrom(type)) - // { - // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - // } - - // if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type))) - // { - // return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - // } - - // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); - //} - - /// - /// Converts an object into a dictionary - /// - /// - /// - /// - /// - /// - /// - public static IDictionary? ToDictionary(this T o, params Expression>[] ignoreProperties) + /// + /// Returns an XmlSerialized safe string representation for the value + /// + /// + /// The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown + /// + public static string ToXmlString(this object value, Type type) + { + if (value == null) { - return o?.ToDictionary(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); + return string.Empty; } - /// - /// Turns object into dictionary - /// - /// - /// Properties to ignore - /// - public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) + if (type == typeof(string)) { - if (o != null) + return value.ToString().IsNullOrWhiteSpace() ? string.Empty : value.ToString()!; + } + + if (type == typeof(bool)) + { + return XmlConvert.ToString((bool)value); + } + + if (type == typeof(byte)) + { + return XmlConvert.ToString((byte)value); + } + + if (type == typeof(char)) + { + return XmlConvert.ToString((char)value); + } + + if (type == typeof(DateTime)) + { + return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified); + } + + if (type == typeof(DateTimeOffset)) + { + return XmlConvert.ToString((DateTimeOffset)value); + } + + if (type == typeof(decimal)) + { + return XmlConvert.ToString((decimal)value); + } + + if (type == typeof(double)) + { + return XmlConvert.ToString((double)value); + } + + if (type == typeof(float)) + { + return XmlConvert.ToString((float)value); + } + + if (type == typeof(Guid)) + { + return XmlConvert.ToString((Guid)value); + } + + if (type == typeof(int)) + { + return XmlConvert.ToString((int)value); + } + + if (type == typeof(long)) + { + return XmlConvert.ToString((long)value); + } + + if (type == typeof(sbyte)) + { + return XmlConvert.ToString((sbyte)value); + } + + if (type == typeof(short)) + { + return XmlConvert.ToString((short)value); + } + + if (type == typeof(TimeSpan)) + { + return XmlConvert.ToString((TimeSpan)value); + } + + if (type == typeof(uint)) + { + return XmlConvert.ToString((uint)value); + } + + if (type == typeof(ulong)) + { + return XmlConvert.ToString((ulong)value); + } + + if (type == typeof(ushort)) + { + return XmlConvert.ToString((ushort)value); + } + + throw new NotSupportedException("Cannot convert type " + type.FullName + + " to a string using ToXmlString as it is not supported by XmlConvert"); + } + + internal static string? ToDebugString(this object? obj, int levels = 0) + { + if (obj == null) + { + return "{null}"; + } + + try + { + if (obj is string) { - var props = TypeDescriptor.GetProperties(o); - var d = new Dictionary(); - foreach (var prop in props.Cast().Where(x => ignoreProperties.Contains(x.Name) == false)) - { - var val = prop.GetValue(o); - if (val != null) - { - d.Add(prop.Name, (TVal)val); - } - } - return d; + return "\"{0}\"".InvariantFormat(obj); } - return new Dictionary(); - } - - - internal static string? ToDebugString(this object? obj, int levels = 0) - { - if (obj == null) return "{null}"; - try + if (obj is int || obj is short || obj is long || obj is float || obj is double || obj is bool || + obj is int? || obj is short? || obj is long? || obj is float? || obj is double? || obj is bool?) { - if (obj is string) - { - return "\"{0}\"".InvariantFormat(obj); - } - if (obj is int || obj is short || obj is long || obj is float || obj is double || obj is bool || obj is int? || obj is short? || obj is long? || obj is float? || obj is double? || obj is bool?) - { - return "{0}".InvariantFormat(obj); - } - if (obj is Enum) - { - return "[{0}]".InvariantFormat(obj); - } - if (obj is IEnumerable enumerable) - { - var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList(); + return "{0}".InvariantFormat(obj); + } - return items.Any() - ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) - : null; - } + if (obj is Enum) + { + return "[{0}]".InvariantFormat(obj); + } - var props = obj.GetType().GetProperties(); - if ((props.Length == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) - { - try - { - var key = props[0].GetValue(obj, null) as string; - var value = props[1].GetValue(obj, null).ToDebugString(levels - 1); - return "{0}={1}".InvariantFormat(key, value); - } - catch (Exception) - { - return "[KeyValuePropertyException]"; - } - } - if (levels > -1) - { - var items = - (from propertyInfo in props - let value = GetPropertyDebugString(propertyInfo, obj, levels) - where value != null - select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); + if (obj is IEnumerable enumerable) + { + var items = (from object enumItem in enumerable + let value = GetEnumPropertyDebugString(enumItem, levels) + where value != null + select value).Take(10).ToList(); - return items.Any() - ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items)) - : null; + return items.Any() + ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) + : null; + } + + PropertyInfo[] props = obj.GetType().GetProperties(); + if (props.Length == 2 && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) + { + try + { + var key = props[0].GetValue(obj, null) as string; + var value = props[1].GetValue(obj, null).ToDebugString(levels - 1); + return "{0}={1}".InvariantFormat(key, value); + } + catch (Exception) + { + return "[KeyValuePropertyException]"; } } - catch (Exception ex) - { - return "[Exception:{0}]".InvariantFormat(ex.Message); - } - return null; - } - /// - /// Attempts to serialize the value to an XmlString using ToXmlString - /// - /// - /// - /// - internal static Attempt TryConvertToXmlString(this object value, Type type) + if (levels > -1) + { + var items = + (from propertyInfo in props + let value = GetPropertyDebugString(propertyInfo, obj, levels) + where value != null + select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); + + return items.Any() + ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, string.Join(", ", items)) + : null; + } + } + catch (Exception ex) { - try - { - var output = value.ToXmlString(type); - return Attempt.Succeed(output); - } - catch (NotSupportedException ex) - { - return Attempt.Fail(ex); - } + return "[Exception:{0}]".InvariantFormat(ex.Message); } - /// - /// Returns an XmlSerialized safe string representation for the value - /// - /// - /// The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown - /// - public static string ToXmlString(this object value, Type type) + return null; + } + + /// + /// Attempts to serialize the value to an XmlString using ToXmlString + /// + /// + /// + /// + internal static Attempt TryConvertToXmlString(this object value, Type type) + { + try { - if (value == null) return string.Empty; - if (type == typeof(string)) return (value.ToString().IsNullOrWhiteSpace() ? "" : value.ToString()!); - if (type == typeof(bool)) return XmlConvert.ToString((bool)value); - if (type == typeof(byte)) return XmlConvert.ToString((byte)value); - if (type == typeof(char)) return XmlConvert.ToString((char)value); - if (type == typeof(DateTime)) return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified); - if (type == typeof(DateTimeOffset)) return XmlConvert.ToString((DateTimeOffset)value); - if (type == typeof(decimal)) return XmlConvert.ToString((decimal)value); - if (type == typeof(double)) return XmlConvert.ToString((double)value); - if (type == typeof(float)) return XmlConvert.ToString((float)value); - if (type == typeof(Guid)) return XmlConvert.ToString((Guid)value); - if (type == typeof(int)) return XmlConvert.ToString((int)value); - if (type == typeof(long)) return XmlConvert.ToString((long)value); - if (type == typeof(sbyte)) return XmlConvert.ToString((sbyte)value); - if (type == typeof(short)) return XmlConvert.ToString((short)value); - if (type == typeof(TimeSpan)) return XmlConvert.ToString((TimeSpan)value); - if (type == typeof(uint)) return XmlConvert.ToString((uint)value); - if (type == typeof(ulong)) return XmlConvert.ToString((ulong)value); - if (type == typeof(ushort)) return XmlConvert.ToString((ushort)value); - - throw new NotSupportedException("Cannot convert type " + type.FullName + " to a string using ToXmlString as it is not supported by XmlConvert"); + var output = value.ToXmlString(type); + return Attempt.Succeed(output); } - - /// - /// Returns an XmlSerialized safe string representation for the value and type - /// - /// - /// - /// - public static string ToXmlString(this object value) + catch (NotSupportedException ex) { - return value.ToXmlString(typeof (T)); + return Attempt.Fail(ex); } + } - private static string? GetEnumPropertyDebugString(object enumItem, int levels) + /// + /// Returns an XmlSerialized safe string representation for the value and type + /// + /// + /// + /// + public static string ToXmlString(this object value) => value.ToXmlString(typeof(T)); + + public static Guid AsGuid(this object value) => value is Guid guid ? guid : Guid.Empty; + + private static string? GetEnumPropertyDebugString(object enumItem, int levels) + { + try { - try - { - return enumItem.ToDebugString(levels - 1); - } - catch (Exception) - { - return "[GetEnumPartException]"; - } + return enumItem.ToDebugString(levels - 1); } - - private static string? GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) + catch (Exception) { - try - { - return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); - } - catch (Exception) - { - return "[GetPropertyValueException]"; - } + return "[GetEnumPartException]"; } + } - public static Guid AsGuid(this object value) + private static string? GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) + { + try { - return value is Guid guid ? guid : Guid.Empty; + return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string NormalizeNumberDecimalSeparator(string s) + catch (Exception) { - var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; - return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + return "[GetPropertyValueException]"; } + } - // gets a converter for source, that can convert to target, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TypeConverter? GetCachedSourceTypeConverter(Type source, Type target) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string NormalizeNumberDecimalSeparator(string s) + { + var normalized = Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; + return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); + } + + // gets a converter for source, that can convert to target, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConverter? GetCachedSourceTypeConverter(Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + + if (InputTypeConverterCache.TryGetValue(key, out TypeConverter? typeConverter)) { - var key = new CompositeTypeTypeKey(source, target); - - if (InputTypeConverterCache.TryGetValue(key, out var typeConverter)) - { - return typeConverter; - } - - var converter = TypeDescriptor.GetConverter(source); - if (converter.CanConvertTo(target)) - { - return InputTypeConverterCache[key] = converter; - } - - InputTypeConverterCache[key] = null; - return null; + return typeConverter; } - // gets a converter for target, that can convert from source, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static TypeConverter? GetCachedTargetTypeConverter(Type source, Type target) + TypeConverter converter = TypeDescriptor.GetConverter(source); + if (converter.CanConvertTo(target)) { - var key = new CompositeTypeTypeKey(source, target); - - if (DestinationTypeConverterCache.TryGetValue(key, out var typeConverter)) - { - return typeConverter; - } - - var converter = TypeDescriptor.GetConverter(target); - if (converter.CanConvertFrom(source)) - { - return DestinationTypeConverterCache[key] = converter; - } - - DestinationTypeConverterCache[key] = null; - return null; + return InputTypeConverterCache[key] = converter; } - // gets the underlying type of a nullable type, or null if the type is not nullable - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Type? GetCachedGenericNullableType(Type type) + InputTypeConverterCache[key] = null; + return null; + } + + // gets a converter for target, that can convert from source, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TypeConverter? GetCachedTargetTypeConverter(Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + + if (DestinationTypeConverterCache.TryGetValue(key, out TypeConverter? typeConverter)) { - if (NullableGenericCache.TryGetValue(type, out var underlyingType)) - { - return underlyingType; - } - - if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - Type? underlying = Nullable.GetUnderlyingType(type); - return NullableGenericCache[type] = underlying; - } - - NullableGenericCache[type] = null; - return null; + return typeConverter; } - // gets an IConvertible from source to target type, or null if none exists - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool GetCachedCanAssign(object input, Type source, Type target) + TypeConverter converter = TypeDescriptor.GetConverter(target); + if (converter.CanConvertFrom(source)) { - var key = new CompositeTypeTypeKey(source, target); - if (AssignableTypeCache.TryGetValue(key, out var canConvert)) - { - return canConvert; - } - - // "object is" is faster than "Type.IsAssignableFrom. - // We can use it to very quickly determine whether true/false - if (input is IConvertible && target.IsAssignableFrom(source)) - { - return AssignableTypeCache[key] = true; - } - - return AssignableTypeCache[key] = false; + return DestinationTypeConverterCache[key] = converter; } - // determines whether a type can be converted to boolean - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool GetCachedCanConvertToBoolean(Type type) + DestinationTypeConverterCache[key] = null; + return null; + } + + // gets the underlying type of a nullable type, or null if the type is not nullable + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Type? GetCachedGenericNullableType(Type type) + { + if (NullableGenericCache.TryGetValue(type, out Type? underlyingType)) { - if (BoolConvertCache.TryGetValue(type, out var result)) - { - return result; - } - - if (CustomBooleanTypeConverter.CanConvertFrom(type)) - { - return BoolConvertCache[type] = true; - } - - return BoolConvertCache[type] = false; + return underlyingType; } + if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + Type? underlying = Nullable.GetUnderlyingType(type); + return NullableGenericCache[type] = underlying; + } + NullableGenericCache[type] = null; + return null; + } + + // gets an IConvertible from source to target type, or null if none exists + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool GetCachedCanAssign(object input, Type source, Type target) + { + var key = new CompositeTypeTypeKey(source, target); + if (AssignableTypeCache.TryGetValue(key, out var canConvert)) + { + return canConvert; + } + + // "object is" is faster than "Type.IsAssignableFrom. + // We can use it to very quickly determine whether true/false + if (input is IConvertible && target.IsAssignableFrom(source)) + { + return AssignableTypeCache[key] = true; + } + + return AssignableTypeCache[key] = false; + } + + // determines whether a type can be converted to boolean + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool GetCachedCanConvertToBoolean(Type type) + { + if (BoolConvertCache.TryGetValue(type, out var result)) + { + return result; + } + + if (CustomBooleanTypeConverter.CanConvertFrom(type)) + { + return BoolConvertCache[type] = true; + } + + return BoolConvertCache[type] = false; } } diff --git a/src/Umbraco.Core/Extensions/PasswordConfigurationExtensions.cs b/src/Umbraco.Core/Extensions/PasswordConfigurationExtensions.cs index 0c15da6bdb..61b284383a 100644 --- a/src/Umbraco.Core/Extensions/PasswordConfigurationExtensions.cs +++ b/src/Umbraco.Core/Extensions/PasswordConfigurationExtensions.cs @@ -1,41 +1,34 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class PasswordConfigurationExtensions { - public static class PasswordConfigurationExtensions - { - /// - /// Returns the configuration of the membership provider used to configure change password editors - /// - /// - /// - /// - public static IDictionary GetConfiguration( - this IPasswordConfiguration passwordConfiguration, - bool allowManuallyChangingPassword = false) + /// + /// Returns the configuration of the membership provider used to configure change password editors + /// + /// + /// + /// + public static IDictionary GetConfiguration( + this IPasswordConfiguration passwordConfiguration, + bool allowManuallyChangingPassword = false) => + new Dictionary { - return new Dictionary - { - {"minPasswordLength", passwordConfiguration.RequiredLength}, + { "minPasswordLength", passwordConfiguration.RequiredLength }, - // TODO: This doesn't make a ton of sense with asp.net identity and also there's a bunch of other settings - // that we can consider with IPasswordConfiguration, but these are currently still based on how membership providers worked. - {"minNonAlphaNumericChars", passwordConfiguration.GetMinNonAlphaNumericChars()}, + // TODO: This doesn't make a ton of sense with asp.net identity and also there's a bunch of other settings + // that we can consider with IPasswordConfiguration, but these are currently still based on how membership providers worked. + { "minNonAlphaNumericChars", passwordConfiguration.GetMinNonAlphaNumericChars() }, - // A flag to indicate if the current password box should be shown or not, only a user that has access to change other user/member passwords - // doesn't have to specify the current password for the user/member. A user changing their own password must specify their current password. - {"allowManuallyChangingPassword", allowManuallyChangingPassword}, - }; - } + // A flag to indicate if the current password box should be shown or not, only a user that has access to change other user/member passwords + // doesn't have to specify the current password for the user/member. A user changing their own password must specify their current password. + { "allowManuallyChangingPassword", allowManuallyChangingPassword }, + }; - public static int GetMinNonAlphaNumericChars(this IPasswordConfiguration passwordConfiguration) - { - return passwordConfiguration.RequireNonLetterOrDigit ? 2 : 0; - } - - } + public static int GetMinNonAlphaNumericChars(this IPasswordConfiguration passwordConfiguration) => + passwordConfiguration.RequireNonLetterOrDigit ? 2 : 0; } diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs index 48769bda2c..f7ad53d7d6 100644 --- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs @@ -1,1377 +1,1469 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class PublishedContentExtensions { - public static class PublishedContentExtensions + #region Name + + /// + /// Gets the name of the content item. + /// + /// The content item. + /// + /// + /// The specific culture to get the name for. If null is used the current culture is used (Default is + /// null). + /// + public static string? Name(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) { - #region Name - - /// - /// Gets the name of the content item. - /// - /// The content item. - /// - /// The specific culture to get the name for. If null is used the current culture is used (Default is null). - public static string? Name(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + if (content == null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture()) - return content.Cultures.TryGetValue(string.Empty, out var invariantInfos) ? invariantInfos.Name : null; - - // handle context culture for variant - if (culture == null) - culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - - // get - return culture != string.Empty && content.Cultures.TryGetValue(culture, out var infos) ? infos.Name : null; + throw new ArgumentNullException(nameof(content)); } - #endregion - - #region Url segment - - /// - /// Gets the URL segment of the content item. - /// - /// The content item. - /// - /// The specific culture to get the URL segment for. If null is used the current culture is used (Default is null). - public static string? UrlSegment(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture()) - return content.Cultures.TryGetValue("", out var invariantInfos) ? invariantInfos.UrlSegment : null; - - // handle context culture for variant - if (culture == null) - culture = variationContextAccessor?.VariationContext?.Culture ?? ""; - - // get - return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.UrlSegment : null; + return content.Cultures.TryGetValue(string.Empty, out PublishedCultureInfo? invariantInfos) + ? invariantInfos.Name + : null; } - #endregion - - #region Culture - - /// - /// Determines whether the content has a culture. - /// - /// Culture is case-insensitive. - public static bool HasCulture(this IPublishedContent content, string? culture) + // handle context culture for variant + if (culture == null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - return content.Cultures.ContainsKey(culture ?? string.Empty); + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - /// - /// Determines whether the content is invariant, or has a culture. - /// - /// Culture is case-insensitive. - public static bool IsInvariantOrHasCulture(this IPublishedContent content, string culture) - => !content.ContentType.VariesByCulture() || content.Cultures.ContainsKey(culture ?? ""); - - /// - /// Filters a sequence of to return invariant items, and items that are published for the specified culture. - /// - /// The content items. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is null). - internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - if (contents == null) throw new ArgumentNullException(nameof(contents)); + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.Name + : null; + } - culture = culture ?? variationContextAccessor.VariationContext?.Culture ?? ""; + #endregion - // either does not vary by culture, or has the specified culture - return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); - } + #region Url segment - /// - /// Gets the culture date of the content item. - /// - /// The content item. - /// - /// The specific culture to get the name for. If null is used the current culture is used (Default is null). - public static DateTime CultureDate(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + /// + /// Gets the URL segment of the content item. + /// + /// The content item. + /// + /// + /// The specific culture to get the URL segment for. If null is used the current culture is used + /// (Default is null). + /// + public static string? UrlSegment(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + { + if (content == null) { - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture()) - return content.UpdateDate; - - // handle context culture for variant - if (culture == null) - culture = variationContextAccessor?.VariationContext?.Culture ?? ""; - - // get - return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.Date : DateTime.MinValue; + throw new ArgumentNullException(nameof(content)); } - #endregion - - #region IsComposedOf - - /// - /// Gets a value indicating whether the content is of a content type composed of the given alias - /// - /// The content. - /// The content type alias. - /// A value indicating whether the content is of a content type composed of a content type identified by the alias. - public static bool IsComposedOf(this IPublishedContent content, string alias) + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) { - return content.ContentType.CompositionAliases.InvariantContains(alias); + return content.Cultures.TryGetValue(string.Empty, out PublishedCultureInfo? invariantInfos) + ? invariantInfos.UrlSegment + : null; } - #endregion - - #region Template - - /// - /// Returns the current template Alias - /// - /// Empty string if none is set. - public static string GetTemplateAlias(this IPublishedContent content, IFileService fileService) + // handle context culture for variant + if (culture == null) { - if (content.TemplateId.HasValue == false) - { - return string.Empty; - } - - var template = fileService.GetTemplate(content.TemplateId.Value); - return template?.Alias ?? string.Empty; + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, - WebRoutingSettings webRoutingSettings, int templateId) - { - return content.IsAllowedTemplate(contentTypeService, - webRoutingSettings.DisableAlternativeTemplates, - webRoutingSettings.ValidateAlternativeTemplates, templateId); - } - - public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, int templateId) - { - if (disableAlternativeTemplates) - return content.TemplateId == templateId; + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.UrlSegment + : null; + } - if (content.TemplateId == templateId || !validateAlternativeTemplates) - return true; + #endregion - var publishedContentContentType = contentTypeService.Get(content.ContentType.Id); - if (publishedContentContentType == null) - throw new NullReferenceException("No content type returned for published content (contentType='" + content.ContentType.Id + "')"); + #region IsComposedOf - return publishedContentContentType.IsAllowedTemplate(templateId); - - } - public static bool IsAllowedTemplate(this IPublishedContent content, IFileService fileService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias) - { - var template = fileService.GetTemplate(templateAlias); - return template != null && content.IsAllowedTemplate(contentTypeService, disableAlternativeTemplates, validateAlternativeTemplates, template.Id); - } + /// + /// Gets a value indicating whether the content is of a content type composed of the given alias + /// + /// The content. + /// The content type alias. + /// + /// A value indicating whether the content is of a content type composed of a content type identified by the + /// alias. + /// + public static bool IsComposedOf(this IPublishedContent content, string alias) => + content.ContentType.CompositionAliases.InvariantContains(alias); - #endregion - - #region HasValue, Value, Value - - /// - /// Gets a value indicating whether the content has a value for a property identified by its alias. - /// - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// A value indicating whether the content has a value for the property identified by the alias. - /// Returns true if HasValue is true, or a fallback strategy can provide a value. - public static bool HasValue(this IPublishedContent content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default) - { - var property = content.GetProperty(alias); + #endregion - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return true; + #region Axes: parent - // else let fallback try to get a value - return publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, null, out _, out _); - } + // Parent is native - /// - /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. - /// - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - public static object? Value(this IPublishedContent content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) + /// + /// Gets the parent of the content, of a given content type. + /// + /// The content type. + /// The content. + /// The parent of content, of the given content type, else null. + public static T? Parent(this IPublishedContent content) + where T : class, IPublishedContent + { + if (content == null) { - var property = content.GetProperty(alias); - - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return property.GetValue(culture, segment); - - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value, out property)) - return value; - - // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - return property?.GetValue(culture, segment); + throw new ArgumentNullException(nameof(content)); } - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type. - /// - /// The target property type. - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, converted to the specified type. - public static T? Value(this IPublishedContent content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) - { - var property = content.GetProperty(alias); - - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return property.Value(publishedValueFallback, culture, segment); - - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value, out property)) - return value; + return content.Parent as T; + } - // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property == null ? default : property.Value(publishedValueFallback, culture, segment); - } - - #endregion + #endregion - #region IsSomething: misc. + #region Url - /// - /// Determines whether the specified content is a specified content type. - /// - /// The content to determine content type of. - /// The alias of the content type to test against. - /// True if the content is of the specified content type; otherwise false. - public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias) + /// + /// Gets the url of the content item. + /// + /// + /// + /// If the content item is a document, then this method returns the url of the + /// document. If it is a media, then this methods return the media url for the + /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other + /// properties. + /// + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + /// + public static string Url(this IPublishedContent content, IPublishedUrlProvider publishedUrlProvider, string? culture = null, UrlMode mode = UrlMode.Default) + { + if (publishedUrlProvider == null) { - return content.ContentType.Alias.InvariantEquals(docTypeAlias); + throw new InvalidOperationException( + "Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); } - /// - /// Determines whether the specified content is a specified content type or it's derived types. - /// - /// The content to determine content type of. - /// The alias of the content type to test against. - /// When true, recurses up the content type tree to check inheritance; when false just calls IsDocumentType(this IPublishedContent content, string docTypeAlias). - /// True if the content is of the specified content type or a derived content type; otherwise false. - public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias, bool recursive) + switch (content.ContentType.ItemType) { - if (content.IsDocumentType(docTypeAlias)) - return true; - - return recursive && content.IsComposedOf(docTypeAlias); - } - - #endregion + case PublishedItemType.Content: + return publishedUrlProvider.GetUrl(content, mode, culture); - #region IsSomething: equality + case PublishedItemType.Media: + return publishedUrlProvider.GetMediaUrl(content, mode, culture); - public static bool IsEqual(this IPublishedContent content, IPublishedContent other) - { - return content.Id == other.Id; + default: + throw new NotSupportedException(); } + } - public static bool IsNotEqual(this IPublishedContent content, IPublishedContent other) - { - return content.IsEqual(other) == false; - } - - #endregion - - #region IsSomething: ancestors and descendants + #endregion - public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) - { - return other.Level < content.Level && content.Path.InvariantStartsWith(other.Path.EnsureEndsWith(',')); - } + #region Culture - public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) + /// + /// Determines whether the content has a culture. + /// + /// Culture is case-insensitive. + public static bool HasCulture(this IPublishedContent content, string? culture) + { + if (content == null) { - return content.Path.InvariantEquals(other.Path) || content.IsDescendant(other); + throw new ArgumentNullException(nameof(content)); } - public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) - { - return content.Level < other.Level && other.Path.InvariantStartsWith(content.Path.EnsureEndsWith(',')); - } + return content.Cultures.ContainsKey(culture ?? string.Empty); + } - public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) - { - return other.Path.InvariantEquals(content.Path) || content.IsAncestor(other); - } + /// + /// Determines whether the content is invariant, or has a culture. + /// + /// Culture is case-insensitive. + public static bool IsInvariantOrHasCulture(this IPublishedContent content, string culture) + => !content.ContentType.VariesByCulture() || content.Cultures.ContainsKey(culture ?? string.Empty); - #endregion - - #region Axes: ancestors, ancestors-or-self - - // as per XPath 1.0 specs �2.2, - // - the ancestor axis contains the ancestors of the context node; the ancestors of the context node consist - // of the parent of context node and the parent's parent and so on; thus, the ancestor axis will always - // include the root node, unless the context node is the root node. - // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, - // the ancestor axis will always include the root node. - // - // as per XPath 2.0 specs �3.2.1.1, - // - the ancestor axis is defined as the transitive closure of the parent axis; it contains the ancestors - // of the context node (the parent, the parent of the parent, and so on) - The ancestor axis includes the - // root node of the tree in which the context node is found, unless the context node is the root node. - // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, - // the ancestor-or-self axis will always include the root node. - // - // the ancestor and ancestor-or-self axis are reverse axes ie they contain the context node or nodes that - // are before the context node in document order. - // - // document order is defined by �2.4.1 as: - // - the root node is the first node. - // - every node occurs before all of its children and descendants. - // - the relative order of siblings is the order in which they occur in the children property of their parent node. - // - children and descendants occur before following siblings. - - /// - /// Gets the ancestors of the content. - /// - /// The content. - /// The ancestors of the content, in down-top order. - /// Does not consider the content itself. - public static IEnumerable Ancestors(this IPublishedContent content) + /// + /// Filters a sequence of to return invariant items, and items that are published for + /// the specified culture. + /// + /// The content items. + /// + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null). + /// + internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent + { + if (contents == null) { - return content.AncestorsOrSelf(false, null); + throw new ArgumentNullException(nameof(contents)); } - /// - /// Gets the ancestors of the content, at a level lesser or equal to a specified level. - /// - /// The content. - /// The level. - /// The ancestors of the content, at a level lesser or equal to the specified level, in down-top order. - /// Does not consider the content itself. Only content that are "high enough" in the tree are returned. - public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) - { - return content.AncestorsOrSelf(false, n => n.Level <= maxLevel); - } + culture = culture ?? variationContextAccessor.VariationContext?.Culture ?? string.Empty; - /// - /// Gets the ancestors of the content, of a specified content type. - /// - /// The content. - /// The content type. - /// The ancestors of the content, of the specified content type, in down-top order. - /// Does not consider the content itself. Returns all ancestors, of the specified content type. - public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) - { - return content.AncestorsOrSelf(false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } + // either does not vary by culture, or has the specified culture + return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); + } - /// - /// Gets the ancestors of the content, of a specified content type. - /// - /// The content type. - /// The content. - /// The ancestors of the content, of the specified content type, in down-top order. - /// Does not consider the content itself. Returns all ancestors, of the specified content type. - public static IEnumerable Ancestors(this IPublishedContent content) - where T : class, IPublishedContent + /// + /// Gets the culture date of the content item. + /// + /// The content item. + /// + /// + /// The specific culture to get the name for. If null is used the current culture is used (Default is + /// null). + /// + public static DateTime CultureDate(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) { - return content.Ancestors().OfType(); + return content.UpdateDate; } - /// - /// Gets the ancestors of the content, at a level lesser or equal to a specified level, and of a specified content type. - /// - /// The content type. - /// The content. - /// The level. - /// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified - /// content type, in down-top order. - /// Does not consider the content itself. Only content that are "high enough" in the trees, and of the - /// specified content type, are returned. - public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) - where T : class, IPublishedContent + // handle context culture for variant + if (culture == null) { - return content.Ancestors(maxLevel).OfType(); + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } - /// - /// Gets the content and its ancestors. - /// - /// The content. - /// The content and its ancestors, in down-top order. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content) - { - return content.AncestorsOrSelf(true, null); - } + // get + return culture != string.Empty && content.Cultures.TryGetValue(culture, out PublishedCultureInfo? infos) + ? infos.Date + : DateTime.MinValue; + } - /// - /// Gets the content and its ancestors, at a level lesser or equal to a specified level. - /// - /// The content. - /// The level. - /// The content and its ancestors, at a level lesser or equal to the specified level, - /// in down-top order. - /// Only content that are "high enough" in the tree are returned. So it may or may not begin - /// with the content itself, depending on its level. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) - { - return content.AncestorsOrSelf(true, n => n.Level <= maxLevel); - } + #endregion - /// - /// Gets the content and its ancestors, of a specified content type. - /// - /// The content. - /// The content type. - /// The content and its ancestors, of the specified content type, in down-top order. - /// May or may not begin with the content itself, depending on its content type. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) - { - return content.AncestorsOrSelf(true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } + #region Template - /// - /// Gets the content and its ancestors, of a specified content type. - /// - /// The content type. - /// The content. - /// The content and its ancestors, of the specified content type, in down-top order. - /// May or may not begin with the content itself, depending on its content type. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content) - where T : class, IPublishedContent + /// + /// Returns the current template Alias + /// + /// Empty string if none is set. + public static string GetTemplateAlias(this IPublishedContent content, IFileService fileService) + { + if (content.TemplateId.HasValue == false) { - return content.AncestorsOrSelf().OfType(); + return string.Empty; } - /// - /// Gets the content and its ancestor, at a lever lesser or equal to a specified level, and of a specified content type. - /// - /// The content type. - /// The content. - /// The level. - /// The content and its ancestors, at a level lesser or equal to the specified level, and of the specified - /// content type, in down-top order. - /// May or may not begin with the content itself, depending on its level and content type. - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) - where T : class, IPublishedContent - { - return content.AncestorsOrSelf(maxLevel).OfType(); - } + ITemplate? template = fileService.GetTemplate(content.TemplateId.Value); + return template?.Alias ?? string.Empty; + } - /// - /// Gets the ancestor of the content, ie its parent. - /// - /// The content. - /// The ancestor of the content. - /// This method is here for consistency purposes but does not make much sense. - public static IPublishedContent? Ancestor(this IPublishedContent content) - { - return content.Parent; - } + public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, WebRoutingSettings webRoutingSettings, int templateId) => + content.IsAllowedTemplate(contentTypeService, webRoutingSettings.DisableAlternativeTemplates, webRoutingSettings.ValidateAlternativeTemplates, templateId); - /// - /// Gets the nearest ancestor of the content, at a lever lesser or equal to a specified level. - /// - /// The content. - /// The level. - /// The nearest (in down-top order) ancestor of the content, at a level lesser or equal to the specified level. - /// Does not consider the content itself. May return null. - public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel) + public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, int templateId) + { + if (disableAlternativeTemplates) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.Level <= maxLevel); + return content.TemplateId == templateId; } - /// - /// Gets the nearest ancestor of the content, of a specified content type. - /// - /// The content. - /// The content type alias. - /// The nearest (in down-top order) ancestor of the content, of the specified content type. - /// Does not consider the content itself. May return null. - public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias) + if (content.TemplateId == templateId || !validateAlternativeTemplates) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + return true; } - /// - /// Gets the nearest ancestor of the content, of a specified content type. - /// - /// The content type. - /// The content. - /// The nearest (in down-top order) ancestor of the content, of the specified content type. - /// Does not consider the content itself. May return null. - public static T? Ancestor(this IPublishedContent content) - where T : class, IPublishedContent - { - return content.Ancestors().FirstOrDefault(); - } + IContentType? publishedContentContentType = contentTypeService.Get(content.ContentType.Id); + if (publishedContentContentType == null) + { + throw new NullReferenceException("No content type returned for published content (contentType='" + + content.ContentType.Id + "')"); + } + + return publishedContentContentType.IsAllowedTemplate(templateId); + } + + public static bool IsAllowedTemplate(this IPublishedContent content, IFileService fileService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias) + { + ITemplate? template = fileService.GetTemplate(templateAlias); + return template != null && content.IsAllowedTemplate(contentTypeService, disableAlternativeTemplates, validateAlternativeTemplates, template.Id); + } - /// - /// Gets the nearest ancestor of the content, at the specified level and of the specified content type. - /// - /// The content type. - /// The content. - /// The level. - /// The ancestor of the content, at the specified level and of the specified content type. - /// Does not consider the content itself. If the ancestor at the specified level is - /// not of the specified type, returns null. - public static T? Ancestor(this IPublishedContent content, int maxLevel) - where T : class, IPublishedContent - { - return content.Ancestors(maxLevel).FirstOrDefault(); - } + #endregion - /// - /// Gets the content or its nearest ancestor. - /// - /// The content. - /// The content. - /// This method is here for consistency purposes but does not make much sense. - public static IPublishedContent AncestorOrSelf(this IPublishedContent content) - { - return content; - } + #region HasValue, Value, Value - /// - /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level. - /// - /// The content. - /// The level. - /// The content or its nearest (in down-top order) ancestor, at a level lesser or equal to the specified level. - /// May or may not return the content itself depending on its level. May return null. - public static IPublishedContent? AncestorOrSelf(this IPublishedContent content, int maxLevel) - { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.Level <= maxLevel); - } + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// A value indicating whether the content has a value for the property identified by the alias. + /// Returns true if HasValue is true, or a fallback strategy can provide a value. + public static bool HasValue(this IPublishedContent content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default) + { + IPublishedProperty? property = content.GetProperty(alias); - /// - /// Gets the content or its nearest ancestor, of a specified content type. - /// - /// The content. - /// The content type. - /// The content or its nearest (in down-top order) ancestor, of the specified content type. - /// May or may not return the content itself depending on its content type. May return null. - public static IPublishedContent? AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + return true; } - /// - /// Gets the content or its nearest ancestor, of a specified content type. - /// - /// The content type. - /// The content. - /// The content or its nearest (in down-top order) ancestor, of the specified content type. - /// May or may not return the content itself depending on its content type. May return null. - public static T? AncestorOrSelf(this IPublishedContent content) - where T : class, IPublishedContent - { - return content.AncestorsOrSelf().FirstOrDefault(); - } + // else let fallback try to get a value + return publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, null, out _, out _); + } - /// - /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level, and of a specified content type. - /// - /// The content type. - /// The content. - /// The level. - /// - public static T? AncestorOrSelf(this IPublishedContent content, int maxLevel) - where T : class, IPublishedContent - { - return content.AncestorsOrSelf(maxLevel).FirstOrDefault(); - } + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + public static object? Value( + this IPublishedContent content, + IPublishedValueFallback publishedValueFallback, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + object? defaultValue = default) + { + IPublishedProperty? property = content.GetProperty(alias); - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func? func) + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) { - var ancestorsOrSelf = content.EnumerateAncestors(orSelf); - return func == null ? ancestorsOrSelf : ancestorsOrSelf.Where(func); + return property.GetValue(culture, segment); } - /// - /// Enumerates ancestors of the content, bottom-up. - /// - /// The content. - /// Indicates whether the content should be included. - /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). - internal static IEnumerable EnumerateAncestors(this IPublishedContent? content, bool orSelf) + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value, out property)) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (orSelf) yield return content; - while ((content = content.Parent) != null) - yield return content; + return value; } - #endregion - - #region Axes: breadcrumbs - - /// - /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . - /// - /// The content. - /// Indicates whether the specified content should be included. - /// - /// The breadcrumbs (ancestors and self, top to bottom) for the specified . - /// - public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) - { - return content.AncestorsOrSelf(andSelf, null).Reverse(); - } - - /// - /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . - /// - /// The content. - /// The minimum level. - /// Indicates whether the specified content should be included. - /// - /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . - /// - public static IEnumerable Breadcrumbs(this IPublishedContent content, int minLevel, bool andSelf = true) - { - return content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); - } - - /// - /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . - /// - /// The root content type. - /// The content. - /// Indicates whether the specified content should be included. - /// - /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . - /// - public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) - where T : class, IPublishedContent - { - static IEnumerable TakeUntil(IEnumerable source, Func predicate) - { - foreach (var item in source) - { - yield return item; - if (predicate(item)) - { - yield break; - } - } - } - - return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); - } - - #endregion - - #region Axes: descendants, descendants-or-self - - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// - public static IEnumerable DescendantsOrSelfOfType(this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string docTypeAlias, string? culture = null) - { - return parentNodes.SelectMany(x => x.DescendantsOrSelfOfType(variationContextAccessor, docTypeAlias, culture)); - } - - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// - public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return parentNodes.SelectMany(x => x.DescendantsOrSelf(variationContextAccessor, culture)); - } - - - // as per XPath 1.0 specs �2.2, - // - the descendant axis contains the descendants of the context node; a descendant is a child or a child of a child and so on; thus - // the descendant axis never contains attribute or namespace nodes. - // - the descendant-or-self axis contains the context node and the descendants of the context node. - // - // as per XPath 2.0 specs �3.2.1.1, - // - the descendant axis is defined as the transitive closure of the child axis; it contains the descendants of the context node (the - // children, the children of the children, and so on). - // - the descendant-or-self axis contains the context node and the descendants of the context node. - // - // the descendant and descendant-or-self axis are forward axes ie they contain the context node or nodes that are after the context - // node in document order. - // - // document order is defined by �2.4.1 as: - // - the root node is the first node. - // - every node occurs before all of its children and descendants. - // - the relative order of siblings is the order in which they occur in the children property of their parent node. - // - children and descendants occur before following siblings. - - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, false, null, culture); - } - - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, false, p => p.Level >= level, culture); - } - - public static IEnumerable DescendantsOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); - } - - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.Descendants(variationContextAccessor, culture).OfType(); - } - - public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent - { - return content.Descendants(variationContextAccessor, level, culture).OfType(); - } - - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, true, null, culture); - } - - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, true, p => p.Level >= level, culture); - } - - public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string ?culture = null) - { - return content.DescendantsOrSelf(variationContextAccessor, true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); - } - - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.DescendantsOrSelf(variationContextAccessor, culture).OfType(); - } - - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent - { - return content.DescendantsOrSelf(variationContextAccessor, level, culture).OfType(); - } - - public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.Children(variationContextAccessor, culture)?.FirstOrDefault(); - } - - public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x.Level == level); - } - - public static IPublishedContent? DescendantOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } - - public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x is T) as T; - } - - public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent - { - return content.Descendant(variationContextAccessor, level, culture) as T; - } - - public static IPublishedContent DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content; - } - - public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x.Level == level); - } - - public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); - } - - public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x is T) as T; - } - - public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent - { - return content.DescendantOrSelf(variationContextAccessor, level, culture) as T; - } - - internal static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, Func? func, string? culture = null) - { - return content.EnumerateDescendants(variationContextAccessor, orSelf, culture).Where(x => func == null || func(x)); - } - - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, string? culture = null) - { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (orSelf) yield return content; - - var children = content.Children(variationContextAccessor, culture); - if (children is not null) - { - foreach (var desc in children.SelectMany(x => x.EnumerateDescendants(variationContextAccessor, culture))) - yield return desc; - } - } + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) + return property?.GetValue(culture, segment); + } - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type. + public static T? Value( + this IPublishedContent content, + IPublishedValueFallback publishedValueFallback, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + T? defaultValue = default) + { + IPublishedProperty? property = content.GetProperty(alias); + + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) + { + return property.Value(publishedValueFallback, culture, segment); + } + + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out T? value, out property)) + { + return value; + } + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(publishedValueFallback, culture, segment); + } + + #endregion + + #region IsSomething: misc. + + /// + /// Determines whether the specified content is a specified content type. + /// + /// The content to determine content type of. + /// The alias of the content type to test against. + /// True if the content is of the specified content type; otherwise false. + public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias) => + content.ContentType.Alias.InvariantEquals(docTypeAlias); + + /// + /// Determines whether the specified content is a specified content type or it's derived types. + /// + /// The content to determine content type of. + /// The alias of the content type to test against. + /// + /// When true, recurses up the content type tree to check inheritance; when false just calls + /// IsDocumentType(this IPublishedContent content, string docTypeAlias). + /// + /// True if the content is of the specified content type or a derived content type; otherwise false. + public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias, bool recursive) + { + if (content.IsDocumentType(docTypeAlias)) + { + return true; + } + + return recursive && content.IsComposedOf(docTypeAlias); + } + + #endregion + + #region IsSomething: equality + + public static bool IsEqual(this IPublishedContent content, IPublishedContent other) => content.Id == other.Id; + + public static bool IsNotEqual(this IPublishedContent content, IPublishedContent other) => + content.IsEqual(other) == false; + + #endregion + + #region IsSomething: ancestors and descendants + + public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) => + other.Level < content.Level && content.Path.InvariantStartsWith(other.Path.EnsureEndsWith(',')); + + public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) => + content.Path.InvariantEquals(other.Path) || content.IsDescendant(other); + + public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) => + content.Level < other.Level && other.Path.InvariantStartsWith(content.Path.EnsureEndsWith(',')); + + public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) => + other.Path.InvariantEquals(content.Path) || content.IsAncestor(other); + + #endregion + + #region Axes: ancestors, ancestors-or-self + + // as per XPath 1.0 specs �2.2, + // - the ancestor axis contains the ancestors of the context node; the ancestors of the context node consist + // of the parent of context node and the parent's parent and so on; thus, the ancestor axis will always + // include the root node, unless the context node is the root node. + // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, + // the ancestor axis will always include the root node. + // + // as per XPath 2.0 specs �3.2.1.1, + // - the ancestor axis is defined as the transitive closure of the parent axis; it contains the ancestors + // of the context node (the parent, the parent of the parent, and so on) - The ancestor axis includes the + // root node of the tree in which the context node is found, unless the context node is the root node. + // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, + // the ancestor-or-self axis will always include the root node. + // + // the ancestor and ancestor-or-self axis are reverse axes ie they contain the context node or nodes that + // are before the context node in document order. + // + // document order is defined by �2.4.1 as: + // - the root node is the first node. + // - every node occurs before all of its children and descendants. + // - the relative order of siblings is the order in which they occur in the children property of their parent node. + // - children and descendants occur before following siblings. + + /// + /// Gets the ancestors of the content. + /// + /// The content. + /// The ancestors of the content, in down-top order. + /// Does not consider the content itself. + public static IEnumerable Ancestors(this IPublishedContent content) => + content.AncestorsOrSelf(false, null); + + /// + /// Gets the ancestors of the content, at a level lesser or equal to a specified level. + /// + /// The content. + /// The level. + /// The ancestors of the content, at a level lesser or equal to the specified level, in down-top order. + /// Does not consider the content itself. Only content that are "high enough" in the tree are returned. + public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) => + content.AncestorsOrSelf(false, n => n.Level <= maxLevel); + + /// + /// Gets the ancestors of the content, of a specified content type. + /// + /// The content. + /// The content type. + /// The ancestors of the content, of the specified content type, in down-top order. + /// Does not consider the content itself. Returns all ancestors, of the specified content type. + public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) => + content.AncestorsOrSelf(false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + /// + /// Gets the ancestors of the content, of a specified content type. + /// + /// The content type. + /// The content. + /// The ancestors of the content, of the specified content type, in down-top order. + /// Does not consider the content itself. Returns all ancestors, of the specified content type. + public static IEnumerable Ancestors(this IPublishedContent content) + where T : class, IPublishedContent => + content.Ancestors().OfType(); + + /// + /// Gets the ancestors of the content, at a level lesser or equal to a specified level, and of a specified content + /// type. + /// + /// The content type. + /// The content. + /// The level. + /// + /// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified + /// content type, in down-top order. + /// + /// + /// Does not consider the content itself. Only content that are "high enough" in the trees, and of the + /// specified content type, are returned. + /// + public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) + where T : class, IPublishedContent => + content.Ancestors(maxLevel).OfType(); + + /// + /// Gets the content and its ancestors. + /// + /// The content. + /// The content and its ancestors, in down-top order. + public static IEnumerable AncestorsOrSelf(this IPublishedContent content) => + content.AncestorsOrSelf(true, null); + + /// + /// Gets the content and its ancestors, at a level lesser or equal to a specified level. + /// + /// The content. + /// The level. + /// + /// The content and its ancestors, at a level lesser or equal to the specified level, + /// in down-top order. + /// + /// + /// Only content that are "high enough" in the tree are returned. So it may or may not begin + /// with the content itself, depending on its level. + /// + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) => + content.AncestorsOrSelf(true, n => n.Level <= maxLevel); + + /// + /// Gets the content and its ancestors, of a specified content type. + /// + /// The content. + /// The content type. + /// The content and its ancestors, of the specified content type, in down-top order. + /// May or may not begin with the content itself, depending on its content type. + public static IEnumerable + AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) => + content.AncestorsOrSelf(true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + /// + /// Gets the content and its ancestors, of a specified content type. + /// + /// The content type. + /// The content. + /// The content and its ancestors, of the specified content type, in down-top order. + /// May or may not begin with the content itself, depending on its content type. + public static IEnumerable AncestorsOrSelf(this IPublishedContent content) + where T : class, IPublishedContent => + content.AncestorsOrSelf().OfType(); + + /// + /// Gets the content and its ancestor, at a lever lesser or equal to a specified level, and of a specified content + /// type. + /// + /// The content type. + /// The content. + /// The level. + /// + /// The content and its ancestors, at a level lesser or equal to the specified level, and of the specified + /// content type, in down-top order. + /// + /// May or may not begin with the content itself, depending on its level and content type. + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) + where T : class, IPublishedContent => + content.AncestorsOrSelf(maxLevel).OfType(); + + /// + /// Gets the ancestor of the content, ie its parent. + /// + /// The content. + /// The ancestor of the content. + /// This method is here for consistency purposes but does not make much sense. + public static IPublishedContent? Ancestor(this IPublishedContent content) => content.Parent; + + /// + /// Gets the nearest ancestor of the content, at a lever lesser or equal to a specified level. + /// + /// The content. + /// The level. + /// The nearest (in down-top order) ancestor of the content, at a level lesser or equal to the specified level. + /// Does not consider the content itself. May return null. + public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel) => + content.EnumerateAncestors(false).FirstOrDefault(x => x.Level <= maxLevel); + + /// + /// Gets the nearest ancestor of the content, of a specified content type. + /// + /// The content. + /// The content type alias. + /// The nearest (in down-top order) ancestor of the content, of the specified content type. + /// Does not consider the content itself. May return null. + public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias) => content + .EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + /// + /// Gets the nearest ancestor of the content, of a specified content type. + /// + /// The content type. + /// The content. + /// The nearest (in down-top order) ancestor of the content, of the specified content type. + /// Does not consider the content itself. May return null. + public static T? Ancestor(this IPublishedContent content) + where T : class, IPublishedContent => + content.Ancestors().FirstOrDefault(); + + /// + /// Gets the nearest ancestor of the content, at the specified level and of the specified content type. + /// + /// The content type. + /// The content. + /// The level. + /// The ancestor of the content, at the specified level and of the specified content type. + /// + /// Does not consider the content itself. If the ancestor at the specified level is + /// not of the specified type, returns null. + /// + public static T? Ancestor(this IPublishedContent content, int maxLevel) + where T : class, IPublishedContent => + content.Ancestors(maxLevel).FirstOrDefault(); + + /// + /// Gets the content or its nearest ancestor. + /// + /// The content. + /// The content. + /// This method is here for consistency purposes but does not make much sense. + public static IPublishedContent AncestorOrSelf(this IPublishedContent content) => content; + + /// + /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level. + /// + /// The content. + /// The level. + /// The content or its nearest (in down-top order) ancestor, at a level lesser or equal to the specified level. + /// May or may not return the content itself depending on its level. May return null. + public static IPublishedContent? AncestorOrSelf(this IPublishedContent content, int maxLevel) => + content.EnumerateAncestors(true).FirstOrDefault(x => x.Level <= maxLevel); + + /// + /// Gets the content or its nearest ancestor, of a specified content type. + /// + /// The content. + /// The content type. + /// The content or its nearest (in down-top order) ancestor, of the specified content type. + /// May or may not return the content itself depending on its content type. May return null. + public static IPublishedContent? AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) => content + .EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + /// + /// Gets the content or its nearest ancestor, of a specified content type. + /// + /// The content type. + /// The content. + /// The content or its nearest (in down-top order) ancestor, of the specified content type. + /// May or may not return the content itself depending on its content type. May return null. + public static T? AncestorOrSelf(this IPublishedContent content) + where T : class, IPublishedContent => + content.AncestorsOrSelf().FirstOrDefault(); + + /// + /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level, and of a specified + /// content type. + /// + /// The content type. + /// The content. + /// The level. + /// + public static T? AncestorOrSelf(this IPublishedContent content, int maxLevel) + where T : class, IPublishedContent => + content.AncestorsOrSelf(maxLevel).FirstOrDefault(); + + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func? func) + { + IEnumerable ancestorsOrSelf = content.EnumerateAncestors(orSelf); + return func == null ? ancestorsOrSelf : ancestorsOrSelf.Where(func); + } + + /// + /// Enumerates ancestors of the content, bottom-up. + /// + /// The content. + /// Indicates whether the content should be included. + /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). + internal static IEnumerable EnumerateAncestors(this IPublishedContent? content, bool orSelf) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (orSelf) { yield return content; - var children = content.Children(variationContextAccessor, culture); - if (children is not null) - { - foreach (var desc in children.SelectMany(x => x.EnumerateDescendants(variationContextAccessor, culture))) - yield return desc; - } } - #endregion - - #region Axes: children - - /// - /// Gets the children of the content item. - /// - /// The content item. - /// - /// - /// The specific culture to get the URL children for. Default is null which will use the current culture in - /// - /// - /// Gets children that are available for the specified culture. - /// Children are sorted by their sortOrder. - /// - /// For culture, - /// if null is used the current culture is used. - /// If an empty string is used only invariant children are returned. - /// If "*" is used all children are returned. - /// - /// - /// If a variant culture is specified or there is a current culture in the then the Children returned - /// will include both the variant children matching the culture AND the invariant children because the invariant children flow with the current culture. - /// However, if an empty string is specified only invariant children are returned. - /// - /// - public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + while ((content = content.Parent) != null) { - // handle context culture for variant - if (culture == null) - culture = variationContextAccessor?.VariationContext?.Culture ?? ""; - - var children = content.ChildrenForAllCultures; - return culture == "*" - ? children - : children?.Where(x => x.IsInvariantOrHasCulture(culture)); + yield return content; } - - /// - /// Gets the children of the content, filtered by a predicate. - /// - /// The content. - /// Published snapshot instance - /// The predicate. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content, filtered by the predicate. - /// - /// Children are sorted by their sortOrder. - /// - public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) - { - return content.Children(variationContextAccessor, culture)?.Where(predicate); - } - - /// - /// Gets the children of the content, of any of the specified types. - /// - /// The content. - /// Published snapshot instance - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The content type alias. - /// The children of the content, of any of the specified types. - public static IEnumerable? ChildrenOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? contentTypeAlias, string? culture = null) - { - return content.Children(variationContextAccessor, x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); - } - - /// - /// Gets the children of the content, of a given content type. - /// - /// The content type. - /// The content. - /// Published snapshot instance - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of content, of the given content type. - /// - /// Children are sorted by their sortOrder. - /// - public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.Children(variationContextAccessor, culture)?.OfType(); - } - - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.Children(variationContextAccessor, culture)?.FirstOrDefault(); - } - - /// - /// Gets the first child of the content, of a given content type. - /// - public static IPublishedContent? FirstChildOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture)?.FirstOrDefault(); - } - - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) - { - return content.Children(variationContextAccessor, predicate, culture)?.FirstOrDefault(); - } - - public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Guid uniqueId, string? culture = null) - { - return content.Children(variationContextAccessor, x => x.Key == uniqueId, culture)?.FirstOrDefault(); - } - - public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.Children(variationContextAccessor, culture)?.FirstOrDefault(); - } - - public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) - where T : class, IPublishedContent - { - return content.Children(variationContextAccessor, culture)?.FirstOrDefault(predicate); - } - - #endregion - - #region Axes: parent - - // Parent is native - - /// - /// Gets the parent of the content, of a given content type. - /// - /// The content type. - /// The content. - /// The parent of content, of the given content type, else null. - public static T? Parent(this IPublishedContent content) - where T : class, IPublishedContent - { - if (content == null) throw new ArgumentNullException(nameof(content)); - return content.Parent as T; - } - - #endregion - - #region Axes: siblings - - /// - /// Gets the siblings of the content. - /// - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? Siblings(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture)?.Where(x => x.Id != content.Id); - } - - /// - /// Gets the siblings of the content, of a given content type. - /// - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The content type alias. - /// The siblings of the content, of the given content type. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? SiblingsOfType(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return SiblingsAndSelfOfType(content, publishedSnapshot, variationContextAccessor, contentTypeAlias, culture)?.Where(x => x.Id != content.Id); - } - - /// - /// Gets the siblings of the content, of a given content type. - /// - /// The content type. - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content, of the given content type. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable? Siblings(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture)?.Where(x => x.Id != content.Id); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position. - /// - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content including the node itself. - public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) - { - return content.Parent != null - ? content.Parent.Children(variationContextAccessor, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture).WhereIsInvariantOrHasCulture(variationContextAccessor, culture); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. - /// - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The content type alias. - /// The siblings of the content including the node itself, of the given content type. - public static IEnumerable? SiblingsAndSelfOfType(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) - { - return content.Parent != null - ? content.Parent.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture).OfTypes(contentTypeAlias).WhereIsInvariantOrHasCulture(variationContextAccessor, culture); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. - /// - /// The content type. - /// The content. - /// Published snapshot instance - /// Variation context accessor. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The siblings of the content including the node itself, of the given content type. - public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - return content.Parent != null - ? content.Parent.Children(variationContextAccessor, culture) - : publishedSnapshot?.Content?.GetAtRoot(culture).OfType().WhereIsInvariantOrHasCulture(variationContextAccessor, culture); - } - - #endregion - - #region Axes: custom - - /// - /// Gets the root content (ancestor or self at level 1) for the specified . - /// - /// The content. - /// - /// The root content (ancestor or self at level 1) for the specified . - /// - /// - /// This is the same as calling with maxLevel set to 1. - /// - public static IPublishedContent? Root(this IPublishedContent content) - { - return content.AncestorOrSelf(1); - } - - /// - /// Gets the root content (ancestor or self at level 1) for the specified if it's of the specified content type . - /// - /// The content type. - /// The content. - /// - /// The root content (ancestor or self at level 1) for the specified of content type . - /// - /// - /// This is the same as calling with maxLevel set to 1. - /// - public static T? Root(this IPublishedContent content) - where T : class, IPublishedContent - { - return content.AncestorOrSelf(1); - } - - #endregion - - #region Writer and creator - - public static string? GetCreatorName(this IPublishedContent content, IUserService userService) - { - var user = userService.GetProfileById(content.CreatorId); - return user?.Name; - } - - public static string? GetWriterName(this IPublishedContent content, IUserService userService) - { - var user = userService.GetProfileById(content.WriterId); - return user?.Name; - } - - #endregion - - #region Url - - /// - /// Gets the url of the content item. - /// - /// - /// If the content item is a document, then this method returns the url of the - /// document. If it is a media, then this methods return the media url for the - /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other - /// properties. - /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. - /// - public static string Url(this IPublishedContent content, IPublishedUrlProvider publishedUrlProvider, string? culture = null, UrlMode mode = UrlMode.Default) - { - if (publishedUrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); - - switch (content.ContentType.ItemType) - { - case PublishedItemType.Content: - return publishedUrlProvider.GetUrl(content, mode, culture); - - case PublishedItemType.Media: - return publishedUrlProvider.GetMediaUrl(content, mode, culture, Constants.Conventions.Media.File); - - default: - throw new NotSupportedException(); - } - } - - #endregion - - #region Axes: children - - /// - /// Gets the children of the content in a DataTable. - /// - /// The content. - /// Variation context accessor. - /// The content type service. - /// The media type service. - /// The member type service. - /// The published url provider. - /// An optional content type alias. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content. - public static DataTable ChildrenAsTable(this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, IContentTypeService contentTypeService, - IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, - IPublishedUrlProvider publishedUrlProvider, string contentTypeAliasFilter = "", string? culture = null) - => GenerateDataTable(content, variationContextAccessor, contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); - - /// - /// Gets the children of the content in a DataTable. - /// - /// The content. - /// Variation context accessor. - /// The content type service. - /// The media type service. - /// The member type service. - /// The published url provider. - /// An optional content type alias. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content. - private static DataTable GenerateDataTable(IPublishedContent content, - IVariationContextAccessor variationContextAccessor, IContentTypeService contentTypeService, - IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, - IPublishedUrlProvider publishedUrlProvider, string contentTypeAliasFilter = "", string? culture = null) - { - var firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() - ? content.Children(variationContextAccessor, culture)?.Any() ?? false - ? content.Children(variationContextAccessor, culture)?.ElementAt(0) - : null - : content.Children(variationContextAccessor, culture)?.FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); - if (firstNode == null) - return new DataTable(); //no children found - - //use new utility class to create table so that we don't have to maintain code in many places, just one - var dt = DataTableExtensions.GenerateDataTable( - //pass in the alias of the first child node since this is the node type we're rendering headers for - firstNode.ContentType.Alias, - //pass in the callback to extract the Dictionary of all defined aliases to their names - alias => GetPropertyAliasesAndNames(contentTypeService, mediaTypeService, memberTypeService, alias), - //pass in a callback to populate the datatable, yup its a bit ugly but it's already legacy and we just want to maintain code in one place. - () => - { - //create all row data - var tableData = DataTableExtensions.CreateTableData(); - var children = content.Children(variationContextAccessor)?.OrderBy(x => x.SortOrder); - if (children is not null) - { - //loop through each child and create row data for it - foreach (var n in children) - { - if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) - { - if (n.ContentType.Alias.InvariantEquals(contentTypeAliasFilter) == false) - continue; //skip this one, it doesn't match the filter - } - - var standardVals = new Dictionary - { - { "Id", n.Id }, - { "NodeName", n.Name(variationContextAccessor) }, - { "NodeTypeAlias", n.ContentType.Alias }, - { "CreateDate", n.CreateDate }, - { "UpdateDate", n.UpdateDate }, - { "CreatorId", n.CreatorId}, - { "WriterId", n.WriterId }, - { "Url", n.Url(publishedUrlProvider) } - }; - - var userVals = new Dictionary(); - var properties = n.Properties?.Where(p => p.GetSourceValue() is not null) ?? Array.Empty(); - foreach (var p in properties) - { - // probably want the "object value" of the property here... - userVals[p.Alias] = p.GetValue(); - } - //add the row data - DataTableExtensions.AddRowData(tableData, standardVals, userVals); - } - } - - return tableData; - }); - return dt; - } - - #endregion - - #region PropertyAliasesAndNames - - private static Func>? _getPropertyAliasesAndNames; - - /// - /// This is used only for unit tests to set the delegate to look up aliases/names dictionary of a content type - /// - internal static Func> GetPropertyAliasesAndNames - { - get => _getPropertyAliasesAndNames ?? GetAliasesAndNames; - set => _getPropertyAliasesAndNames = value; - } - - private static Dictionary GetAliasesAndNames(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, string alias) - { - var type = contentTypeService.Get(alias) - ?? mediaTypeService.Get(alias) - ?? (IContentTypeBase?)memberTypeService.Get(alias); - var fields = GetAliasesAndNames(type); - - // ensure the standard fields are there - var stdFields = new Dictionary - { - {"Id", "Id"}, - {"NodeName", "NodeName"}, - {"NodeTypeAlias", "NodeTypeAlias"}, - {"CreateDate", "CreateDate"}, - {"UpdateDate", "UpdateDate"}, - {"CreatorId", "CreatorId"}, - {"WriterId", "WriterId"}, - {"Url", "Url"} - }; - - foreach (var field in stdFields.Where(x => fields.ContainsKey(x.Key) == false)) - { - fields[field.Key] = field.Value; - } - - return fields; - } - - private static Dictionary GetAliasesAndNames(IContentTypeBase? contentType) => contentType?.PropertyTypes.ToDictionary(x => x.Alias, x => x.Name) ?? new Dictionary(); - - #endregion } + + #endregion + + #region Axes: breadcrumbs + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) => + content.AncestorsOrSelf(andSelf, null).Reverse(); + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level + /// higher or equal to . + /// + /// The content. + /// The minimum level. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher + /// or equal to . + /// + public static IEnumerable Breadcrumbs( + this IPublishedContent content, + int minLevel, + bool andSelf = true) => + content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level + /// higher or equal to the specified root content type . + /// + /// The root content type. + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher + /// or equal to the specified root content type . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + where T : class, IPublishedContent + { + static IEnumerable TakeUntil(IEnumerable source, Func predicate) + { + foreach (IPublishedContent item in source) + { + yield return item; + if (predicate(item)) + { + yield break; + } + } + } + + return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); + } + + #endregion + + #region Axes: descendants, descendants-or-self + + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// Variation context accessor. + /// + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelfOfType( + this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string docTypeAlias, string? culture = null) => parentNodes.SelectMany(x => + x.DescendantsOrSelfOfType(variationContextAccessor, docTypeAlias, culture)); + + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + parentNodes.SelectMany(x => x.DescendantsOrSelf(variationContextAccessor, culture)); + + // as per XPath 1.0 specs �2.2, + // - the descendant axis contains the descendants of the context node; a descendant is a child or a child of a child and so on; thus + // the descendant axis never contains attribute or namespace nodes. + // - the descendant-or-self axis contains the context node and the descendants of the context node. + // + // as per XPath 2.0 specs �3.2.1.1, + // - the descendant axis is defined as the transitive closure of the child axis; it contains the descendants of the context node (the + // children, the children of the children, and so on). + // - the descendant-or-self axis contains the context node and the descendants of the context node. + // + // the descendant and descendant-or-self axis are forward axes ie they contain the context node or nodes that are after the context + // node in document order. + // + // document order is defined by �2.4.1 as: + // - the root node is the first node. + // - every node occurs before all of its children and descendants. + // - the relative order of siblings is the order in which they occur in the children property of their parent node. + // - children and descendants occur before following siblings. + public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, false, null, culture); + + public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, false, p => p.Level >= level, culture); + + public static IEnumerable DescendantsOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + + public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.Descendants(variationContextAccessor, culture).OfType(); + + public static IEnumerable Descendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + where T : class, IPublishedContent => + content.Descendants(variationContextAccessor, level, culture).OfType(); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, true, null, culture); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, true, p => p.Level >= level, culture); + + public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => + content.DescendantsOrSelf(variationContextAccessor, true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.DescendantsOrSelf(variationContextAccessor, culture).OfType(); + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + where T : class, IPublishedContent => + content.DescendantsOrSelf(variationContextAccessor, level, culture).OfType(); + + public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => + content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + + public static IPublishedContent? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x.Level == level); + + public static IPublishedContent? DescendantOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, false, culture) + .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.EnumerateDescendants(variationContextAccessor, false, culture).FirstOrDefault(x => x is T) as T; + + public static T? Descendant(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + where T : class, IPublishedContent => + content.Descendant(variationContextAccessor, level, culture) as T; + + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => content; + + public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x.Level == level); + + public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content + .EnumerateDescendants(variationContextAccessor, true, culture) + .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + + public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.EnumerateDescendants(variationContextAccessor, true, culture).FirstOrDefault(x => x is T) as T; + + public static T? DescendantOrSelf(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) + where T : class, IPublishedContent => + content.DescendantOrSelf(variationContextAccessor, level, culture) as T; + + internal static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + bool orSelf, + Func? func, + string? culture = null) => + content.EnumerateDescendants(variationContextAccessor, orSelf, culture) + .Where(x => func == null || func(x)); + + internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, bool orSelf, string? culture = null) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (orSelf) + { + yield return content; + } + + IEnumerable? children = content.Children(variationContextAccessor, culture); + if (children is not null) + { + foreach (IPublishedContent desc in children.SelectMany(x => + x.EnumerateDescendants(variationContextAccessor, culture))) + { + yield return desc; + } + } + } + + internal static IEnumerable EnumerateDescendants(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + { + yield return content; + IEnumerable? children = content.Children(variationContextAccessor, culture); + if (children is not null) + { + foreach (IPublishedContent desc in children.SelectMany(x => + x.EnumerateDescendants(variationContextAccessor, culture))) + { + yield return desc; + } + } + } + + #endregion + + #region Axes: children + + /// + /// Gets the children of the content item. + /// + /// The content item. + /// + /// + /// The specific culture to get the URL children for. Default is null which will use the current culture in + /// + /// + /// + /// Gets children that are available for the specified culture. + /// Children are sorted by their sortOrder. + /// + /// For culture, + /// if null is used the current culture is used. + /// If an empty string is used only invariant children are returned. + /// If "*" is used all children are returned. + /// + /// + /// If a variant culture is specified or there is a current culture in the then the + /// Children returned + /// will include both the variant children matching the culture AND the invariant children because the invariant + /// children flow with the current culture. + /// However, if an empty string is specified only invariant children are returned. + /// + /// + public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) + { + // handle context culture for variant + if (culture == null) + { + culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; + } + + IEnumerable? children = content.ChildrenForAllCultures; + return culture == "*" + ? children + : children?.Where(x => x.IsInvariantOrHasCulture(culture)); + } + + /// + /// Gets the children of the content, filtered by a predicate. + /// + /// The content. + /// The accessor for VariationContext + /// The predicate. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of the content, filtered by the predicate. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable? Children( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + Func predicate, + string? culture = null) => + content.Children(variationContextAccessor, culture)?.Where(predicate); + + /// + /// Gets the children of the content, of any of the specified types. + /// + /// The content. + /// The accessor for the VariationContext + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The content type alias. + /// The children of the content, of any of the specified types. + public static IEnumerable? ChildrenOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? contentTypeAlias, string? culture = null) => + content.Children(variationContextAccessor, x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + + /// + /// Gets the children of the content, of a given content type. + /// + /// The content type. + /// The content. + /// The accessor for the VariationContext + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of content, of the given content type. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable? Children(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.Children(variationContextAccessor, culture)?.OfType(); + + public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) => + content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + + /// + /// Gets the first child of the content, of a given content type. + /// + public static IPublishedContent? FirstChildOfType(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => + content.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture)?.FirstOrDefault(); + + public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) => content.Children(variationContextAccessor, predicate, culture)?.FirstOrDefault(); + + public static IPublishedContent? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Guid uniqueId, string? culture = null) => content + .Children(variationContextAccessor, x => x.Key == uniqueId, culture)?.FirstOrDefault(); + + public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + content.Children(variationContextAccessor, culture)?.FirstOrDefault(); + + public static T? FirstChild(this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) + where T : class, IPublishedContent => + content.Children(variationContextAccessor, culture)?.FirstOrDefault(predicate); + + #endregion + + #region Axes: siblings + + /// + /// Gets the siblings of the content. + /// + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? Siblings( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string? culture = null) => + SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture)?.Where(x => x.Id != content.Id); + + /// + /// Gets the siblings of the content, of a given content type. + /// + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The content type alias. + /// The siblings of the content, of the given content type. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? SiblingsOfType( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string contentTypeAlias, + string? culture = null) => + SiblingsAndSelfOfType(content, publishedSnapshot, variationContextAccessor, contentTypeAlias, culture) + ?.Where(x => x.Id != content.Id); + + /// + /// Gets the siblings of the content, of a given content type. + /// + /// The content type. + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content, of the given content type. + /// + /// Note that in V7 this method also return the content node self. + /// + public static IEnumerable? Siblings(this IPublishedContent content, IPublishedSnapshot? publishedSnapshot, IVariationContextAccessor variationContextAccessor, string? culture = null) + where T : class, IPublishedContent => + SiblingsAndSelf(content, publishedSnapshot, variationContextAccessor, culture) + ?.Where(x => x.Id != content.Id); + + /// + /// Gets the siblings of the content including the node itself to indicate the position. + /// + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content including the node itself. + public static IEnumerable? SiblingsAndSelf( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string? culture = null) => + content.Parent != null + ? content.Parent.Children(variationContextAccessor, culture) + : publishedSnapshot?.Content?.GetAtRoot(culture) + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + + /// + /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. + /// + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The content type alias. + /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable? SiblingsAndSelfOfType( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string contentTypeAlias, + string? culture = null) => + content.Parent != null + ? content.Parent.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture) + : publishedSnapshot?.Content?.GetAtRoot(culture).OfTypes(contentTypeAlias) + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + + /// + /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. + /// + /// The content type. + /// The content. + /// Published snapshot instance + /// Variation context accessor. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable? SiblingsAndSelf( + this IPublishedContent content, + IPublishedSnapshot? publishedSnapshot, + IVariationContextAccessor variationContextAccessor, + string? culture = null) + where T : class, IPublishedContent => + content.Parent != null + ? content.Parent.Children(variationContextAccessor, culture) + : publishedSnapshot?.Content?.GetAtRoot(culture).OfType() + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + + #endregion + + #region Axes: custom + + /// + /// Gets the root content (ancestor or self at level 1) for the specified . + /// + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified . + /// + /// + /// This is the same as calling + /// with maxLevel + /// set to 1. + /// + public static IPublishedContent? Root(this IPublishedContent content) => content.AncestorOrSelf(1); + + /// + /// Gets the root content (ancestor or self at level 1) for the specified if it's of the + /// specified content type . + /// + /// The content type. + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified of content type + /// . + /// + /// + /// This is the same as calling + /// with + /// maxLevel set to 1. + /// + public static T? Root(this IPublishedContent content) + where T : class, IPublishedContent => + content.AncestorOrSelf(1); + + #endregion + + #region Writer and creator + + public static string? GetCreatorName(this IPublishedContent content, IUserService userService) + { + IProfile? user = userService.GetProfileById(content.CreatorId); + return user?.Name; + } + + public static string? GetWriterName(this IPublishedContent content, IUserService userService) + { + IProfile? user = userService.GetProfileById(content.WriterId); + return user?.Name; + } + + #endregion + + #region Axes: children + + /// + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// Variation context accessor. + /// The content type service. + /// The media type service. + /// The member type service. + /// The published url provider. + /// An optional content type alias. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of the content. + public static DataTable ChildrenAsTable( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IPublishedUrlProvider publishedUrlProvider, + string contentTypeAliasFilter = "", + string? culture = null) + => GenerateDataTable(content, variationContextAccessor, contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); + + /// + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// Variation context accessor. + /// The content type service. + /// The media type service. + /// The member type service. + /// The published url provider. + /// An optional content type alias. + /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is + /// null) + /// + /// The children of the content. + private static DataTable GenerateDataTable( + IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IPublishedUrlProvider publishedUrlProvider, + string contentTypeAliasFilter = "", + string? culture = null) + { + IPublishedContent? firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() + ? content.Children(variationContextAccessor, culture)?.Any() ?? false + ? content.Children(variationContextAccessor, culture)?.ElementAt(0) + : null + : content.Children(variationContextAccessor, culture) + ?.FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); + if (firstNode == null) + { + // No children found + return new DataTable(); + } + + // use new utility class to create table so that we don't have to maintain code in many places, just one + DataTable dt = DataTableExtensions.GenerateDataTable( + + // pass in the alias of the first child node since this is the node type we're rendering headers for + firstNode.ContentType.Alias, + + // pass in the callback to extract the Dictionary of all defined aliases to their names + alias => GetPropertyAliasesAndNames(contentTypeService, mediaTypeService, memberTypeService, alias), + () => + { + // here we pass in a callback to populate the datatable, yup its a bit ugly but it's already legacy and we just want to maintain code in one place. + // create all row data + List>, IEnumerable>>> + tableData = DataTableExtensions.CreateTableData(); + IOrderedEnumerable? children = + content.Children(variationContextAccessor)?.OrderBy(x => x.SortOrder); + if (children is not null) + { + // loop through each child and create row data for it + foreach (IPublishedContent n in children) + { + if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) + { + if (n.ContentType.Alias.InvariantEquals(contentTypeAliasFilter) == false) + { + continue; // skip this one, it doesn't match the filter + } + } + + var standardVals = new Dictionary + { + { "Id", n.Id }, + { "NodeName", n.Name(variationContextAccessor) }, + { "NodeTypeAlias", n.ContentType.Alias }, + { "CreateDate", n.CreateDate }, + { "UpdateDate", n.UpdateDate }, + { "CreatorId", n.CreatorId }, + { "WriterId", n.WriterId }, + { "Url", n.Url(publishedUrlProvider) }, + }; + + var userVals = new Dictionary(); + IEnumerable properties = + n.Properties?.Where(p => p.GetSourceValue() is not null) ?? + Array.Empty(); + foreach (IPublishedProperty p in properties) + { + // probably want the "object value" of the property here... + userVals[p.Alias] = p.GetValue(); + } + + // Add the row data + DataTableExtensions.AddRowData(tableData, standardVals, userVals); + } + } + + return tableData; + }); + return dt; + } + + #endregion + + #region PropertyAliasesAndNames + + private static Func>? _getPropertyAliasesAndNames; + + /// + /// This is used only for unit tests to set the delegate to look up aliases/names dictionary of a content type + /// + internal static Func> + GetPropertyAliasesAndNames + { + get => _getPropertyAliasesAndNames ?? GetAliasesAndNames; + set => _getPropertyAliasesAndNames = value; + } + + private static Dictionary GetAliasesAndNames(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, string alias) + { + IContentTypeBase? type = contentTypeService.Get(alias) + ?? mediaTypeService.Get(alias) + ?? (IContentTypeBase?)memberTypeService.Get(alias); + Dictionary fields = GetAliasesAndNames(type); + + // ensure the standard fields are there + var stdFields = new Dictionary + { + { "Id", "Id" }, + { "NodeName", "NodeName" }, + { "NodeTypeAlias", "NodeTypeAlias" }, + { "CreateDate", "CreateDate" }, + { "UpdateDate", "UpdateDate" }, + { "CreatorId", "CreatorId" }, + { "WriterId", "WriterId" }, + { "Url", "Url" }, + }; + + foreach (KeyValuePair field in stdFields.Where(x => fields.ContainsKey(x.Key) == false)) + { + fields[field.Key] = field.Value; + } + + return fields; + } + + private static Dictionary GetAliasesAndNames(IContentTypeBase? contentType) => + contentType?.PropertyTypes.ToDictionary(x => x.Alias, x => x.Name) ?? new Dictionary(); + + #endregion } diff --git a/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs b/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs index 17133cddaa..c85178c85c 100644 --- a/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs @@ -1,210 +1,267 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for IPublishedElement. +/// +public static class PublishedElementExtensions { - /// - /// Provides extension methods for IPublishedElement. - /// - public static class PublishedElementExtensions + #region OfTypes + + // the .OfType() filter is nice when there's only one type + // this is to support filtering with multiple types + public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) + where T : IPublishedElement { - #region OfTypes - - // the .OfType() filter is nice when there's only one type - // this is to support filtering with multiple types - public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) - where T : IPublishedElement + if (types == null || types.Length == 0) { - if (types == null || types.Length == 0) return Enumerable.Empty(); - - return contents.Where(x => types.InvariantContains(x.ContentType.Alias)); + return Enumerable.Empty(); } - #endregion - - #region IsComposedOf - - /// - /// Gets a value indicating whether the content is of a content type composed of the given alias - /// - /// The content. - /// The content type alias. - /// A value indicating whether the content is of a content type composed of a content type identified by the alias. - public static bool IsComposedOf(this IPublishedElement content, string alias) - { - return content.ContentType.CompositionAliases.InvariantContains(alias); - } - - #endregion - - #region HasProperty - - /// - /// Gets a value indicating whether the content has a property identified by its alias. - /// - /// The content. - /// The property alias. - /// A value indicating whether the content has the property identified by the alias. - /// The content may have a property, and that property may not have a value. - public static bool HasProperty(this IPublishedElement content, string alias) - { - return content.ContentType.GetPropertyType(alias) != null; - } - - #endregion - - #region HasValue - - /// - /// Gets a value indicating whether the content has a value for a property identified by its alias. - /// - /// Returns true if GetProperty(alias) is not null and GetProperty(alias).HasValue is true. - public static bool HasValue(this IPublishedElement content, string alias, string? culture = null, string? segment = null) - { - var prop = content.GetProperty(alias); - return prop != null && prop.HasValue(culture, segment); - } - - #endregion - - #region Value - - /// - /// Gets the value of a content's property identified by its alias. - /// - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object? Value(this IPublishedElement content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) - { - var property = content.GetProperty(alias); - - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return property.GetValue(culture, segment); - - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) - return value; - - // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property?.GetValue(culture, segment); - } - - #endregion - - #region Value - - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type. - /// - /// The target property type. - /// The content. - /// The published value fallback implementation. - /// The property alias. - /// The variation language. - /// The variation segment. - /// Optional fallback strategy. - /// The default value. - /// The value of the content's property identified by the alias, converted to the specified type. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static T? Value(this IPublishedElement content, IPublishedValueFallback publishedValueFallback, string alias, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) - { - var property = content.GetProperty(alias); - - // if we have a property, and it has a value, return that value - if (property != null && property.HasValue(culture, segment)) - return property.Value(publishedValueFallback, culture, segment); - - // else let fallback try to get a value - if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) - return value; - - // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property == null ? default : property.Value(publishedValueFallback, culture, segment); - } - - #endregion - - #region ToIndexedArray - - public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) - where TContent : class, IPublishedElement - { - var set = source.Select((content, index) => new IndexedArrayItem(content, index)).ToArray(); - foreach (var setItem in set) setItem.TotalCount = set.Length; - return set; - } - - #endregion - - #region IsSomething - - /// - /// Gets a value indicating whether the content is visible. - /// - /// The content. - /// The published value fallback implementation. - /// A value indicating whether the content is visible. - /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, - /// the content is visible. - public static bool IsVisible(this IPublishedElement content, IPublishedValueFallback publishedValueFallback) - { - // rely on the property converter - will return default bool value, ie false, if property - // is not defined, or has no value, else will return its value. - return content.Value(publishedValueFallback, Constants.Conventions.Content.NaviHide) == false; - } - - #endregion - - #region MediaUrl - - /// - /// Gets the url for a media. - /// - /// The content item. - /// The published url provider. - /// The culture (use current culture by default). - /// The url mode (use site configuration by default). - /// The alias of the property (use 'umbracoFile' by default). - /// The url for the media. - /// - /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. - /// - public static string MediaUrl(this IPublishedContent content, IPublishedUrlProvider publishedUrlProvider, string? culture = null, UrlMode mode = UrlMode.Default, string propertyAlias = Constants.Conventions.Media.File) - { - if (publishedUrlProvider == null) throw new ArgumentNullException(nameof(publishedUrlProvider)); - - return publishedUrlProvider.GetMediaUrl(content, mode, culture, propertyAlias); - } - - #endregion + return contents.Where(x => types.InvariantContains(x.ContentType.Alias)); } + + #endregion + + #region IsComposedOf + + /// + /// Gets a value indicating whether the content is of a content type composed of the given alias + /// + /// The content. + /// The content type alias. + /// + /// A value indicating whether the content is of a content type composed of a content type identified by the + /// alias. + /// + public static bool IsComposedOf(this IPublishedElement content, string alias) => + content.ContentType.CompositionAliases.InvariantContains(alias); + + #endregion + + #region HasProperty + + /// + /// Gets a value indicating whether the content has a property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether the content has the property identified by the alias. + /// The content may have a property, and that property may not have a value. + public static bool HasProperty(this IPublishedElement content, string alias) => + content.ContentType.GetPropertyType(alias) != null; + + #endregion + + #region HasValue + + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// + /// Returns true if GetProperty(alias) is not null and GetProperty(alias).HasValue is + /// true. + /// + public static bool HasValue(this IPublishedElement content, string alias, string? culture = null, string? segment = null) + { + IPublishedProperty? prop = content.GetProperty(alias); + return prop != null && prop.HasValue(culture, segment); + } + + #endregion + + #region Value + + /// + /// Gets the value of a content's property identified by its alias. + /// + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering + /// content. + /// + /// + /// If no property with the specified alias exists, or if the property has no value, returns + /// . + /// + /// + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the + /// converter. + /// + /// The alias is case-insensitive. + /// + public static object? Value( + this IPublishedElement content, + IPublishedValueFallback publishedValueFallback, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + object? defaultValue = default) + { + IPublishedProperty? property = content.GetProperty(alias); + + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) + { + return property.GetValue(culture, segment); + } + + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out var value)) + { + return value; + } + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property?.GetValue(culture, segment); + } + + #endregion + + #region Value + + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The published value fallback implementation. + /// The property alias. + /// The variation language. + /// The variation segment. + /// Optional fallback strategy. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type. + /// + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering + /// content. + /// + /// + /// If no property with the specified alias exists, or if the property has no value, or if it could not be + /// converted, returns default(T). + /// + /// + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the + /// converter. + /// + /// The alias is case-insensitive. + /// + public static T? Value( + this IPublishedElement content, + IPublishedValueFallback publishedValueFallback, + string alias, + string? culture = null, + string? segment = null, + Fallback fallback = default, + T? defaultValue = default) + { + IPublishedProperty? property = content.GetProperty(alias); + + // if we have a property, and it has a value, return that value + if (property != null && property.HasValue(culture, segment)) + { + return property.Value(publishedValueFallback, culture, segment); + } + + // else let fallback try to get a value + if (publishedValueFallback.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out T? value)) + { + return value; + } + + // else... if we have a property, at least let the converter return its own + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(publishedValueFallback, culture, segment); + } + + #endregion + + #region ToIndexedArray + + public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) + where TContent : class, IPublishedElement + { + IndexedArrayItem[] set = + source.Select((content, index) => new IndexedArrayItem(content, index)).ToArray(); + foreach (IndexedArrayItem setItem in set) + { + setItem.TotalCount = set.Length; + } + + return set; + } + + #endregion + + #region IsSomething + + /// + /// Gets a value indicating whether the content is visible. + /// + /// The content. + /// The published value fallback implementation. + /// A value indicating whether the content is visible. + /// + /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, + /// the content is visible. + /// + public static bool IsVisible(this IPublishedElement content, IPublishedValueFallback publishedValueFallback) => + + // rely on the property converter - will return default bool value, ie false, if property + // is not defined, or has no value, else will return its value. + content.Value(publishedValueFallback, Constants.Conventions.Content.NaviHide) == false; + + #endregion + + #region MediaUrl + + /// + /// Gets the url for a media. + /// + /// The content item. + /// The published url provider. + /// The culture (use current culture by default). + /// The url mode (use site configuration by default). + /// The alias of the property (use 'umbracoFile' by default). + /// The url for the media. + /// + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + /// + public static string MediaUrl( + this IPublishedContent content, + IPublishedUrlProvider publishedUrlProvider, + string? culture = null, + UrlMode mode = UrlMode.Default, + string propertyAlias = Constants.Conventions.Media.File) + { + if (publishedUrlProvider == null) + { + throw new ArgumentNullException(nameof(publishedUrlProvider)); + } + + return publishedUrlProvider.GetMediaUrl(content, mode, culture, propertyAlias); + } + + #endregion } diff --git a/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs b/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs index b4ffc40130..0d84e3268e 100644 --- a/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs @@ -1,52 +1,52 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for . +/// +public static class PublishedModelFactoryExtensions { /// - /// Provides extension methods for . + /// Returns true if the current is an implementation of + /// and is enabled /// - public static class PublishedModelFactoryExtensions + public static bool IsLiveFactoryEnabled(this IPublishedModelFactory factory) { - /// - /// Returns true if the current is an implementation of and is enabled - /// - public static bool IsLiveFactoryEnabled(this IPublishedModelFactory factory) + if (factory is IAutoPublishedModelFactory liveFactory) { - if (factory is IAutoPublishedModelFactory liveFactory) - { - return liveFactory.Enabled; - } - - // if it's not ILivePublishedModelFactory we know we're not using a live factory - return false; + return liveFactory.Enabled; } - /// - /// Sets a flag to reset the ModelsBuilder models if the is - /// - /// - /// This does not recompile the InMemory models, only sets a flag to tell models builder to recompile when they are requested. - /// - internal static void WithSafeLiveFactoryReset(this IPublishedModelFactory factory, Action action) - { - if (factory is IAutoPublishedModelFactory liveFactory) - { - lock (liveFactory.SyncRoot) - { - liveFactory.Reset(); + // if it's not ILivePublishedModelFactory we know we're not using a live factory + return false; + } - action(); - } - } - else + /// + /// Sets a flag to reset the ModelsBuilder models if the is + /// + /// + /// + /// This does not recompile the InMemory models, only sets a flag to tell models builder to recompile when they are + /// requested. + /// + internal static void WithSafeLiveFactoryReset(this IPublishedModelFactory factory, Action action) + { + if (factory is IAutoPublishedModelFactory liveFactory) + { + lock (liveFactory.SyncRoot) { + liveFactory.Reset(); + action(); } } - + else + { + action(); + } } } diff --git a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs index 3ff5c77719..267157cf7a 100644 --- a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs +++ b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs @@ -1,77 +1,80 @@ // Copyright (c) Umbraco. // See LICENSE for more details. + +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for IPublishedProperty. +/// +public static class PublishedPropertyExtension { - /// - /// Provides extension methods for IPublishedProperty. - /// - public static class PublishedPropertyExtension + #region Value + + public static object? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) { - #region Value - - public static object? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, object? defaultValue = default) + if (property.HasValue(culture, segment)) { - if (property.HasValue(culture, segment)) - return property.GetValue(culture, segment); - - return publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var value) - ? value - : property.GetValue(culture, segment); // give converter a chance to return it's own vision of "no value" + return property.GetValue(culture, segment); } - #endregion + return publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var value) + ? value + : property.GetValue(culture, segment); // give converter a chance to return it's own vision of "no value" + } - #region Value + #endregion - public static T? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) + #region Value + + public static T? Value(this IPublishedProperty property, IPublishedValueFallback publishedValueFallback, string? culture = null, string? segment = null, Fallback fallback = default, T? defaultValue = default) + { + if (property.HasValue(culture, segment)) { - if (property.HasValue(culture, segment)) + // we have a value + // try to cast or convert it + var value = property.GetValue(culture, segment); + if (value is T valueAsT) { - // we have a value - // try to cast or convert it - var value = property.GetValue(culture, segment); - if (value is T valueAsT) - { - return valueAsT; - } - - var valueConverted = value.TryConvertTo(); - if (valueConverted.Success) - { - return valueConverted.Result; - } - - // cannot cast nor convert the value, nothing we can return but 'default' - // note: we don't want to fallback in that case - would make little sense - return default; + return valueAsT; } - // we don't have a value, try fallback - if (publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue)) + Attempt valueConverted = value.TryConvertTo(); + if (valueConverted.Success) { - return fallbackValue; + return valueConverted.Result; } - // we don't have a value - neither direct nor fallback - // give a chance to the converter to return something (eg empty enumerable) - var noValue = property.GetValue(culture, segment); - if (noValue is T noValueAsT) - { - return noValueAsT; - } - - var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted.Success) - { - return noValueConverted.Result; - } - - // cannot cast noValue nor convert it, nothing we can return but 'default' + // cannot cast nor convert the value, nothing we can return but 'default' + // note: we don't want to fallback in that case - would make little sense return default; } - #endregion + // we don't have a value, try fallback + if (publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out T? fallbackValue)) + { + return fallbackValue; + } + + // we don't have a value - neither direct nor fallback + // give a chance to the converter to return something (eg empty enumerable) + var noValue = property.GetValue(culture, segment); + if (noValue is T noValueAsT) + { + return noValueAsT; + } + + Attempt noValueConverted = noValue.TryConvertTo(); + if (noValueConverted.Success) + { + return noValueConverted.Result; + } + + // cannot cast noValue nor convert it, nothing we can return but 'default' + return default; } + + #endregion } diff --git a/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs b/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs index 9fd7da4640..5e6d356674 100644 --- a/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedSnapshotAccessorExtensions.cs @@ -1,18 +1,17 @@ -using System; using Umbraco.Cms.Core.PublishedCache; -namespace Umbraco.Extensions -{ - public static class PublishedSnapshotAccessorExtensions - { - public static IPublishedSnapshot GetRequiredPublishedSnapshot(this IPublishedSnapshotAccessor publishedSnapshotAccessor) - { - if (publishedSnapshotAccessor.TryGetPublishedSnapshot(out var publishedSnapshot)) - { - return publishedSnapshot!; - } +namespace Umbraco.Extensions; - throw new InvalidOperationException("Wasn't possible to a get a valid Snapshot"); +public static class PublishedSnapshotAccessorExtensions +{ + public static IPublishedSnapshot GetRequiredPublishedSnapshot( + this IPublishedSnapshotAccessor publishedSnapshotAccessor) + { + if (publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) + { + return publishedSnapshot!; } + + throw new InvalidOperationException("Wasn't possible to a get a valid Snapshot"); } } diff --git a/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs index e9e6618f8c..475f093785 100644 --- a/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs +++ b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs @@ -1,96 +1,99 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Configuration; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.UmbracoSettings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Get concatenated user and default character replacements +/// taking into account +/// +public static class RequestHandlerSettingsExtension { /// - /// Get concatenated user and default character replacements - /// taking into account + /// Get concatenated user and default character replacements + /// taking into account /// - public static class RequestHandlerSettingsExtension + public static IEnumerable GetCharReplacements(this RequestHandlerSettings requestHandlerSettings) { - /// - /// Get concatenated user and default character replacements - /// taking into account - /// - public static IEnumerable GetCharReplacements(this RequestHandlerSettings requestHandlerSettings) + if (requestHandlerSettings.EnableDefaultCharReplacements is false) { - if (requestHandlerSettings.EnableDefaultCharReplacements is false) - { - return requestHandlerSettings.UserDefinedCharCollection ?? Enumerable.Empty(); - } - - if (requestHandlerSettings.UserDefinedCharCollection == null || requestHandlerSettings.UserDefinedCharCollection.Any() is false) - { - return RequestHandlerSettings.DefaultCharCollection; - } - - return MergeUnique(requestHandlerSettings.UserDefinedCharCollection, RequestHandlerSettings.DefaultCharCollection); + return requestHandlerSettings.UserDefinedCharCollection ?? Enumerable.Empty(); } - /// - /// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection - /// - internal static void MergeReplacements(this RequestHandlerSettings requestHandlerSettings, IConfiguration configuration) + if (requestHandlerSettings.UserDefinedCharCollection == null || + requestHandlerSettings.UserDefinedCharCollection.Any() is false) { - string sectionKey = $"{Constants.Configuration.ConfigRequestHandler}:"; - - IEnumerable charCollection = GetReplacements( - configuration, - $"{sectionKey}{nameof(RequestHandlerSettings.CharCollection)}"); - - IEnumerable userDefinedCharCollection = GetReplacements( - configuration, - $"{sectionKey}{nameof(requestHandlerSettings.UserDefinedCharCollection)}"); - - IEnumerable mergedCollection = MergeUnique(userDefinedCharCollection, charCollection); - - requestHandlerSettings.UserDefinedCharCollection = mergedCollection; + return RequestHandlerSettings.DefaultCharCollection; } - private static IEnumerable GetReplacements(IConfiguration configuration, string key) + return MergeUnique( + requestHandlerSettings.UserDefinedCharCollection, + RequestHandlerSettings.DefaultCharCollection); + } + + /// + /// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection + /// + internal static void MergeReplacements( + this RequestHandlerSettings requestHandlerSettings, + IConfiguration configuration) + { + var sectionKey = $"{Constants.Configuration.ConfigRequestHandler}:"; + + IEnumerable charCollection = GetReplacements( + configuration, + $"{sectionKey}{nameof(RequestHandlerSettings.CharCollection)}"); + + IEnumerable userDefinedCharCollection = GetReplacements( + configuration, + $"{sectionKey}{nameof(requestHandlerSettings.UserDefinedCharCollection)}"); + + IEnumerable mergedCollection = MergeUnique(userDefinedCharCollection, charCollection); + + requestHandlerSettings.UserDefinedCharCollection = mergedCollection; + } + + private static IEnumerable GetReplacements(IConfiguration configuration, string key) + { + var replacements = new List(); + IEnumerable config = configuration.GetSection(key).GetChildren(); + + foreach (IConfigurationSection section in config) { - var replacements = new List(); - IEnumerable config = configuration.GetSection(key).GetChildren(); - - foreach (IConfigurationSection section in config) - { - var @char = section.GetValue(nameof(CharItem.Char)); - var replacement = section.GetValue(nameof(CharItem.Replacement)); - replacements.Add(new CharItem { Char = @char, Replacement = replacement }); - } - - return replacements; + var @char = section.GetValue(nameof(CharItem.Char)); + var replacement = section.GetValue(nameof(CharItem.Replacement)); + replacements.Add(new CharItem { Char = @char, Replacement = replacement }); } - /// - /// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in alternativeReplacements - /// - private static IEnumerable MergeUnique( - IEnumerable priorityReplacements, - IEnumerable alternativeReplacements) - { - var priorityReplacementsList = priorityReplacements.ToList(); - var alternativeReplacementsList = alternativeReplacements.ToList(); + return replacements; + } - foreach (CharItem alternativeReplacement in alternativeReplacementsList) + /// + /// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in + /// alternativeReplacements + /// + private static IEnumerable MergeUnique( + IEnumerable priorityReplacements, + IEnumerable alternativeReplacements) + { + var priorityReplacementsList = priorityReplacements.ToList(); + var alternativeReplacementsList = alternativeReplacements.ToList(); + + foreach (CharItem alternativeReplacement in alternativeReplacementsList) + { + foreach (CharItem priorityReplacement in priorityReplacementsList) { - foreach (CharItem priorityReplacement in priorityReplacementsList) + if (priorityReplacement.Char == alternativeReplacement.Char) { - if (priorityReplacement.Char == alternativeReplacement.Char) - { - alternativeReplacement.Replacement = priorityReplacement.Replacement; - } + alternativeReplacement.Replacement = priorityReplacement.Replacement; } } - - return priorityReplacementsList.Union( - alternativeReplacementsList, - new CharacterReplacementEqualityComparer()); } + + return priorityReplacementsList.Union( + alternativeReplacementsList, + new CharacterReplacementEqualityComparer()); } } diff --git a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs index 72930b89f8..219b73c39f 100644 --- a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs +++ b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs @@ -1,33 +1,34 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class RuntimeStateExtensions { - public static class RuntimeStateExtensions - { - /// - /// Returns true if the installer is enabled based on the current runtime state - /// - /// - /// - public static bool EnableInstaller(this IRuntimeState state) - => state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade; - // TODO: If we want to enable the installer for package migrations, but IMO i think we should do migrations in the back office - // if they are not unattended. - //=> state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade || state.Level == RuntimeLevel.PackageMigrations; + /// + /// Returns true if the installer is enabled based on the current runtime state + /// + /// + /// + public static bool EnableInstaller(this IRuntimeState state) + => state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade; - /// - /// Returns true if Umbraco is greater than - /// - public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + // TODO: If we want to enable the installer for package migrations, but IMO i think we should do migrations in the back office + // if they are not unattended. + // => state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade || state.Level == RuntimeLevel.PackageMigrations; - /// - /// Returns true if the runtime state indicates that unattended boot logic should execute - /// - /// - /// - public static bool RunUnattendedBootLogic(this IRuntimeState state) - => (state.Reason == RuntimeLevelReason.UpgradeMigrations || state.Reason == RuntimeLevelReason.UpgradePackageMigrations) - && state.Level == RuntimeLevel.Run; - } + /// + /// Returns true if Umbraco is greater than + /// + public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + + /// + /// Returns true if the runtime state indicates that unattended boot logic should execute + /// + /// + /// + public static bool RunUnattendedBootLogic(this IRuntimeState state) + => (state.Reason == RuntimeLevelReason.UpgradeMigrations || + state.Reason == RuntimeLevelReason.UpgradePackageMigrations) + && state.Level == RuntimeLevel.Run; } diff --git a/src/Umbraco.Core/Extensions/SemVersionExtensions.cs b/src/Umbraco.Core/Extensions/SemVersionExtensions.cs index e8b2a2534b..afdd49612e 100644 --- a/src/Umbraco.Core/Extensions/SemVersionExtensions.cs +++ b/src/Umbraco.Core/Extensions/SemVersionExtensions.cs @@ -1,22 +1,19 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Semver; -namespace Umbraco.Extensions -{ - public static class SemVersionExtensions - { - public static string ToSemanticString(this SemVersion semVersion) - { - return semVersion.ToString().Replace("--", "-").Replace("-+", "+"); - } +namespace Umbraco.Extensions; - public static string ToSemanticStringWithoutBuild(this SemVersion semVersion) - { - var version = semVersion.ToSemanticString(); - var indexOfBuild = version.IndexOf('+'); - return indexOfBuild >= 0 ? version.Substring(0, indexOfBuild) : version; - } +public static class SemVersionExtensions +{ + public static string ToSemanticString(this SemVersion semVersion) => + semVersion.ToString().Replace("--", "-").Replace("-+", "+"); + + public static string ToSemanticStringWithoutBuild(this SemVersion semVersion) + { + var version = semVersion.ToSemanticString(); + var indexOfBuild = version.IndexOf('+'); + return indexOfBuild >= 0 ? version[..indexOfBuild] : version; } } diff --git a/src/Umbraco.Core/Extensions/StringExtensions.cs b/src/Umbraco.Core/Extensions/StringExtensions.cs index c41bc290ff..694b4d05e6 100644 --- a/src/Umbraco.Core/Extensions/StringExtensions.cs +++ b/src/Umbraco.Core/Extensions/StringExtensions.cs @@ -1,1465 +1,1563 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; -using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// String extension methods +/// +public static class StringExtensions { - /// - /// String extension methods - /// - public static class StringExtensions + internal static readonly Lazy Whitespace = new(() => new Regex(@"\s+", RegexOptions.Compiled)); + + private const char DefaultEscapedStringEscapeChar = '\\'; + private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); + private static readonly char[] ToCSharpEscapeChars; + internal static readonly string[] JsonEmpties = { "[]", "{}" }; + + /// + /// The namespace for URLs (from RFC 4122, Appendix C). + /// See RFC 4122 + /// + internal static readonly Guid UrlNamespace = new("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); + + private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray(); + + // From: http://stackoverflow.com/a/961504/5018 + // filters control characters but allows only properly-formed surrogate sequences + private static readonly Lazy InvalidXmlChars = new(() => + new Regex( + @"(? e[0]) + 1]; + foreach (var escape in escapes) { - var escapes = new[] { "\aa", "\bb", "\ff", "\nn", "\rr", "\tt", "\vv", "\"\"", "\\\\", "??", "\00" }; - ToCSharpEscapeChars = new char[escapes.Max(e => e[0]) + 1]; - foreach (var escape in escapes) - ToCSharpEscapeChars[escape[0]] = escape[1]; + ToCSharpEscapeChars[escape[0]] = escape[1]; } + } - /// - /// Convert a path to node ids in the order from right to left (deepest to shallowest) - /// - /// - /// - public static int[] GetIdsFromPathReversed(this string path) + /// + /// Convert a path to node ids in the order from right to left (deepest to shallowest) + /// + /// + /// + public static int[] GetIdsFromPathReversed(this string path) + { + var nodeIds = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var output) + ? Attempt.Succeed(output) + : Attempt.Fail()) + .Where(x => x.Success) + .Select(x => x.Result) + .Reverse() + .ToArray(); + return nodeIds; + } + + /// + /// Removes new lines and tabs + /// + /// + /// + public static string StripWhitespace(this string txt) => Regex.Replace(txt, @"\s", string.Empty); + + public static string StripFileExtension(this string fileName) + { + // filenames cannot contain line breaks + if (fileName.Contains(Environment.NewLine) || fileName.Contains("\r") || fileName.Contains("\n")) { - var nodeIds = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var output) ? Attempt.Succeed(output) : Attempt.Fail()) - .Where(x => x.Success) - .Select(x=>x.Result) - .Reverse() - .ToArray(); - return nodeIds; - } - - /// - /// Removes new lines and tabs - /// - /// - /// - public static string StripWhitespace(this string txt) - { - return Regex.Replace(txt, @"\s", string.Empty); - } - - public static string StripFileExtension(this string fileName) - { - //filenames cannot contain line breaks - if (fileName.Contains(Environment.NewLine) || fileName.Contains("\r") || fileName.Contains("\n")) return fileName; - - var lastIndex = fileName.LastIndexOf('.'); - if (lastIndex > 0) - { - var ext = fileName.Substring(lastIndex); - //file extensions cannot contain whitespace - if (ext.Contains(" ")) return fileName; - - return string.Format("{0}", fileName.Substring(0, fileName.IndexOf(ext, StringComparison.Ordinal))); - } - return fileName; - - } - /// - /// Determines the extension of the path or URL - /// - /// - /// Extension of the file - public static string GetFileExtension(this string file) + var lastIndex = fileName.LastIndexOf('.'); + if (lastIndex > 0) { - //Find any characters between the last . and the start of a query string or the end of the string - const string pattern = @"(?\.[^\.\?]+)(\?.*|$)"; - var match = Regex.Match(file, pattern); - return match.Success - ? match.Groups["extension"].Value - : string.Empty; - } + var ext = fileName.Substring(lastIndex); - /// - /// This tries to detect a json string, this is not a fail safe way but it is quicker than doing - /// a try/catch when deserializing when it is not json. - /// - /// - /// - public static bool DetectIsJson(this string input) - { - if (input.IsNullOrWhiteSpace()) return false; - input = input.Trim(); - return (input.StartsWith("{") && input.EndsWith("}")) - || (input.StartsWith("[") && input.EndsWith("]")); - } - - internal static readonly Lazy Whitespace = new Lazy(() => new Regex(@"\s+", RegexOptions.Compiled)); - internal static readonly string[] JsonEmpties = { "[]", "{}" }; - public static bool DetectIsEmptyJson(this string input) - { - return JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty)); - } - - public static string ReplaceNonAlphanumericChars(this string input, string replacement) - { - //any character that is not alphanumeric, convert to a hyphen - var mName = input; - foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c))) + // file extensions cannot contain whitespace + if (ext.Contains(" ")) { - mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement); - } - return mName; - } - - public static string ReplaceNonAlphanumericChars(this string input, char replacement) - { - var inputArray = input.ToCharArray(); - var outputArray = new char[input.Length]; - for (var i = 0; i < inputArray.Length; i++) - outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; - return new string(outputArray); - } - private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray(); - - /// - /// Cleans string to aid in preventing xss attacks. - /// - /// - /// - /// - public static string CleanForXss(this string input, params char[] ignoreFromClean) - { - //remove any HTML - input = input.StripHtml(); - //strip out any potential chars involved with XSS - return input.ExceptChars(new HashSet(CleanForXssChars.Except(ignoreFromClean))); - } - - public static string ExceptChars(this string str, HashSet toExclude) - { - var sb = new StringBuilder(str.Length); - foreach (var c in str.Where(c => toExclude.Contains(c) == false)) - { - sb.Append(c); - } - return sb.ToString(); - } - - /// - /// Returns a stream from a string - /// - /// - /// - internal static Stream GenerateStreamFromString(this string s) - { - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(s); - writer.Flush(); - stream.Position = 0; - return stream; - } - - /// - /// This will append the query string to the URL - /// - /// - /// - /// - /// - /// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are - /// delimited properly with '&' - /// - public static string AppendQueryStringToUrl(this string url, params string[] queryStrings) - { - //remove any prefixed '&' or '?' - for (var i = 0; i < queryStrings.Length; i++) - { - queryStrings[i] = queryStrings[i].TrimStart(Constants.CharArrays.QuestionMarkAmpersand).TrimEnd(Constants.CharArrays.Ampersand); + return fileName; } - var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); - - if (url.Contains("?")) - { - return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); - } - return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); + return string.Format("{0}", fileName.Substring(0, fileName.IndexOf(ext, StringComparison.Ordinal))); } + return fileName; + } - //this is from SqlMetal and just makes it a bit of fun to allow pluralization - public static string MakePluralName(this string name) - { - if ((name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || name.EndsWith("ch", StringComparison.OrdinalIgnoreCase)) || (name.EndsWith("s", StringComparison.OrdinalIgnoreCase) || name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))) - { - name = name + "es"; - return name; - } - if ((name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && (name.Length > 1)) && !IsVowel(name[name.Length - 2])) - { - name = name.Remove(name.Length - 1, 1); - name = name + "ies"; - return name; - } - if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase)) - { - name = name + "s"; - } - return name; - } + /// + /// Determines the extension of the path or URL + /// + /// + /// Extension of the file + public static string GetFileExtension(this string file) + { + // Find any characters between the last . and the start of a query string or the end of the string + const string pattern = @"(?\.[^\.\?]+)(\?.*|$)"; + Match match = Regex.Match(file, pattern); + return match.Success + ? match.Groups["extension"].Value + : string.Empty; + } - public static bool IsVowel(this char c) + /// + /// This tries to detect a json string, this is not a fail safe way but it is quicker than doing + /// a try/catch when deserializing when it is not json. + /// + /// + /// + public static bool DetectIsJson(this string input) + { + if (input.IsNullOrWhiteSpace()) { - switch (c) - { - case 'O': - case 'U': - case 'Y': - case 'A': - case 'E': - case 'I': - case 'o': - case 'u': - case 'y': - case 'a': - case 'e': - case 'i': - return true; - } return false; } - /// - /// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts char or char[]. - /// - /// The value. - /// For removing. - /// - public static string Trim(this string value, string forRemoving) + input = input.Trim(); + return (input.StartsWith("{") && input.EndsWith("}")) + || (input.StartsWith("[") && input.EndsWith("]")); + } + + public static bool DetectIsEmptyJson(this string input) => + JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty)); + + public static string ReplaceNonAlphanumericChars(this string input, string replacement) + { + // any character that is not alphanumeric, convert to a hyphen + var mName = input; + foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c))) { - if (string.IsNullOrEmpty(value)) return value; - return value.TrimEnd(forRemoving).TrimStart(forRemoving); + mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement); } - public static string EncodeJsString(this string s) + return mName; + } + + public static string ReplaceNonAlphanumericChars(this string input, char replacement) + { + var inputArray = input.ToCharArray(); + var outputArray = new char[input.Length]; + for (var i = 0; i < inputArray.Length; i++) { - var sb = new StringBuilder(); - foreach (var c in s) - { - switch (c) - { - case '\"': - sb.Append("\\\""); - break; - case '\\': - sb.Append("\\\\"); - break; - case '\b': - sb.Append("\\b"); - break; - case '\f': - sb.Append("\\f"); - break; - case '\n': - sb.Append("\\n"); - break; - case '\r': - sb.Append("\\r"); - break; - case '\t': - sb.Append("\\t"); - break; - default: - int i = (int)c; - if (i < 32 || i > 127) - { - sb.AppendFormat("\\u{0:X04}", i); - } - else - { - sb.Append(c); - } - break; - } - } - return sb.ToString(); + outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; } - public static string TrimEnd(this string value, string forRemoving) - { - if (string.IsNullOrEmpty(value)) return value; - if (string.IsNullOrEmpty(forRemoving)) return value; + return new string(outputArray); + } - while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) - { - value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase)); - } - return value; + /// + /// Cleans string to aid in preventing xss attacks. + /// + /// + /// + /// + public static string CleanForXss(this string input, params char[] ignoreFromClean) + { + // remove any HTML + input = input.StripHtml(); + + // strip out any potential chars involved with XSS + return input.ExceptChars(new HashSet(CleanForXssChars.Except(ignoreFromClean))); + } + + public static string ExceptChars(this string str, HashSet toExclude) + { + var sb = new StringBuilder(str.Length); + foreach (var c in str.Where(c => toExclude.Contains(c) == false)) + { + sb.Append(c); } - public static string TrimStart(this string value, string forRemoving) - { - if (string.IsNullOrEmpty(value)) return value; - if (string.IsNullOrEmpty(forRemoving)) return value; + return sb.ToString(); + } - while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) - { - value = value.Substring(forRemoving.Length); - } - return value; + /// + /// This will append the query string to the URL + /// + /// + /// + /// + /// + /// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are + /// delimited properly with '&' + /// + public static string AppendQueryStringToUrl(this string url, params string[] queryStrings) + { + // remove any prefixed '&' or '?' + for (var i = 0; i < queryStrings.Length; i++) + { + queryStrings[i] = queryStrings[i].TrimStart(Constants.CharArrays.QuestionMarkAmpersand) + .TrimEnd(Constants.CharArrays.Ampersand); } - public static string EnsureStartsWith(this string input, string toStartWith) + var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); + + if (url.Contains("?")) { - if (input.StartsWith(toStartWith)) return input; - return toStartWith + input.TrimStart(toStartWith); + return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); } - public static string EnsureStartsWith(this string input, char value) + return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); + } + + /// + /// Returns a stream from a string + /// + /// + /// + internal static Stream GenerateStreamFromString(this string s) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(s); + writer.Flush(); + stream.Position = 0; + return stream; + } + + // this is from SqlMetal and just makes it a bit of fun to allow pluralization + public static string MakePluralName(this string name) + { + if (name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || + name.EndsWith("ch", StringComparison.OrdinalIgnoreCase) || + name.EndsWith("s", StringComparison.OrdinalIgnoreCase) || + name.EndsWith("sh", StringComparison.OrdinalIgnoreCase)) { - return input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input; + name += "es"; + return name; } - public static string EnsureEndsWith(this string input, char value) + if (name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && name.Length > 1 && + !IsVowel(name[^2])) { - return input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value; + name = name.Remove(name.Length - 1, 1); + name += "ies"; + return name; } - public static string EnsureEndsWith(this string input, string toEndWith) + if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase)) { - return input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith; + name += "s"; } - public static bool IsLowerCase(this char ch) - { - return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); - } + return name; + } - public static bool IsUpperCase(this char ch) + public static bool IsVowel(this char c) + { + switch (c) { - return ch.ToString(CultureInfo.InvariantCulture) == ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(); - } - - /// Indicates whether a specified string is null, empty, or - /// consists only of white-space characters. - /// The value to check. - /// Returns if the value is null, - /// empty, or consists only of white-space characters, otherwise - /// returns . - public static bool IsNullOrWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value); - - public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) - { - return str.IsNullOrWhiteSpace() ? defaultValue : str; - } - - /// The to delimited list. - /// The list. - /// The delimiter. - /// the list - [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")] - public static IList ToDelimitedList(this string list, string delimiter = ",") - { - var delimiters = new[] { delimiter }; - return !list.IsNullOrWhiteSpace() - ? list.Split(delimiters, StringSplitOptions.RemoveEmptyEntries) - .Select(i => i.Trim()) - .ToList() - : new List(); - } - - /// enum try parse. - /// The str type. - /// The ignore case. - /// The result. - /// The type - /// The enum try parse. - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] - [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] - public static bool EnumTryParse(this string strType, bool ignoreCase, out T? result) - { - try - { - result = (T)Enum.Parse(typeof(T), strType, ignoreCase); + case 'O': + case 'U': + case 'Y': + case 'A': + case 'E': + case 'I': + case 'o': + case 'u': + case 'y': + case 'a': + case 'e': + case 'i': return true; - } - catch + } + + return false; + } + + /// + /// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts + /// char or char[]. + /// + /// The value. + /// For removing. + /// + public static string Trim(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + return value.TrimEnd(forRemoving).TrimStart(forRemoving); + } + + public static string EncodeJsString(this string s) + { + var sb = new StringBuilder(); + foreach (var c in s) + { + switch (c) { - result = default(T); - return false; + case '\"': + sb.Append("\\\""); + break; + case '\\': + sb.Append("\\\\"); + break; + case '\b': + sb.Append("\\b"); + break; + case '\f': + sb.Append("\\f"); + break; + case '\n': + sb.Append("\\n"); + break; + case '\r': + sb.Append("\\r"); + break; + case '\t': + sb.Append("\\t"); + break; + default: + int i = c; + if (i < 32 || i > 127) + { + sb.AppendFormat("\\u{0:X04}", i); + } + else + { + sb.Append(c); + } + + break; } } - /// - /// Parse string to Enum - /// - /// The enum type - /// The string to parse - /// The ignore case - /// The parsed enum - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] - [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] - public static T EnumParse(this string strType, bool ignoreCase) + return sb.ToString(); + } + + public static string TrimEnd(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) { - return (T)Enum.Parse(typeof(T), strType, ignoreCase); + return value; } - /// - /// Strips all HTML from a string. - /// - /// The text. - /// Returns the string without any HTML tags. - public static string StripHtml(this string text) + if (string.IsNullOrEmpty(forRemoving)) { - const string pattern = @"<(.|\n)*?>"; - return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled); + return value; } - /// - /// Encodes as GUID. - /// - /// The input. - /// - public static Guid EncodeAsGuid(this string input) + while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) { - if (string.IsNullOrWhiteSpace(input)) throw new ArgumentNullException("input"); - - var convertToHex = input.ConvertToHex(); - var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32; - var hex = convertToHex.Substring(0, hexLength).PadLeft(32, '0'); - var output = Guid.Empty; - return Guid.TryParse(hex, out output) ? output : Guid.Empty; + value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase)); } - /// - /// Converts to hex. - /// - /// The input. - /// - public static string ConvertToHex(this string input) - { - if (string.IsNullOrEmpty(input)) return string.Empty; + return value; + } - var sb = new StringBuilder(input.Length); - foreach (var c in input) + public static string TrimStart(this string value, string forRemoving) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + if (string.IsNullOrEmpty(forRemoving)) + { + return value; + } + + while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase)) + { + value = value.Substring(forRemoving.Length); + } + + return value; + } + + public static string EnsureStartsWith(this string input, string toStartWith) + { + if (input.StartsWith(toStartWith)) + { + return input; + } + + return toStartWith + input.TrimStart(toStartWith); + } + + public static string EnsureStartsWith(this string input, char value) => + input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input; + + public static string EnsureEndsWith(this string input, char value) => + input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value; + + public static string EnsureEndsWith(this string input, string toEndWith) => + input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith; + + public static bool IsLowerCase(this char ch) => ch.ToString(CultureInfo.InvariantCulture) == + ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(); + + public static bool IsUpperCase(this char ch) => ch.ToString(CultureInfo.InvariantCulture) == + ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(); + + /// + /// Indicates whether a specified string is null, empty, or + /// consists only of white-space characters. + /// + /// The value to check. + /// + /// Returns if the value is null, + /// empty, or consists only of white-space characters, otherwise + /// returns . + /// + public static bool IsNullOrWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value); + + public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) => + str.IsNullOrWhiteSpace() ? defaultValue : str; + + /// The to delimited list. + /// The list. + /// The delimiter. + /// the list + [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")] + public static IList ToDelimitedList(this string list, string delimiter = ",") + { + var delimiters = new[] { delimiter }; + return !list.IsNullOrWhiteSpace() + ? list.Split(delimiters, StringSplitOptions.RemoveEmptyEntries) + .Select(i => i.Trim()) + .ToList() + : new List(); + } + + /// enum try parse. + /// The str type. + /// The ignore case. + /// The result. + /// The type + /// The enum try parse. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] + public static bool EnumTryParse(this string strType, bool ignoreCase, out T? result) + { + try + { + result = (T)Enum.Parse(typeof(T), strType, ignoreCase); + return true; + } + catch + { + result = default; + return false; + } + } + + /// + /// Parse string to Enum + /// + /// The enum type + /// The string to parse + /// The ignore case + /// The parsed enum + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")] + public static T EnumParse(this string strType, bool ignoreCase) => (T)Enum.Parse(typeof(T), strType, ignoreCase); + + /// + /// Strips all HTML from a string. + /// + /// The text. + /// Returns the string without any HTML tags. + public static string StripHtml(this string text) + { + const string pattern = @"<(.|\n)*?>"; + return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled); + } + + /// + /// Encodes as GUID. + /// + /// The input. + /// + public static Guid EncodeAsGuid(this string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + throw new ArgumentNullException("input"); + } + + var convertToHex = input.ConvertToHex(); + var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32; + var hex = convertToHex.Substring(0, hexLength).PadLeft(32, '0'); + Guid output = Guid.Empty; + return Guid.TryParse(hex, out output) ? output : Guid.Empty; + } + + /// + /// Converts to hex. + /// + /// The input. + /// + public static string ConvertToHex(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + var sb = new StringBuilder(input.Length); + foreach (var c in input) + { + sb.AppendFormat("{0:x2}", Convert.ToUInt32(c)); + } + + return sb.ToString(); + } + + public static string DecodeFromHex(this string hexValue) + { + var strValue = string.Empty; + while (hexValue.Length > 0) + { + strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); + hexValue = hexValue.Substring(2, hexValue.Length - 2); + } + + return strValue; + } + + /// + /// Encodes a string to a safe URL base64 string + /// + /// + /// + public static string ToUrlBase64(this string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + // return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ","); + var bytes = Encoding.UTF8.GetBytes(input); + return UrlTokenEncode(bytes); + } + + /// + /// Decodes a URL safe base64 string back + /// + /// + /// + public static string? FromUrlBase64(this string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + // if (input.IsInvalidBase64()) return null; + try + { + // var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "=")); + var decodedBytes = UrlTokenDecode(input); + return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null; + } + catch (FormatException) + { + return null; + } + } + + /// + /// formats the string with invariant culture + /// + /// The format. + /// The args. + /// + public static string InvariantFormat(this string? format, params object?[] args) => + string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, args); + + /// + /// Converts an integer to an invariant formatted string + /// + /// + /// + public static string ToInvariantString(this int str) => str.ToString(CultureInfo.InvariantCulture); + + public static string ToInvariantString(this long str) => str.ToString(CultureInfo.InvariantCulture); + + /// + /// Compares 2 strings with invariant culture and case ignored + /// + /// The compare. + /// The compare to. + /// + public static bool InvariantEquals(this string? compare, string? compareTo) => + string.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase); + + public static bool InvariantStartsWith(this string compare, string compareTo) => + compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + + public static bool InvariantEndsWith(this string compare, string compareTo) => + compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); + + public static bool InvariantContains(this string compare, string compareTo) => + compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0; + + public static bool InvariantContains(this IEnumerable compare, string compareTo) => + compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase); + + public static int InvariantIndexOf(this string s, string value) => + s.IndexOf(value, StringComparison.OrdinalIgnoreCase); + + public static int InvariantLastIndexOf(this string s, string value) => + s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); + + /// + /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method + /// + /// + /// + /// + public static T? ParseInto(this string val) => (T?)val.ParseInto(typeof(T)); + + /// + /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method + /// + /// + /// + /// + public static object? ParseInto(this string val, Type type) + { + if (string.IsNullOrEmpty(val) == false) + { + TypeConverter tc = TypeDescriptor.GetConverter(type); + return tc.ConvertFrom(val); + } + + return val; + } + + /// + /// Generates a hash of a string based on the FIPS compliance setting. + /// + /// Refers to itself + /// The hashed string + public static string GenerateHash(this string str) => str.ToSHA1(); + + /// + /// Generate a hash of a string based on the specified hash algorithm. + /// + /// The hash algorithm implementation to use. + /// The to hash. + /// + /// The hashed string. + /// + public static string GenerateHash(this string str) + where T : HashAlgorithm => str.GenerateHash(typeof(T).FullName); + + /// + /// Converts the string to SHA1 + /// + /// refers to itself + /// The SHA1 hashed string + public static string ToSHA1(this string stringToConvert) => stringToConvert.GenerateHash("SHA1"); + + /// + /// Decodes a string that was encoded with UrlTokenEncode + /// + /// + /// + public static byte[] UrlTokenDecode(this string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + if (input.Length == 0) + { + return Array.Empty(); + } + + // calc array size - must be groups of 4 + var arrayLength = input.Length; + var remain = arrayLength % 4; + if (remain != 0) + { + arrayLength += 4 - remain; + } + + var inArray = new char[arrayLength]; + for (var i = 0; i < input.Length; i++) + { + var ch = input[i]; + switch (ch) { - sb.AppendFormat("{0:x2}", Convert.ToUInt32(c)); + case '-': // restore '-' as '+' + inArray[i] = '+'; + break; + + case '_': // restore '_' as '/' + inArray[i] = '/'; + break; + + default: // keep char unchanged + inArray[i] = ch; + break; } - return sb.ToString(); } - public static string DecodeFromHex(this string hexValue) + // pad with '=' + for (var j = input.Length; j < inArray.Length; j++) { - var strValue = ""; - while (hexValue.Length > 0) + inArray[j] = '='; + } + + return Convert.FromBase64CharArray(inArray, 0, inArray.Length); + } + + /// + /// Generate a hash of a string based on the hashType passed in + /// + /// Refers to itself + /// + /// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a + /// list of possible values. + /// + /// The hashed string + private static string GenerateHash(this string str, string? hashType) + { + HashAlgorithm? hasher = null; + + // create an instance of the correct hashing provider based on the type passed in + if (hashType is not null) + { + hasher = HashAlgorithm.Create(hashType); + } + + if (hasher == null) + { + throw new InvalidOperationException("No hashing type found by name " + hashType); + } + + using (hasher) + { + // convert our string into byte array + var byteArray = Encoding.UTF8.GetBytes(str); + + // get the hashed values created by our selected provider + var hashedByteArray = hasher.ComputeHash(byteArray); + + // create a StringBuilder object + var stringBuilder = new StringBuilder(); + + // loop to each byte + foreach (var b in hashedByteArray) { - strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); - hexValue = hexValue.Substring(2, hexValue.Length - 2); + // append it to our StringBuilder + stringBuilder.Append(b.ToString("x2")); } - return strValue; + + // return the hashed value + return stringBuilder.ToString(); + } + } + + /// + /// Encodes a string so that it is 'safe' for URLs, files, etc.. + /// + /// + /// + public static string UrlTokenEncode(this byte[] input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); } - /// - /// Encodes a string to a safe URL base64 string - /// - /// - /// - public static string ToUrlBase64(this string input) + if (input.Length == 0) { - if (input == null) throw new ArgumentNullException(nameof(input)); - - if (string.IsNullOrEmpty(input)) - return string.Empty; - - //return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ","); - var bytes = Encoding.UTF8.GetBytes(input); - return UrlTokenEncode(bytes); + return string.Empty; } - /// - /// Decodes a URL safe base64 string back - /// - /// - /// - public static string? FromUrlBase64(this string input) + // base-64 digits are A-Z, a-z, 0-9, + and / + // the = char is used for trailing padding + var str = Convert.ToBase64String(input); + + var pos = str.IndexOf('='); + if (pos < 0) { - if (input == null) throw new ArgumentNullException(nameof(input)); + pos = str.Length; + } - //if (input.IsInvalidBase64()) return null; - - try + // replace chars that would cause problems in URLs + var chArray = new char[pos]; + for (var i = 0; i < pos; i++) + { + var ch = str[i]; + switch (ch) { - //var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "=")); - var decodedBytes = UrlTokenDecode(input); - return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null; - } - catch (FormatException) - { - return null; + case '+': // replace '+' with '-' + chArray[i] = '-'; + break; + + case '/': // replace '/' with '_' + chArray[i] = '_'; + break; + + default: // keep char unchanged + chArray[i] = ch; + break; } } - /// - /// formats the string with invariant culture - /// - /// The format. - /// The args. - /// - public static string InvariantFormat(this string? format, params object?[] args) + return new string(chArray); + } + + /// + /// Ensures that the folder path ends with a DirectorySeparatorChar + /// + /// + /// + public static string NormaliseDirectoryPath(this string currentFolder) + { + currentFolder = currentFolder + .IfNull(x => string.Empty) + .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + return currentFolder; + } + + /// + /// Truncates the specified text string. + /// + /// The text. + /// Length of the max. + /// The suffix. + /// + public static string Truncate(this string text, int maxLength, string suffix = "...") + { + // replaces the truncated string to a ... + var truncatedString = text; + + if (maxLength <= 0) { - return string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, args); - } - - /// - /// Converts an integer to an invariant formatted string - /// - /// - /// - public static string ToInvariantString(this int str) - { - return str.ToString(CultureInfo.InvariantCulture); - } - - public static string ToInvariantString(this long str) - { - return str.ToString(CultureInfo.InvariantCulture); - } - - /// - /// Compares 2 strings with invariant culture and case ignored - /// - /// The compare. - /// The compare to. - /// - public static bool InvariantEquals(this string? compare, string? compareTo) - { - return String.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase); - } - - public static bool InvariantStartsWith(this string compare, string compareTo) - { - return compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); - } - - public static bool InvariantEndsWith(this string compare, string compareTo) - { - return compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase); - } - - public static bool InvariantContains(this string compare, string compareTo) - { - return compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0; - } - - public static bool InvariantContains(this IEnumerable compare, string compareTo) - { - return compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase); - } - - public static int InvariantIndexOf(this string s, string value) - { - return s.IndexOf(value, StringComparison.OrdinalIgnoreCase); - } - - public static int InvariantLastIndexOf(this string s, string value) - { - return s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); - } - - - /// - /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method - /// - /// - /// - /// - public static T? ParseInto(this string val) - { - return (T?)val.ParseInto(typeof(T)); - } - - /// - /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method - /// - /// - /// - /// - public static object? ParseInto(this string val, Type type) - { - if (string.IsNullOrEmpty(val) == false) - { - TypeConverter tc = TypeDescriptor.GetConverter(type); - return tc.ConvertFrom(val); - } - return val; - } - - /// - /// Generates a hash of a string based on the FIPS compliance setting. - /// - /// Refers to itself - /// The hashed string - public static string GenerateHash(this string str) => str.ToSHA1(); - - /// - /// Generate a hash of a string based on the specified hash algorithm. - /// - /// The hash algorithm implementation to use. - /// The to hash. - /// - /// The hashed string. - /// - public static string GenerateHash(this string str) - where T : HashAlgorithm => str.GenerateHash(typeof(T).FullName); - - /// - /// Converts the string to SHA1 - /// - /// refers to itself - /// The SHA1 hashed string - public static string ToSHA1(this string stringToConvert) => stringToConvert.GenerateHash("SHA1"); - - /// Generate a hash of a string based on the hashType passed in - /// - /// Refers to itself - /// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a list of possible values. - /// The hashed string - private static string GenerateHash(this string str, string? hashType) - { - HashAlgorithm? hasher = null; - //create an instance of the correct hashing provider based on the type passed in - if (hashType is not null) - { - hasher = HashAlgorithm.Create(hashType); - } - - if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType); - using (hasher) - { - //convert our string into byte array - var byteArray = Encoding.UTF8.GetBytes(str); - - //get the hashed values created by our selected provider - var hashedByteArray = hasher.ComputeHash(byteArray); - - //create a StringBuilder object - var stringBuilder = new StringBuilder(); - - //loop to each byte - foreach (var b in hashedByteArray) - { - //append it to our StringBuilder - stringBuilder.Append(b.ToString("x2")); - } - - //return the hashed value - return stringBuilder.ToString(); - } - } - - /// - /// Decodes a string that was encoded with UrlTokenEncode - /// - /// - /// - public static byte[] UrlTokenDecode(this string input) - { - if (input == null) - throw new ArgumentNullException(nameof(input)); - - if (input.Length == 0) - return Array.Empty(); - - // calc array size - must be groups of 4 - var arrayLength = input.Length; - var remain = arrayLength % 4; - if (remain != 0) arrayLength += 4 - remain; - - var inArray = new char[arrayLength]; - for (var i = 0; i < input.Length; i++) - { - var ch = input[i]; - switch (ch) - { - case '-': // restore '-' as '+' - inArray[i] = '+'; - break; - - case '_': // restore '_' as '/' - inArray[i] = '/'; - break; - - default: // keep char unchanged - inArray[i] = ch; - break; - } - } - - // pad with '=' - for (var j = input.Length; j < inArray.Length; j++) - inArray[j] = '='; - - return Convert.FromBase64CharArray(inArray, 0, inArray.Length); - } - - /// - /// Encodes a string so that it is 'safe' for URLs, files, etc.. - /// - /// - /// - public static string UrlTokenEncode(this byte[] input) - { - if (input == null) - throw new ArgumentNullException(nameof(input)); - - if (input.Length == 0) - return string.Empty; - - // base-64 digits are A-Z, a-z, 0-9, + and / - // the = char is used for trailing padding - - var str = Convert.ToBase64String(input); - - var pos = str.IndexOf('='); - if (pos < 0) pos = str.Length; - - // replace chars that would cause problems in URLs - var chArray = new char[pos]; - for (var i = 0; i < pos; i++) - { - var ch = str[i]; - switch (ch) - { - case '+': // replace '+' with '-' - chArray[i] = '-'; - break; - - case '/': // replace '/' with '_' - chArray[i] = '_'; - break; - - default: // keep char unchanged - chArray[i] = ch; - break; - } - } - - return new string(chArray); - } - - /// - /// Ensures that the folder path ends with a DirectorySeparatorChar - /// - /// - /// - public static string NormaliseDirectoryPath(this string currentFolder) - { - currentFolder = currentFolder - .IfNull(x => String.Empty) - .TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; - return currentFolder; - } - - /// - /// Truncates the specified text string. - /// - /// The text. - /// Length of the max. - /// The suffix. - /// - public static string Truncate(this string text, int maxLength, string suffix = "...") - { - // replaces the truncated string to a ... - var truncatedString = text; - - if (maxLength <= 0) return truncatedString; - var strLength = maxLength - suffix.Length; - - if (strLength <= 0) return truncatedString; - - if (text == null || text.Length <= maxLength) return truncatedString; - - truncatedString = text.Substring(0, strLength); - truncatedString = truncatedString.TrimEnd(); - truncatedString += suffix; - return truncatedString; } - /// - /// Strips carrage returns and line feeds from the specified text. - /// - /// The input. - /// - public static string StripNewLines(this string input) + var strLength = maxLength - suffix.Length; + + if (strLength <= 0) { - return input.Replace("\r", "").Replace("\n", ""); + return truncatedString; } - /// - /// Converts to single line by replacing line breaks with spaces. - /// - public static string ToSingleLine(this string text) + if (text == null || text.Length <= maxLength) + { + return truncatedString; + } + + truncatedString = text.Substring(0, strLength); + truncatedString = truncatedString.TrimEnd(); + truncatedString += suffix; + + return truncatedString; + } + + /// + /// Strips carrage returns and line feeds from the specified text. + /// + /// The input. + /// + public static string StripNewLines(this string input) => input.Replace("\r", string.Empty).Replace("\n", string.Empty); + + /// + /// Converts to single line by replacing line breaks with spaces. + /// + public static string ToSingleLine(this string text) + { + if (string.IsNullOrEmpty(text)) { - if (string.IsNullOrEmpty(text)) return text; - text = text.Replace("\r\n", " "); // remove CRLF - text = text.Replace("\r", " "); // remove CR - text = text.Replace("\n", " "); // remove LF return text; } - public static string OrIfNullOrWhiteSpace(this string input, string alternative) + text = text.Replace("\r\n", " "); // remove CRLF + text = text.Replace("\r", " "); // remove CR + text = text.Replace("\n", " "); // remove LF + return text; + } + + public static string OrIfNullOrWhiteSpace(this string input, string alternative) => + !string.IsNullOrWhiteSpace(input) + ? input + : alternative; + + /// + /// Returns a copy of the string with the first character converted to uppercase. + /// + /// The string. + /// The converted string. + public static string ToFirstUpper(this string input) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpper() + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to lowercase. + /// + /// The string. + /// The converted string. + public static string ToFirstLower(this string input) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLower() + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the + /// specified culture. + /// + /// The string. + /// The culture. + /// The converted string. + public static string ToFirstUpper(this string input, CultureInfo culture) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpper(culture) + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the + /// specified culture. + /// + /// The string. + /// The culture. + /// The converted string. + public static string ToFirstLower(this string input, CultureInfo culture) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLower(culture) + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the + /// invariant culture. + /// + /// The string. + /// The converted string. + public static string ToFirstUpperInvariant(this string input) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToUpperInvariant() + input.Substring(1); + + /// + /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the + /// invariant culture. + /// + /// The string. + /// The converted string. + public static string ToFirstLowerInvariant(this string input) => + string.IsNullOrWhiteSpace(input) + ? input + : input.Substring(0, 1).ToLowerInvariant() + input.Substring(1); + + /// + /// Returns a new string in which all occurrences of specified strings are replaced by other specified strings. + /// + /// The string to filter. + /// The replacements definition. + /// The filtered string. + public static string ReplaceMany(this string text, IDictionary replacements) + { + if (text == null) { - return !string.IsNullOrWhiteSpace(input) - ? input - : alternative; + throw new ArgumentNullException(nameof(text)); } - /// - /// Returns a copy of the string with the first character converted to uppercase. - /// - /// The string. - /// The converted string. - public static string ToFirstUpper(this string input) + if (replacements == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToUpper() + input.Substring(1); + throw new ArgumentNullException(nameof(replacements)); } - /// - /// Returns a copy of the string with the first character converted to lowercase. - /// - /// The string. - /// The converted string. - public static string ToFirstLower(this string input) + foreach (KeyValuePair item in replacements) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToLower() + input.Substring(1); + text = text.Replace(item.Key, item.Value); } - /// - /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the specified culture. - /// - /// The string. - /// The culture. - /// The converted string. - public static string ToFirstUpper(this string input, CultureInfo culture) + return text; + } + + /// + /// Returns a new string in which all occurrences of specified characters are replaced by a specified character. + /// + /// The string to filter. + /// The characters to replace. + /// The replacement character. + /// The filtered string. + public static string ReplaceMany(this string text, char[] chars, char replacement) + { + if (text == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToUpper(culture) + input.Substring(1); + throw new ArgumentNullException(nameof(text)); } - /// - /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the specified culture. - /// - /// The string. - /// The culture. - /// The converted string. - public static string ToFirstLower(this string input, CultureInfo culture) + if (chars == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToLower(culture) + input.Substring(1); + throw new ArgumentNullException(nameof(chars)); } - /// - /// Returns a copy of the string with the first character converted to uppercase using the casing rules of the invariant culture. - /// - /// The string. - /// The converted string. - public static string ToFirstUpperInvariant(this string input) + for (var i = 0; i < chars.Length; i++) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToUpperInvariant() + input.Substring(1); + text = text.Replace(chars[i], replacement); } - /// - /// Returns a copy of the string with the first character converted to lowercase using the casing rules of the invariant culture. - /// - /// The string. - /// The converted string. - public static string ToFirstLowerInvariant(this string input) + return text; + } + + /// + /// Returns a new string in which only the first occurrence of a specified string is replaced by a specified + /// replacement string. + /// + /// The string to filter. + /// The string to replace. + /// The replacement string. + /// The filtered string. + public static string ReplaceFirst(this string text, string search, string replace) + { + if (text == null) { - return string.IsNullOrWhiteSpace(input) - ? input - : input.Substring(0, 1).ToLowerInvariant() + input.Substring(1); + throw new ArgumentNullException(nameof(text)); } - /// - /// Returns a new string in which all occurrences of specified strings are replaced by other specified strings. - /// - /// The string to filter. - /// The replacements definition. - /// The filtered string. - public static string ReplaceMany(this string text, IDictionary replacements) + var pos = text.IndexOf(search, StringComparison.InvariantCulture); + + if (pos < 0) { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (replacements == null) throw new ArgumentNullException(nameof(replacements)); - - - foreach (KeyValuePair item in replacements) - text = text.Replace(item.Key, item.Value); - return text; } - /// - /// Returns a new string in which all occurrences of specified characters are replaced by a specified character. - /// - /// The string to filter. - /// The characters to replace. - /// The replacement character. - /// The filtered string. - public static string ReplaceMany(this string text, char[] chars, char replacement) + return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + } + + /// + /// An extension method that returns a new string in which all occurrences of a + /// specified string in the current instance are replaced with another specified string. + /// StringComparison specifies the type of search to use for the specified string. + /// + /// Current instance of the string + /// Specified string to replace + /// Specified string to inject + /// String Comparison object to specify search type + /// Updated string + public static string Replace(this string source, string oldString, string newString, StringComparison stringComparison) + { + // This initialization ensures the first check starts at index zero of the source. On successive checks for + // a match, the source is skipped to immediately after the last replaced occurrence for efficiency + // and to avoid infinite loops when oldString and newString compare equal. + var index = -1 * newString.Length; + + // Determine if there are any matches left in source, starting from just after the result of replacing the last match. + while ((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0) { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (chars == null) throw new ArgumentNullException(nameof(chars)); + // Remove the old text. + source = source.Remove(index, oldString.Length); - - for (int i = 0; i < chars.Length; i++) - text = text.Replace(chars[i], replacement); - - return text; + // Add the replacement text. + source = source.Insert(index, newString); } - /// - /// Returns a new string in which only the first occurrence of a specified string is replaced by a specified replacement string. - /// - /// The string to filter. - /// The string to replace. - /// The replacement string. - /// The filtered string. - public static string ReplaceFirst(this string text, string search, string replace) + return source; + } + + /// + /// Converts a literal string into a C# expression. + /// + /// Current instance of the string. + /// The string in a C# format. + public static string ToCSharpString(this string s) + { + if (s == null) { - if (text == null) throw new ArgumentNullException(nameof(text)); - - var pos = text.IndexOf(search, StringComparison.InvariantCulture); - - if (pos < 0) - return text; - - return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + return ""; } - - - /// - /// An extension method that returns a new string in which all occurrences of a - /// specified string in the current instance are replaced with another specified string. - /// StringComparison specifies the type of search to use for the specified string. - /// - /// Current instance of the string - /// Specified string to replace - /// Specified string to inject - /// String Comparison object to specify search type - /// Updated string - public static string Replace(this string source, string oldString, string newString, StringComparison stringComparison) + // http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal + var sb = new StringBuilder(s.Length + 2); + for (var rp = 0; rp < s.Length; rp++) { - // This initialization ensures the first check starts at index zero of the source. On successive checks for - // a match, the source is skipped to immediately after the last replaced occurrence for efficiency - // and to avoid infinite loops when oldString and newString compare equal. - int index = -1 * newString.Length; - - // Determine if there are any matches left in source, starting from just after the result of replacing the last match. - while ((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0) + var c = s[rp]; + if (c < ToCSharpEscapeChars.Length && ToCSharpEscapeChars[c] != '\0') { - // Remove the old text. - source = source.Remove(index, oldString.Length); - - // Add the replacement text. - source = source.Insert(index, newString); + sb.Append('\\').Append(ToCSharpEscapeChars[c]); } - - return source; - } - - /// - /// Converts a literal string into a C# expression. - /// - /// Current instance of the string. - /// The string in a C# format. - public static string ToCSharpString(this string s) - { - if (s == null) return ""; - - // http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal - - var sb = new StringBuilder(s.Length + 2); - for (var rp = 0; rp < s.Length; rp++) + else if (c <= '~' && c >= ' ') { - var c = s[rp]; - if (c < ToCSharpEscapeChars.Length && '\0' != ToCSharpEscapeChars[c]) - sb.Append('\\').Append(ToCSharpEscapeChars[c]); - else if ('~' >= c && c >= ' ') - sb.Append(c); - else - sb.Append(@"\x") - .Append(ToCSharpHexDigitLower[c >> 12 & 0x0F]) - .Append(ToCSharpHexDigitLower[c >> 8 & 0x0F]) - .Append(ToCSharpHexDigitLower[c >> 4 & 0x0F]) - .Append(ToCSharpHexDigitLower[c & 0x0F]); + sb.Append(c); } - - return sb.ToString(); - - // requires full trust - /* - using (var writer = new StringWriter()) - using (var provider = CodeDomProvider.CreateProvider("CSharp")) + else { - provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null); - return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), ""); + sb.Append(@"\x") + .Append(ToCSharpHexDigitLower[(c >> 12) & 0x0F]) + .Append(ToCSharpHexDigitLower[(c >> 8) & 0x0F]) + .Append(ToCSharpHexDigitLower[(c >> 4) & 0x0F]) + .Append(ToCSharpHexDigitLower[c & 0x0F]); } - */ } - public static string EscapeRegexSpecialCharacters(this string text) + return sb.ToString(); + + // requires full trust + /* + using (var writer = new StringWriter()) + using (var provider = CodeDomProvider.CreateProvider("CSharp")) { - var regexSpecialCharacters = new Dictionary - { - {".", @"\."}, - {"(", @"\("}, - {")", @"\)"}, - {"]", @"\]"}, - {"[", @"\["}, - {"{", @"\{"}, - {"}", @"\}"}, - {"?", @"\?"}, - {"!", @"\!"}, - {"$", @"\$"}, - {"^", @"\^"}, - {"+", @"\+"}, - {"*", @"\*"}, - {"|", @"\|"}, - {"<", @"\<"}, - {">", @"\>"} - }; - return ReplaceMany(text, regexSpecialCharacters); + provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null); + return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), ""); + } + */ + } + + public static string EscapeRegexSpecialCharacters(this string text) + { + var regexSpecialCharacters = new Dictionary + { + { ".", @"\." }, + { "(", @"\(" }, + { ")", @"\)" }, + { "]", @"\]" }, + { "[", @"\[" }, + { "{", @"\{" }, + { "}", @"\}" }, + { "?", @"\?" }, + { "!", @"\!" }, + { "$", @"\$" }, + { "^", @"\^" }, + { "+", @"\+" }, + { "*", @"\*" }, + { "|", @"\|" }, + { "<", @"\<" }, + { ">", @"\>" }, + }; + return ReplaceMany(text, regexSpecialCharacters); + } + + /// + /// Checks whether a string "haystack" contains within it any of the strings in the "needles" collection and returns + /// true if it does or false if it doesn't + /// + /// The string to check + /// The collection of strings to check are contained within the first string + /// + /// The type of comparison to perform - defaults to + /// + /// True if any of the needles are contained with haystack; otherwise returns false + /// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313 + public static bool ContainsAny(this string haystack, IEnumerable needles, StringComparison comparison = StringComparison.CurrentCulture) + { + if (haystack == null) + { + throw new ArgumentNullException("haystack"); } - /// - /// Checks whether a string "haystack" contains within it any of the strings in the "needles" collection and returns true if it does or false if it doesn't - /// - /// The string to check - /// The collection of strings to check are contained within the first string - /// The type of comparison to perform - defaults to - /// True if any of the needles are contained with haystack; otherwise returns false - /// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313 - public static bool ContainsAny(this string haystack, IEnumerable needles, StringComparison comparison = StringComparison.CurrentCulture) + if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any()) { - if (haystack == null) - throw new ArgumentNullException("haystack"); + return false; + } - if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any()) + return needles.Any(value => haystack.IndexOf(value, comparison) >= 0); + } + + public static bool CsvContains(this string csv, string value) + { + if (string.IsNullOrEmpty(csv)) + { + return false; + } + + var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return idCheckList.Contains(value); + } + + /// + /// Converts a file name to a friendly name for a content item + /// + /// + /// + public static string ToFriendlyName(this string fileName) + { + // strip the file extension + fileName = fileName.StripFileExtension(); + + // underscores and dashes to spaces + fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' '); + + // any other conversions ? + + // Pascalcase (to be done last) + fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName); + + // Replace multiple consecutive spaces with a single space + fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries)); + + return fileName; + } + + /// + /// An extension method that returns a new string in which all occurrences of an + /// unicode characters that are invalid in XML files are replaced with an empty string. + /// + /// Current instance of the string + /// Updated string + /// + /// removes any unusual unicode characters that can't be encoded into XML + /// + public static string ToValidXmlString(this string text) => + string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Value.Replace(text, string.Empty); + + /// + /// Converts a string to a Guid - WARNING, depending on the string, this may not be unique + /// + /// + /// + public static Guid ToGuid(this string text) => + CreateGuidFromHash( + UrlNamespace, + text, + CryptoConfig.AllowOnlyFipsAlgorithms ? 5 // SHA1 + : 3); // MD5 + + /// + /// Turns an null-or-whitespace string into a null string. + /// + public static string? NullOrWhiteSpaceAsNull(this string text) + => string.IsNullOrWhiteSpace(text) ? null : text; + + /// + /// Creates a name-based UUID using the algorithm from RFC 4122 §4.3. + /// See + /// GuidUtility.cs + /// for original implementation. + /// + /// The ID of the namespace. + /// The name (within that namespace). + /// + /// The version number of the UUID to create; this value must be either + /// 3 (for MD5 hashing) or 5 (for SHA-1 hashing). + /// + /// A UUID derived from the namespace and name. + /// + /// See + /// Generating a deterministic GUID + /// . + /// + internal static Guid CreateGuidFromHash(Guid namespaceId, string name, int version) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (version != 3 && version != 5) + { + throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5."); + } + + // convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3) + // ASSUME: UTF-8 encoding is always appropriate + var nameBytes = Encoding.UTF8.GetBytes(name); + + // convert the namespace UUID to network order (step 3) + var namespaceBytes = namespaceId.ToByteArray(); + SwapByteOrder(namespaceBytes); + + // comput the hash of the name space ID concatenated with the name (step 4) + byte[] hash; + using (HashAlgorithm algorithm = version == 3 ? MD5.Create() : SHA1.Create()) + { + algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0); + algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length); + hash = algorithm.Hash!; + } + + // most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12) + var newGuid = new byte[16]; + Array.Copy(hash, 0, newGuid, 0, 16); + + // set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8) + newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4)); + + // set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10) + newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80); + + // convert the resulting UUID to local byte order (step 13) + SwapByteOrder(newGuid); + return new Guid(newGuid); + } + + // Converts a GUID (expressed as a byte array) to/from network order (MSB-first). + internal static void SwapByteOrder(byte[] guid) + { + SwapBytes(guid, 0, 3); + SwapBytes(guid, 1, 2); + SwapBytes(guid, 4, 5); + SwapBytes(guid, 6, 7); + } + + private static void SwapBytes(byte[] guid, int left, int right) + { + var temp = guid[left]; + guid[left] = guid[right]; + guid[right] = temp; + } + + /// + /// Checks if a given path is a full path including drive letter + /// + /// + /// + // From: http://stackoverflow.com/a/35046453/5018 + public static bool IsFullPath(this string path) => + string.IsNullOrWhiteSpace(path) == false + && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 + && Path.IsPathRooted(path) + && Path.GetPathRoot(path)?.Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; + + // FORMAT STRINGS + + /// + /// Cleans a string to produce a string that can safely be used in an alias. + /// + /// The text to filter. + /// The short string helper. + /// The safe alias. + public static string ToSafeAlias(this string alias, IShortStringHelper? shortStringHelper) => + shortStringHelper?.CleanStringForSafeAlias(alias) ?? string.Empty; + + /// + /// Cleans a string to produce a string that can safely be used in an alias. + /// + /// The text to filter. + /// A value indicating that we want to camel-case the alias. + /// The short string helper. + /// The safe alias. + public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, bool camel) + { + var a = shortStringHelper.CleanStringForSafeAlias(alias); + if (string.IsNullOrWhiteSpace(a) || camel == false) + { + return a; + } + + return char.ToLowerInvariant(a[0]) + a.Substring(1); + } + + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. + /// + /// The text to filter. + /// The culture. + /// The short string helper. + /// The safe alias. + public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, string culture) => + shortStringHelper.CleanStringForSafeAlias(alias, culture); + + // the new methods to get a url segment + + /// + /// Cleans a string to produce a string that can safely be used in an url segment. + /// + /// The text to filter. + /// The short string helper. + /// The safe url segment. + public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(text)); + } + + return shortStringHelper.CleanStringForUrlSegment(text); + } + + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url + /// segment. + /// + /// The text to filter. + /// The short string helper. + /// The culture. + /// The safe url segment. + public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper, string? culture) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(text)); + } + + return shortStringHelper.CleanStringForUrlSegment(text, culture); + } + + /// + /// Cleans a string. + /// + /// The text to clean. + /// The short string helper. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The clean string. + /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. + public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType) => shortStringHelper.CleanString(text, stringType); + + /// + /// Cleans a string, using a specified separator. + /// + /// The text to clean. + /// The short string helper. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The separator. + /// The clean string. + /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. + public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator) => shortStringHelper.CleanString(text, stringType, separator); + + /// + /// Cleans a string in the context of a specified culture. + /// + /// The text to clean. + /// The short string helper. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The culture. + /// The clean string. + public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, string culture) => shortStringHelper.CleanString(text, stringType, culture); + + /// + /// Cleans a string in the context of a specified culture, using a specified separator. + /// + /// The text to clean. + /// The short string helper. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The separator. + /// The culture. + /// The clean string. + public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator, string culture) => + shortStringHelper.CleanString(text, stringType, separator, culture); + + // note: LegacyCurrent.ShortStringHelper will produce 100% backward-compatible output for SplitPascalCasing. + // other helpers may not. DefaultCurrent.ShortStringHelper produces better, but non-compatible, results. + + /// + /// Splits a Pascal cased string into a phrase separated by spaces. + /// + /// The text to split. + /// + /// The split text. + public static string SplitPascalCasing(this string phrase, IShortStringHelper shortStringHelper) => + shortStringHelper.SplitPascalCasing(phrase, ' '); + + /// + /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a + /// filename, + /// both internally (on disk) and externally (as a url). + /// + /// The text to filter. + /// + /// The safe filename. + public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper) => + shortStringHelper.CleanStringForSafeFileName(text); + + // NOTE: Not sure what this actually does but is used a few places, need to figure it out and then move to StringExtensions and obsolete. + // it basically is yet another version of SplitPascalCasing + // plugging string extensions here to be 99% compatible + // the only diff. is with numbers, Number6Is was "Number6 Is", and the new string helper does it too, + // but the legacy one does "Number6Is"... assuming it is not a big deal. + internal static string SpaceCamelCasing(this string phrase, IShortStringHelper shortStringHelper) => + phrase.Length < 2 ? phrase : phrase.SplitPascalCasing(shortStringHelper).ToFirstUpperInvariant(); + + /// + /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a + /// filename, + /// both internally (on disk) and externally (as a url). + /// + /// The text to filter. + /// + /// The culture. + /// The safe filename. + public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper, string culture) => + shortStringHelper.CleanStringForSafeFileName(text, culture); + + /// + /// Splits a string with an escape character that allows for the split character to exist in a string + /// + /// The string to split + /// The character to split on + /// The character which can be used to escape the character to split on + /// The string split into substrings delimited by the split character + public static IEnumerable EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar) + { + if (value == null) + { + yield break; + } + + var sb = new StringBuilder(value.Length); + var escaped = false; + + foreach (var chr in value.ToCharArray()) + { + if (escaped) { - return false; + escaped = false; + sb.Append(chr); } - - return needles.Any(value => haystack.IndexOf(value, comparison) >= 0); - } - - public static bool CsvContains(this string csv, string value) - { - if (string.IsNullOrEmpty(csv)) + else if (chr == splitChar) { - return false; + yield return sb.ToString(); + sb.Clear(); } - var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - return idCheckList.Contains(value); - } - - /// - /// Converts a file name to a friendly name for a content item - /// - /// - /// - public static string ToFriendlyName(this string fileName) - { - // strip the file extension - fileName = fileName.StripFileExtension(); - - // underscores and dashes to spaces - fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' '); - - // any other conversions ? - - // Pascalcase (to be done last) - fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName); - - // Replace multiple consecutive spaces with a single space - fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries)); - - return fileName; - } - - // From: http://stackoverflow.com/a/961504/5018 - // filters control characters but allows only properly-formed surrogate sequences - private static readonly Lazy InvalidXmlChars = new Lazy(() => - new Regex( - @"(? - /// An extension method that returns a new string in which all occurrences of an - /// unicode characters that are invalid in XML files are replaced with an empty string. - /// - /// Current instance of the string - /// Updated string - /// - /// - /// removes any unusual unicode characters that can't be encoded into XML - /// - public static string ToValidXmlString(this string text) - { - return string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Value.Replace(text, ""); - } - - /// - /// Converts a string to a Guid - WARNING, depending on the string, this may not be unique - /// - /// - /// - public static Guid ToGuid(this string text) - { - return CreateGuidFromHash(UrlNamespace, - text, - CryptoConfig.AllowOnlyFipsAlgorithms - ? 5 // SHA1 - : 3); // MD5 - } - - /// - /// The namespace for URLs (from RFC 4122, Appendix C). - /// - /// See RFC 4122 - /// - internal static readonly Guid UrlNamespace = new Guid("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); - - /// - /// Creates a name-based UUID using the algorithm from RFC 4122 §4.3. - /// - /// See GuidUtility.cs for original implementation. - /// - /// The ID of the namespace. - /// The name (within that namespace). - /// The version number of the UUID to create; this value must be either - /// 3 (for MD5 hashing) or 5 (for SHA-1 hashing). - /// A UUID derived from the namespace and name. - /// See Generating a deterministic GUID. - internal static Guid CreateGuidFromHash(Guid namespaceId, string name, int version) - { - if (name == null) - throw new ArgumentNullException("name"); - if (version != 3 && version != 5) - throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5."); - - // convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3) - // ASSUME: UTF-8 encoding is always appropriate - byte[] nameBytes = Encoding.UTF8.GetBytes(name); - - // convert the namespace UUID to network order (step 3) - byte[] namespaceBytes = namespaceId.ToByteArray(); - SwapByteOrder(namespaceBytes); - - // comput the hash of the name space ID concatenated with the name (step 4) - byte[] hash; - using (HashAlgorithm algorithm = version == 3 ? (HashAlgorithm)MD5.Create() : SHA1.Create()) + else if (chr == escapeChar) { - algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0); - algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length); - hash = algorithm.Hash!; + escaped = true; } - - // most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12) - byte[] newGuid = new byte[16]; - Array.Copy(hash, 0, newGuid, 0, 16); - - // set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8) - newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4)); - - // set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10) - newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80); - - // convert the resulting UUID to local byte order (step 13) - SwapByteOrder(newGuid); - return new Guid(newGuid); - } - - // Converts a GUID (expressed as a byte array) to/from network order (MSB-first). - internal static void SwapByteOrder(byte[] guid) - { - SwapBytes(guid, 0, 3); - SwapBytes(guid, 1, 2); - SwapBytes(guid, 4, 5); - SwapBytes(guid, 6, 7); - } - - private static void SwapBytes(byte[] guid, int left, int right) - { - byte temp = guid[left]; - guid[left] = guid[right]; - guid[right] = temp; - } - - /// - /// Turns an null-or-whitespace string into a null string. - /// - public static string? NullOrWhiteSpaceAsNull(this string text) - => string.IsNullOrWhiteSpace(text) ? null : text; - - - /// - /// Checks if a given path is a full path including drive letter - /// - /// - /// - // From: http://stackoverflow.com/a/35046453/5018 - public static bool IsFullPath(this string path) - { - return string.IsNullOrWhiteSpace(path) == false - && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 - && Path.IsPathRooted(path) - && Path.GetPathRoot(path)?.Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; - } - - // FORMAT STRINGS - - /// - /// Cleans a string to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The short string helper. - /// The safe alias. - public static string ToSafeAlias(this string alias, IShortStringHelper? shortStringHelper) - { - return shortStringHelper?.CleanStringForSafeAlias(alias) ?? string.Empty; - } - - /// - /// Cleans a string to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// A value indicating that we want to camel-case the alias. - /// The short string helper. - /// The safe alias. - public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, bool camel) - { - var a = shortStringHelper.CleanStringForSafeAlias(alias); - if (string.IsNullOrWhiteSpace(a) || camel == false) return a; - return char.ToLowerInvariant(a[0]) + a.Substring(1); - } - - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The culture. - /// The short string helper. - /// The safe alias. - public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, string culture) - { - return shortStringHelper.CleanStringForSafeAlias(alias, culture); - } - - - // the new methods to get a url segment - - /// - /// Cleans a string to produce a string that can safely be used in an url segment. - /// - /// The text to filter. - /// The short string helper. - /// The safe url segment. - public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); - - return shortStringHelper.CleanStringForUrlSegment(text); - } - - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url segment. - /// - /// The text to filter. - /// The short string helper. - /// The culture. - /// The safe url segment. - public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper, string? culture) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); - - return shortStringHelper.CleanStringForUrlSegment(text, culture); - } - - - /// - /// Cleans a string. - /// - /// The text to clean. - /// The short string helper. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The clean string. - /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. - public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType) - { - return shortStringHelper.CleanString(text, stringType); - } - - /// - /// Cleans a string, using a specified separator. - /// - /// The text to clean. - /// The short string helper. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The separator. - /// The clean string. - /// The string is cleaned in the context of the ICurrent.ShortStringHelper default culture. - public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator) - { - return shortStringHelper.CleanString(text, stringType, separator); - } - - /// - /// Cleans a string in the context of a specified culture. - /// - /// The text to clean. - /// The short string helper. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The culture. - /// The clean string. - public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, string culture) - { - return shortStringHelper.CleanString(text, stringType, culture); - } - - /// - /// Cleans a string in the context of a specified culture, using a specified separator. - /// - /// The text to clean. - /// The short string helper. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The separator. - /// The culture. - /// The clean string. - public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator, string culture) - { - return shortStringHelper.CleanString(text, stringType, separator, culture); - } - - // note: LegacyCurrent.ShortStringHelper will produce 100% backward-compatible output for SplitPascalCasing. - // other helpers may not. DefaultCurrent.ShortStringHelper produces better, but non-compatible, results. - - /// - /// Splits a Pascal cased string into a phrase separated by spaces. - /// - /// The text to split. - /// - /// The split text. - public static string SplitPascalCasing(this string phrase, IShortStringHelper shortStringHelper) - { - return shortStringHelper.SplitPascalCasing(phrase, ' '); - } - - //NOTE: Not sure what this actually does but is used a few places, need to figure it out and then move to StringExtensions and obsolete. - // it basically is yet another version of SplitPascalCasing - // plugging string extensions here to be 99% compatible - // the only diff. is with numbers, Number6Is was "Number6 Is", and the new string helper does it too, - // but the legacy one does "Number6Is"... assuming it is not a big deal. - internal static string SpaceCamelCasing(this string phrase, IShortStringHelper shortStringHelper) - { - return phrase.Length < 2 ? phrase : phrase.SplitPascalCasing(shortStringHelper).ToFirstUpperInvariant(); - } - - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). - /// - /// The text to filter. - /// - /// The safe filename. - public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper) - { - return shortStringHelper.CleanStringForSafeFileName(text); - } - - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). - /// - /// The text to filter. - /// - /// The culture. - /// The safe filename. - public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper, string culture) - { - return shortStringHelper.CleanStringForSafeFileName(text, culture); - } - - /// - /// Splits a string with an escape character that allows for the split character to exist in a string - /// - /// The string to split - /// The character to split on - /// The character which can be used to escape the character to split on - /// The string split into substrings delimited by the split character - public static IEnumerable EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar) - { - if (value == null) yield break; - - var sb = new StringBuilder(value.Length); - var escaped = false; - - foreach (var chr in value.ToCharArray()) + else { - if (escaped) - { - escaped = false; - sb.Append(chr); - } - else if (chr == splitChar) - { - yield return sb.ToString(); - sb.Clear(); - } - else if (chr == escapeChar) - { - escaped = true; - } - else - { - sb.Append(chr); - } + sb.Append(chr); } - - yield return sb.ToString(); } + + yield return sb.ToString(); } } diff --git a/src/Umbraco.Core/Extensions/ThreadExtensions.cs b/src/Umbraco.Core/Extensions/ThreadExtensions.cs index 1c585a2de8..b1e5515b88 100644 --- a/src/Umbraco.Core/Extensions/ThreadExtensions.cs +++ b/src/Umbraco.Core/Extensions/ThreadExtensions.cs @@ -1,54 +1,54 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.Globalization; -using System.Threading; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ThreadExtensions { - public static class ThreadExtensions + public static void SanitizeThreadCulture(this Thread thread) { - public static void SanitizeThreadCulture(this Thread thread) + // get the current culture + CultureInfo currentCulture = CultureInfo.CurrentCulture; + + // at the top of any culture should be the invariant culture - find it + // doing an .Equals comparison ensure that we *will* find it and not loop + // endlessly + CultureInfo invariantCulture = currentCulture; + while (invariantCulture.Equals(CultureInfo.InvariantCulture) == false) { - // get the current culture - var currentCulture = CultureInfo.CurrentCulture; - - // at the top of any culture should be the invariant culture - find it - // doing an .Equals comparison ensure that we *will* find it and not loop - // endlessly - var invariantCulture = currentCulture; - while (invariantCulture.Equals(CultureInfo.InvariantCulture) == false) - invariantCulture = invariantCulture.Parent; - - // now that invariant culture should be the same object as CultureInfo.InvariantCulture - // yet for some reasons, sometimes it is not - and this breaks anything that loops on - // culture.Parent until a reference equality to CultureInfo.InvariantCulture. See, for - // example, the following code in PerformanceCounterLib.IsCustomCategory: - // - // CultureInfo culture = CultureInfo.CurrentCulture; - // while (culture != CultureInfo.InvariantCulture) - // { - // library = GetPerformanceCounterLib(machine, culture); - // if (library.IsCustomCategory(category)) - // return true; - // culture = culture.Parent; - // } - // - // The reference comparisons never succeeds, hence the loop never ends, and the - // application hangs. - // - // granted, that comparison should probably be a .Equals comparison, but who knows - // how many times the framework assumes that it can do a reference comparison? So, - // better fix the cultures. - - if (ReferenceEquals(invariantCulture, CultureInfo.InvariantCulture)) - return; - - // if we do not have equality, fix cultures by replacing them with a culture with - // the same name, but obtained here and now, with a proper invariant top culture - - thread.CurrentCulture = CultureInfo.GetCultureInfo(thread.CurrentCulture.Name); - thread.CurrentUICulture = CultureInfo.GetCultureInfo(thread.CurrentUICulture.Name); + invariantCulture = invariantCulture.Parent; } + + // now that invariant culture should be the same object as CultureInfo.InvariantCulture + // yet for some reasons, sometimes it is not - and this breaks anything that loops on + // culture.Parent until a reference equality to CultureInfo.InvariantCulture. See, for + // example, the following code in PerformanceCounterLib.IsCustomCategory: + // + // CultureInfo culture = CultureInfo.CurrentCulture; + // while (culture != CultureInfo.InvariantCulture) + // { + // library = GetPerformanceCounterLib(machine, culture); + // if (library.IsCustomCategory(category)) + // return true; + // culture = culture.Parent; + // } + // + // The reference comparisons never succeeds, hence the loop never ends, and the + // application hangs. + // + // granted, that comparison should probably be a .Equals comparison, but who knows + // how many times the framework assumes that it can do a reference comparison? So, + // better fix the cultures. + if (ReferenceEquals(invariantCulture, CultureInfo.InvariantCulture)) + { + return; + } + + // if we do not have equality, fix cultures by replacing them with a culture with + // the same name, but obtained here and now, with a proper invariant top culture + thread.CurrentCulture = CultureInfo.GetCultureInfo(thread.CurrentCulture.Name); + thread.CurrentUICulture = CultureInfo.GetCultureInfo(thread.CurrentUICulture.Name); } } diff --git a/src/Umbraco.Core/Extensions/TypeExtensions.cs b/src/Umbraco.Core/Extensions/TypeExtensions.cs index bb43c2b5d9..e3da8d9ee1 100644 --- a/src/Umbraco.Core/Extensions/TypeExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeExtensions.cs @@ -1,492 +1,515 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TypeExtensions { - public static class TypeExtensions + public static object? GetDefaultValue(this Type t) => + t.IsValueType + ? Activator.CreateInstance(t) + : null; + + /// + /// Checks if the type is an anonymous type + /// + /// + /// + /// + /// reference: http://jclaes.blogspot.com/2011/05/checking-for-anonymous-types.html + /// + public static bool IsAnonymousType(this Type type) { - public static object? GetDefaultValue(this Type t) + if (type == null) { - return t.IsValueType - ? Activator.CreateInstance(t) - : null; + throw new ArgumentNullException("type"); } - internal static MethodInfo? GetGenericMethod(this Type type, string name, params Type[] parameterTypes) - { - var methods = type.GetMethods().Where(method => method.Name == name); + return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false) + && type.IsGenericType && type.Name.Contains("AnonymousType") + && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$")) + && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic; + } - foreach (var method in methods) + public static IEnumerable GetBaseTypes(this Type? type, bool andSelf) + { + if (andSelf) + { + yield return type; + } + + while ((type = type?.BaseType) != null) + { + yield return type; + } + } + + internal static MethodInfo? GetGenericMethod(this Type type, string name, params Type[] parameterTypes) + { + IEnumerable methods = type.GetMethods().Where(method => method.Name == name); + + foreach (MethodInfo method in methods) + { + if (method.HasParameters(parameterTypes)) { - if (method.HasParameters(parameterTypes)) - return method; + return method; + } + } + + return null; + } + + /// + /// Determines whether the specified type is enumerable. + /// + /// The type. + /// + internal static bool HasParameters(this MethodInfo method, params Type[] parameterTypes) + { + Type[] methodParameters = method.GetParameters().Select(parameter => parameter.ParameterType).ToArray(); + + if (methodParameters.Length != parameterTypes.Length) + { + return false; + } + + for (var i = 0; i < methodParameters.Length; i++) + { + if (methodParameters[i].ToString() != parameterTypes[i].ToString()) + { + return false; + } + } + + return true; + } + + public static IEnumerable AllMethods(this Type target) + { + // var allTypes = target.AllInterfaces().ToList(); + var allTypes = target.GetInterfaces().ToList(); // GetInterfaces is ok here + allTypes.Add(target); + + return allTypes.SelectMany(t => t.GetMethods()); + } + + /// + /// true if the specified type is enumerable; otherwise, false. + /// + public static bool IsEnumerable(this Type type) + { + if (type.IsGenericType) + { + if (type.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable))) + { + return true; + } + } + else + { + if (type.GetInterfaces().Contains(typeof(IEnumerable))) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether [is of generic type] [the specified type]. + /// + /// The type. + /// Type of the generic. + /// + /// true if [is of generic type] [the specified type]; otherwise, false. + /// + public static bool IsOfGenericType(this Type type, Type genericType) + { + return type.TryGetGenericArguments(genericType, out Type[]? args); + } + + /// + /// Will find the generic type of the 'type' parameter passed in that is equal to the 'genericType' parameter passed in + /// + /// + /// + /// + /// + public static bool TryGetGenericArguments(this Type type, Type genericType, out Type[]? genericArgType) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + + if (genericType == null) + { + throw new ArgumentNullException("genericType"); + } + + if (genericType.IsGenericType == false) + { + throw new ArgumentException("genericType must be a generic type"); + } + + Func checkGenericType = (@int, t) => + { + if (@int.IsGenericType) + { + Type def = @int.GetGenericTypeDefinition(); + if (def == t) + { + return @int.GetGenericArguments(); + } } return null; - } + }; - /// - /// Checks if the type is an anonymous type - /// - /// - /// - /// - /// reference: http://jclaes.blogspot.com/2011/05/checking-for-anonymous-types.html - /// - public static bool IsAnonymousType(this Type type) + // first, check if the type passed in is already the generic type + genericArgType = checkGenericType(type, genericType); + if (genericArgType != null) { - if (type == null) throw new ArgumentNullException("type"); - - - return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false) - && type.IsGenericType && type.Name.Contains("AnonymousType") - && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$")) - && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic; - } - - - - /// - /// Determines whether the specified type is enumerable. - /// - /// The type. - /// - internal static bool HasParameters(this MethodInfo method, params Type[] parameterTypes) - { - var methodParameters = method.GetParameters().Select(parameter => parameter.ParameterType).ToArray(); - - if (methodParameters.Length != parameterTypes.Length) - return false; - - for (int i = 0; i < methodParameters.Length; i++) - if (methodParameters[i].ToString() != parameterTypes[i].ToString()) - return false; - return true; } - public static IEnumerable GetBaseTypes(this Type? type, bool andSelf) + // if we're looking for interfaces, enumerate them: + if (genericType.IsInterface) { - if (andSelf) - yield return type; - - while ((type = type?.BaseType) != null) - yield return type; - } - - public static IEnumerable AllMethods(this Type target) - { - //var allTypes = target.AllInterfaces().ToList(); - var allTypes = target.GetInterfaces().ToList(); // GetInterfaces is ok here - allTypes.Add(target); - - return allTypes.SelectMany(t => t.GetMethods()); - } - - /// - /// true if the specified type is enumerable; otherwise, false. - /// - public static bool IsEnumerable(this Type type) - { - if (type.IsGenericType) + foreach (Type @interface in type.GetInterfaces()) { - if (type.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable))) + genericArgType = checkGenericType(@interface, genericType); + if (genericArgType != null) + { return true; + } } - else + } + else + { + // loop back into the base types as long as they are generic + while (type.BaseType != null && type.BaseType != typeof(object)) { - if (type.GetInterfaces().Contains(typeof(IEnumerable))) + genericArgType = checkGenericType(type.BaseType, genericType); + if (genericArgType != null) + { return true; + } + + type = type.BaseType; } - return false; } - /// - /// Determines whether [is of generic type] [the specified type]. - /// - /// The type. - /// Type of the generic. - /// - /// true if [is of generic type] [the specified type]; otherwise, false. - /// - public static bool IsOfGenericType(this Type type, Type genericType) - { - Type[]? args; - return type.TryGetGenericArguments(genericType, out args); - } + return false; + } - /// - /// Will find the generic type of the 'type' parameter passed in that is equal to the 'genericType' parameter passed in - /// - /// - /// - /// - /// - public static bool TryGetGenericArguments(this Type type, Type genericType, out Type[]? genericArgType) + /// + /// Gets all properties in a flat hierarchy + /// + /// Includes both Public and Non-Public properties + /// + /// + public static PropertyInfo[] GetAllProperties(this Type type) + { + if (type.IsInterface) { - if (type == null) - { - throw new ArgumentNullException("type"); - } - if (genericType == null) - { - throw new ArgumentNullException("genericType"); - } - if (genericType.IsGenericType == false) - { - throw new ArgumentException("genericType must be a generic type"); - } + var propertyInfos = new List(); - Func checkGenericType = (@int, t) => + var considered = new List(); + var queue = new Queue(); + considered.Add(type); + queue.Enqueue(type); + while (queue.Count > 0) { - if (@int.IsGenericType) + Type subType = queue.Dequeue(); + foreach (Type subInterface in subType.GetInterfaces()) { - var def = @int.GetGenericTypeDefinition(); - if (def == t) + if (considered.Contains(subInterface)) { - return @int.GetGenericArguments(); - } - } - return null; - }; - - //first, check if the type passed in is already the generic type - genericArgType = checkGenericType(type, genericType); - if (genericArgType != null) - return true; - - //if we're looking for interfaces, enumerate them: - if (genericType.IsInterface) - { - foreach (Type @interface in type.GetInterfaces()) - { - genericArgType = checkGenericType(@interface, genericType); - if (genericArgType != null) - return true; - } - } - else - { - //loop back into the base types as long as they are generic - while (type.BaseType != null && type.BaseType != typeof(object)) - { - genericArgType = checkGenericType(type.BaseType, genericType); - if (genericArgType != null) - return true; - type = type.BaseType; - } - - } - - return false; - - } - - /// - /// Gets all properties in a flat hierarchy - /// - /// Includes both Public and Non-Public properties - /// - /// - public static PropertyInfo[] GetAllProperties(this Type type) - { - if (type.IsInterface) - { - var propertyInfos = new List(); - - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) - { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetInterfaces()) - { - if (considered.Contains(subInterface)) continue; - - considered.Add(subInterface); - queue.Enqueue(subInterface); + continue; } - var typeProperties = subType.GetProperties( - BindingFlags.FlattenHierarchy - | BindingFlags.Public - | BindingFlags.NonPublic - | BindingFlags.Instance); - - var newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); + considered.Add(subInterface); + queue.Enqueue(subInterface); } - return propertyInfos.ToArray(); + PropertyInfo[] typeProperties = subType.GetProperties( + BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Instance); + + IEnumerable newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); } - return type.GetProperties(BindingFlags.FlattenHierarchy - | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + return propertyInfos.ToArray(); } - /// - /// Returns all public properties including inherited properties even for interfaces - /// - /// - /// - /// - /// taken from http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy - /// - public static PropertyInfo[] GetPublicProperties(this Type type) + return type.GetProperties(BindingFlags.FlattenHierarchy + | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + } + + /// + /// Returns all public properties including inherited properties even for interfaces + /// + /// + /// + /// + /// taken from + /// http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy + /// + public static PropertyInfo[] GetPublicProperties(this Type type) + { + if (type.IsInterface) { - if (type.IsInterface) + var propertyInfos = new List(); + + var considered = new List(); + var queue = new Queue(); + considered.Add(type); + queue.Enqueue(type); + while (queue.Count > 0) { - var propertyInfos = new List(); - - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) + Type subType = queue.Dequeue(); + foreach (Type subInterface in subType.GetInterfaces()) { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetInterfaces()) + if (considered.Contains(subInterface)) { - if (considered.Contains(subInterface)) continue; - - considered.Add(subInterface); - queue.Enqueue(subInterface); + continue; } - var typeProperties = subType.GetProperties( - BindingFlags.FlattenHierarchy - | BindingFlags.Public - | BindingFlags.Instance); - - var newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); + considered.Add(subInterface); + queue.Enqueue(subInterface); } - return propertyInfos.ToArray(); + PropertyInfo[] typeProperties = subType.GetProperties( + BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.Instance); + + IEnumerable newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); } - return type.GetProperties(BindingFlags.FlattenHierarchy - | BindingFlags.Public | BindingFlags.Instance); + return propertyInfos.ToArray(); } - /// - /// Determines whether the specified actual type is type. - /// - /// - /// The actual type. - /// - /// true if the specified actual type is type; otherwise, false. - /// - public static bool IsType(this Type actualType) + return type.GetProperties(BindingFlags.FlattenHierarchy + | BindingFlags.Public | BindingFlags.Instance); + } + + /// + /// Determines whether the specified actual type is type. + /// + /// + /// The actual type. + /// + /// true if the specified actual type is type; otherwise, false. + /// + public static bool IsType(this Type actualType) => TypeHelper.IsTypeAssignableFrom(actualType); + + public static bool Inherits(this Type type) => typeof(TBase).IsAssignableFrom(type); + + public static bool Inherits(this Type type, Type tbase) => tbase.IsAssignableFrom(type); + + public static bool Implements(this Type type) => typeof(TInterface).IsAssignableFrom(type); + + public static TAttribute? FirstAttribute(this Type type) => type.FirstAttribute(true); + + public static TAttribute? FirstAttribute(this Type type, bool inherit) + { + var attrs = type.GetCustomAttributes(typeof(TAttribute), inherit); + return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); + } + + public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo) => + propertyInfo.FirstAttribute(true); + + public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo, bool inherit) + { + var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); + return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); + } + + public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo) => + propertyInfo.MultipleAttribute(true); + + public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo, bool inherit) + { + var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); + return attrs.Length > 0 ? attrs.ToList().ConvertAll(input => (TAttribute)input) : null; + } + + /// + /// Returns the full type name with the assembly but without all of the assembly specific version information. + /// + /// + /// + /// + /// This method is like an 'in between' of Type.FullName and Type.AssemblyQualifiedName which returns the type and the + /// assembly separated + /// by a comma. + /// + /// + /// The output of this class would be: + /// Umbraco.Core.TypeExtensions, Umbraco.Core + /// + public static string GetFullNameWithAssembly(this Type type) + { + AssemblyName assemblyName = type.Assembly.GetName(); + + return string.Concat(type.FullName, ", ", assemblyName.FullName.StartsWith("App_Code.") ? "App_Code" : assemblyName.Name); + } + + /// + /// Determines whether an instance of a specified type can be assigned to the current type instance. + /// + /// The current type. + /// The type to compare with the current type. + /// A value indicating whether an instance of the specified type can be assigned to the current type instance. + /// + /// This extended version supports the current type being a generic type definition, and will + /// consider that eg List{int} is "assignable to" IList{}. + /// + public static bool IsAssignableFromGtd(this Type type, Type c) + { + // type *can* be a generic type definition + // c is a real type, cannot be a generic type definition + if (type.IsGenericTypeDefinition == false) { - return TypeHelper.IsTypeAssignableFrom(actualType); + return type.IsAssignableFrom(c); } - public static bool Inherits(this Type type) + if (c.IsInterface == false) { - return typeof(TBase).IsAssignableFrom(type); - } - - public static bool Inherits(this Type type, Type tbase) - { - return tbase.IsAssignableFrom(type); - } - - public static bool Implements(this Type type) - { - return typeof(TInterface).IsAssignableFrom(type); - } - - public static TAttribute? FirstAttribute(this Type type) - { - return type.FirstAttribute(true); - } - - public static TAttribute? FirstAttribute(this Type type, bool inherit) - { - var attrs = type.GetCustomAttributes(typeof(TAttribute), inherit); - return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); - } - - public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo) - { - return propertyInfo.FirstAttribute(true); - } - - public static TAttribute? FirstAttribute(this PropertyInfo propertyInfo, bool inherit) - { - var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); - return (TAttribute?)(attrs.Length > 0 ? attrs[0] : null); - } - - public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo) - { - return propertyInfo.MultipleAttribute(true); - } - - public static IEnumerable? MultipleAttribute(this PropertyInfo propertyInfo, bool inherit) - { - var attrs = propertyInfo.GetCustomAttributes(typeof(TAttribute), inherit); - return (attrs.Length > 0 ? attrs.ToList().ConvertAll(input => (TAttribute)input) : null); - } - - /// - /// Returns the full type name with the assembly but without all of the assembly specific version information. - /// - /// - /// - /// - /// This method is like an 'in between' of Type.FullName and Type.AssemblyQualifiedName which returns the type and the assembly separated - /// by a comma. - /// - /// - /// The output of this class would be: - /// - /// Umbraco.Core.TypeExtensions, Umbraco.Core - /// - public static string GetFullNameWithAssembly(this Type type) - { - var assemblyName = type.Assembly.GetName(); - - return string.Concat(type.FullName, ", ", - assemblyName.FullName.StartsWith("App_Code.") ? "App_Code" : assemblyName.Name); - } - - /// - /// Determines whether an instance of a specified type can be assigned to the current type instance. - /// - /// The current type. - /// The type to compare with the current type. - /// A value indicating whether an instance of the specified type can be assigned to the current type instance. - /// This extended version supports the current type being a generic type definition, and will - /// consider that eg List{int} is "assignable to" IList{}. - public static bool IsAssignableFromGtd(this Type type, Type c) - { - // type *can* be a generic type definition - // c is a real type, cannot be a generic type definition - - if (type.IsGenericTypeDefinition == false) - return type.IsAssignableFrom(c); - - if (c.IsInterface == false) + Type? t = c; + while (t != typeof(object)) { - var t = c; - while (t != typeof(object)) + if (t is not null && t.IsGenericType && t.GetGenericTypeDefinition() == type) { - if (t is not null && t.IsGenericType && t.GetGenericTypeDefinition() == type) return true; - t = t?.BaseType; + return true; } - } - return c.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == type); + t = t?.BaseType; + } } - /// - /// If the given is an array or some other collection - /// comprised of 0 or more instances of a "subtype", get that type - /// - /// the source type - /// - public static Type? GetEnumeratedType(this Type type) + return c.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == type); + } + + /// + /// If the given is an array or some other collection + /// comprised of 0 or more instances of a "subtype", get that type + /// + /// the source type + /// + public static Type? GetEnumeratedType(this Type type) + { + if (typeof(IEnumerable).IsAssignableFrom(type) == false) { - if (typeof(IEnumerable).IsAssignableFrom(type) == false) - return null; - - // provided by Array - var elType = type.GetElementType(); - if (null != elType) return elType; - - // otherwise provided by collection - var elTypes = type.GetGenericArguments(); - if (elTypes.Length > 0) return elTypes[0]; - - // otherwise is not an 'enumerated' type return null; } - public static T? GetCustomAttribute(this Type type, bool inherit) - where T : Attribute + // provided by Array + Type? elType = type.GetElementType(); + if (elType != null) { - return type.GetCustomAttributes(inherit).SingleOrDefault(); + return elType; } - public static IEnumerable GetCustomAttributes(this Type type, bool inherited) - where T : Attribute + // otherwise provided by collection + Type[] elTypes = type.GetGenericArguments(); + if (elTypes.Length > 0) { - if (type == null) return Enumerable.Empty(); - return type.GetCustomAttributes(typeof(T), inherited).OfType(); + return elTypes[0]; } - public static bool HasCustomAttribute(this Type type, bool inherit) - where T : Attribute + // otherwise is not an 'enumerated' type + return null; + } + + public static T? GetCustomAttribute(this Type type, bool inherit) + where T : Attribute => + type.GetCustomAttributes(inherit).SingleOrDefault(); + + public static IEnumerable GetCustomAttributes(this Type? type, bool inherited) + where T : Attribute + { + if (type == null) { - return type.GetCustomAttribute(inherit) != null; + return Enumerable.Empty(); } - /// - /// Tries to return a value based on a property name for an object but ignores case sensitivity - /// - /// - /// - /// - /// - /// - /// - /// Currently this will only work for ProperCase and camelCase properties, see the TODO below to enable complete case insensitivity - /// - internal static Attempt GetMemberIgnoreCase(this Type type, IShortStringHelper shortStringHelper, object target, string memberName) - { - Func> getMember = - memberAlias => - { - try - { - return Attempt.Succeed( - type.InvokeMember(memberAlias, - System.Reflection.BindingFlags.GetProperty | - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public, - null, - target, - null)); - } - catch (MissingMethodException ex) - { - return Attempt.Fail(ex); - } - }; + return type.GetCustomAttributes(typeof(T), inherited).OfType(); + } - //try with the current casing - var attempt = getMember(memberName); - if (attempt.Success == false) + public static bool HasCustomAttribute(this Type type, bool inherit) + where T : Attribute => + type.GetCustomAttribute(inherit) != null; + + /// + /// Tries to return a value based on a property name for an object but ignores case sensitivity + /// + /// + /// + /// + /// + /// + /// + /// Currently this will only work for ProperCase and camelCase properties, see the TODO below to enable complete case + /// insensitivity + /// + internal static Attempt GetMemberIgnoreCase(this Type type, IShortStringHelper shortStringHelper, object target, string memberName) + { + Func> getMember = + memberAlias => { - //if we cannot get with the current alias, try changing it's case - attempt = memberName[0].IsUpperCase() - ? getMember(memberName.ToCleanString(shortStringHelper, CleanStringType.Ascii | CleanStringType.ConvertCase | CleanStringType.CamelCase)) - : getMember(memberName.ToCleanString(shortStringHelper, CleanStringType.Ascii | CleanStringType.ConvertCase | CleanStringType.PascalCase)); + try + { + return Attempt.Succeed( + type.InvokeMember( + memberAlias, + BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public, + null, + target, + null)); + } + catch (MissingMethodException ex) + { + return Attempt.Fail(ex); + } + }; - // TODO: If this still fails then we should get a list of properties from the object and then compare - doing the above without listing - // all properties will surely be faster than using reflection to get ALL properties first and then query against them. - } + // try with the current casing + Attempt attempt = getMember(memberName); + if (attempt.Success == false) + { + // if we cannot get with the current alias, try changing it's case + attempt = memberName[0].IsUpperCase() + ? getMember(memberName.ToCleanString( + shortStringHelper, + CleanStringType.Ascii | CleanStringType.ConvertCase | CleanStringType.CamelCase)) + : getMember(memberName.ToCleanString( + shortStringHelper, + CleanStringType.Ascii | CleanStringType.ConvertCase | CleanStringType.PascalCase)); - return attempt; + // TODO: If this still fails then we should get a list of properties from the object and then compare - doing the above without listing + // all properties will surely be faster than using reflection to get ALL properties first and then query against them. } + return attempt; } } diff --git a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs index 8928d221c5..1ea73af009 100644 --- a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs @@ -1,33 +1,29 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TypeLoaderExtensions { - public static class TypeLoaderExtensions - { - /// - /// Gets all types implementing . - /// - public static IEnumerable GetDataEditors(this TypeLoader mgr) => mgr.GetTypes(); + /// + /// Gets all types implementing . + /// + public static IEnumerable GetDataEditors(this TypeLoader mgr) => mgr.GetTypes(); - /// - /// Gets all types implementing ICacheRefresher. - /// - public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) => mgr.GetTypes(); + /// + /// Gets all types implementing ICacheRefresher. + /// + public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) => mgr.GetTypes(); - /// - /// Gets all types implementing - /// - /// - /// - public static IEnumerable GetActions(this TypeLoader mgr) => mgr.GetTypes(); - } + /// + /// Gets all types implementing + /// + /// + /// + public static IEnumerable GetActions(this TypeLoader mgr) => mgr.GetTypes(); } diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs index 70dd11ff33..1ad94cbdc3 100644 --- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs @@ -1,325 +1,482 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods that return udis for Umbraco entities. +/// +public static class UdiGetterExtensions { /// - /// Provides extension methods that return udis for Umbraco entities. + /// Gets the entity identifier of the entity. /// - public static class UdiGetterExtensions + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this ITemplate entity) { - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this ITemplate entity) + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IContentType entity) + return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentType entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.DocumentType, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMediaType entity) + return new GuidUdi(Constants.UdiEntityType.DocumentType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMediaType entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.MediaType, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMemberType entity) + return new GuidUdi(Constants.UdiEntityType.MediaType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMemberType entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMemberGroup entity) + return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMemberGroup entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IContentTypeComposition entity) - { - if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed(); + } - string type; - if (entity is IContentType) type = Constants.UdiEntityType.DocumentType; - else if (entity is IMediaType) type = Constants.UdiEntityType.MediaType; - else if (entity is IMemberType) type = Constants.UdiEntityType.MemberType; - else throw new NotSupportedException(string.Format("Composition type {0} is not supported.", entity.GetType().FullName)); - return new GuidUdi(type, entity.Key).EnsureClosed(); + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentTypeComposition entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IDataType entity) + string type; + if (entity is IContentType) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed(); + type = Constants.UdiEntityType.DocumentType; + } + else if (entity is IMediaType) + { + type = Constants.UdiEntityType.MediaType; + } + else if (entity is IMemberType) + { + type = Constants.UdiEntityType.MemberType; + } + else + { + throw new NotSupportedException(string.Format( + "Composition type {0} is not supported.", + entity.GetType().FullName)); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this EntityContainer entity) - { - if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(type, entity.Key).EnsureClosed(); + } - string entityType; - if (entity.ContainedObjectType == Constants.ObjectTypes.DataType) - entityType = Constants.UdiEntityType.DataTypeContainer; - else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType) - entityType = Constants.UdiEntityType.DocumentTypeContainer; - else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType) - entityType = Constants.UdiEntityType.MediaTypeContainer; - else - throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType)); - return new GuidUdi(entityType, entity.Key).EnsureClosed(); + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IDataType entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMedia entity) + return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this EntityContainer entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IContent entity) + string entityType; + if (entity.ContainedObjectType == Constants.ObjectTypes.DataType) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document, entity.Key).EnsureClosed(); + entityType = Constants.UdiEntityType.DataTypeContainer; + } + else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType) + { + entityType = Constants.UdiEntityType.DocumentTypeContainer; + } + else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType) + { + entityType = Constants.UdiEntityType.MediaTypeContainer; + } + else + { + throw new NotSupportedException(string.Format( + "Contained object type {0} is not supported.", + entity.ContainedObjectType)); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMember entity) + return new GuidUdi(entityType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMedia entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this Stylesheet entity) + return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContent entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.UdiEntityType.Stylesheet, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this Script entity) + return new GuidUdi( + entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document, + entity.Key) + .EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMember entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.UdiEntityType.Script, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IDictionaryItem entity) + return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this Stylesheet entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.DictionaryItem, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IMacro entity) + return new StringUdi( + Constants.UdiEntityType.Stylesheet, + entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this Script entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Macro, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this IPartialView entity) + return new StringUdi(Constants.UdiEntityType.Script, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)) + .EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IDictionaryItem entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - - // we should throw on Unknown but for the time being, assume it means PartialView - var entityType = entity.ViewType == PartialViewType.PartialViewMacro - ? Constants.UdiEntityType.PartialViewMacro - : Constants.UdiEntityType.PartialView; - - return new StringUdi(entityType, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IContentBase entity) - { - if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.UdiEntityType.DictionaryItem, entity.Key).EnsureClosed(); + } - string type; - if (entity is IContent) type = Constants.UdiEntityType.Document; - else if (entity is IMedia) type = Constants.UdiEntityType.Media; - else if (entity is IMember) type = Constants.UdiEntityType.Member; - else throw new NotSupportedException(string.Format("ContentBase type {0} is not supported.", entity.GetType().FullName)); - return new GuidUdi(type, entity.Key).EnsureClosed(); + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMacro entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static GuidUdi GetUdi(this IRelationType entity) + return new GuidUdi(Constants.UdiEntityType.Macro, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this IPartialView entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static StringUdi GetUdi(this ILanguage entity) + // we should throw on Unknown but for the time being, assume it means PartialView + var entityType = entity.ViewType == PartialViewType.PartialViewMacro + ? Constants.UdiEntityType.PartialViewMacro + : Constants.UdiEntityType.PartialView; + + return new StringUdi(entityType, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentBase entity) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); + throw new ArgumentNullException("entity"); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// The entity identifier of the entity. - public static Udi GetUdi(this IEntity entity) + string type; + if (entity is IContent) { - if (entity == null) throw new ArgumentNullException("entity"); - - // entity could eg be anything implementing IThing - // so we have to go through casts here - - var template = entity as ITemplate; - if (template != null) return template.GetUdi(); - - var contentType = entity as IContentType; - if (contentType != null) return contentType.GetUdi(); - - var mediaType = entity as IMediaType; - if (mediaType != null) return mediaType.GetUdi(); - - var memberType = entity as IMemberType; - if (memberType != null) return memberType.GetUdi(); - - var memberGroup = entity as IMemberGroup; - if (memberGroup != null) return memberGroup.GetUdi(); - - var contentTypeComposition = entity as IContentTypeComposition; - if (contentTypeComposition != null) return contentTypeComposition.GetUdi(); - - var dataTypeComposition = entity as IDataType; - if (dataTypeComposition != null) return dataTypeComposition.GetUdi(); - - var container = entity as EntityContainer; - if (container != null) return container.GetUdi(); - - var media = entity as IMedia; - if (media != null) return media.GetUdi(); - - var content = entity as IContent; - if (content != null) return content.GetUdi(); - - var member = entity as IMember; - if (member != null) return member.GetUdi(); - - var stylesheet = entity as Stylesheet; - if (stylesheet != null) return stylesheet.GetUdi(); - - var script = entity as Script; - if (script != null) return script.GetUdi(); - - var dictionaryItem = entity as IDictionaryItem; - if (dictionaryItem != null) return dictionaryItem.GetUdi(); - - var macro = entity as IMacro; - if (macro != null) return macro.GetUdi(); - - var partialView = entity as IPartialView; - if (partialView != null) return partialView.GetUdi(); - - var contentBase = entity as IContentBase; - if (contentBase != null) return contentBase.GetUdi(); - - var relationType = entity as IRelationType; - if (relationType != null) return relationType.GetUdi(); - - var language = entity as ILanguage; - if (language != null) return language.GetUdi(); - - throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); + type = Constants.UdiEntityType.Document; } + else if (entity is IMedia) + { + type = Constants.UdiEntityType.Media; + } + else if (entity is IMember) + { + type = Constants.UdiEntityType.Member; + } + else + { + throw new NotSupportedException(string.Format( + "ContentBase type {0} is not supported.", + entity.GetType().FullName)); + } + + return new GuidUdi(type, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IRelationType entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this ILanguage entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static Udi GetUdi(this IEntity entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + // entity could eg be anything implementing IThing + // so we have to go through casts here + if (entity is ITemplate template) + { + return template.GetUdi(); + } + + if (entity is IContentType contentType) + { + return contentType.GetUdi(); + } + + if (entity is IMediaType mediaType) + { + return mediaType.GetUdi(); + } + + if (entity is IMemberType memberType) + { + return memberType.GetUdi(); + } + + if (entity is IMemberGroup memberGroup) + { + return memberGroup.GetUdi(); + } + + if (entity is IContentTypeComposition contentTypeComposition) + { + return contentTypeComposition.GetUdi(); + } + + if (entity is IDataType dataTypeComposition) + { + return dataTypeComposition.GetUdi(); + } + + if (entity is EntityContainer container) + { + return container.GetUdi(); + } + + if (entity is IMedia media) + { + return media.GetUdi(); + } + + if (entity is IContent content) + { + return content.GetUdi(); + } + + if (entity is IMember member) + { + return member.GetUdi(); + } + + if (entity is Stylesheet stylesheet) + { + return stylesheet.GetUdi(); + } + + if (entity is Script script) + { + return script.GetUdi(); + } + + if (entity is IDictionaryItem dictionaryItem) + { + return dictionaryItem.GetUdi(); + } + + if (entity is IMacro macro) + { + return macro.GetUdi(); + } + + if (entity is IPartialView partialView) + { + return partialView.GetUdi(); + } + + if (entity is IContentBase contentBase) + { + return contentBase.GetUdi(); + } + + if (entity is IRelationType relationType) + { + return relationType.GetUdi(); + } + + if (entity is ILanguage language) + { + return language.GetUdi(); + } + + throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); } } diff --git a/src/Umbraco.Core/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Core/Extensions/UmbracoBuilderExtensions.cs index 5b4e3a92d9..53e86109c3 100644 --- a/src/Umbraco.Core/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Core/Extensions/UmbracoBuilderExtensions.cs @@ -1,104 +1,103 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Extensions +namespace Umbraco.Cms.Core.Extensions; + +public static class UmbracoBuilderExtensions { - public static class UmbracoBuilderExtensions + /// + /// Registers all within an assembly + /// + /// + /// + /// + /// Type contained within the targeted assembly + /// + public static IUmbracoBuilder AddNotificationsFromAssembly(this IUmbracoBuilder self) { - /// - /// Registers all within an assembly - /// - /// - /// Type contained within the targeted assembly - /// - public static IUmbracoBuilder AddNotificationsFromAssembly(this IUmbracoBuilder self) - { - AddNotificationHandlers(self); - AddAsyncNotificationHandlers(self); + AddNotificationHandlers(self); + AddAsyncNotificationHandlers(self); - return self; - } + return self; + } - private static void AddNotificationHandlers(IUmbracoBuilder self) + private static void AddNotificationHandlers(IUmbracoBuilder self) + { + List notificationHandlers = GetNotificationHandlers(); + foreach (Type notificationHandler in notificationHandlers) { - var notificationHandlers = GetNotificationHandlers(); - foreach (var notificationHandler in notificationHandlers) + List handlerImplementations = GetNotificationHandlerImplementations(notificationHandler); + foreach (Type implementation in handlerImplementations) { - var handlerImplementations = GetNotificationHandlerImplementations(notificationHandler); - foreach (var implementation in handlerImplementations) - { - RegisterNotificationHandler(self, implementation, notificationHandler); - } + RegisterNotificationHandler(self, implementation, notificationHandler); } } + } - private static List GetNotificationHandlers() => - typeof(T).Assembly.GetTypes() - .Where(x => x.IsAssignableToGenericType(typeof(INotificationHandler<>))) - .ToList(); + private static List GetNotificationHandlers() => + typeof(T).Assembly.GetTypes() + .Where(x => x.IsAssignableToGenericType(typeof(INotificationHandler<>))) + .ToList(); - private static List GetNotificationHandlerImplementations(Type handlerType) => - handlerType - .GetInterfaces() - .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(INotificationHandler<>)) - .ToList(); + private static List GetNotificationHandlerImplementations(Type handlerType) => + handlerType + .GetInterfaces() + .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(INotificationHandler<>)) + .ToList(); - private static void AddAsyncNotificationHandlers(IUmbracoBuilder self) + private static void AddAsyncNotificationHandlers(IUmbracoBuilder self) + { + List notificationHandlers = GetAsyncNotificationHandlers(); + foreach (Type notificationHandler in notificationHandlers) { - var notificationHandlers = GetAsyncNotificationHandlers(); - foreach (var notificationHandler in notificationHandlers) + List handlerImplementations = GetAsyncNotificationHandlerImplementations(notificationHandler); + foreach (Type handler in handlerImplementations) { - var handlerImplementations = GetAsyncNotificationHandlerImplementations(notificationHandler); - foreach (var handler in handlerImplementations) - { - RegisterNotificationHandler(self, handler, notificationHandler); - } + RegisterNotificationHandler(self, handler, notificationHandler); } } + } - private static List GetAsyncNotificationHandlers() => - typeof(T).Assembly.GetTypes() - .Where(x => x.IsAssignableToGenericType(typeof(INotificationAsyncHandler<>))) - .ToList(); + private static List GetAsyncNotificationHandlers() => + typeof(T).Assembly.GetTypes() + .Where(x => x.IsAssignableToGenericType(typeof(INotificationAsyncHandler<>))) + .ToList(); - private static List GetAsyncNotificationHandlerImplementations(Type handlerType) => - handlerType - .GetInterfaces() - .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(INotificationAsyncHandler<>)) - .ToList(); + private static List GetAsyncNotificationHandlerImplementations(Type handlerType) => + handlerType + .GetInterfaces() + .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(INotificationAsyncHandler<>)) + .ToList(); - private static void RegisterNotificationHandler(IUmbracoBuilder self, Type notificationHandlerType, Type implementingHandlerType) + private static void RegisterNotificationHandler(IUmbracoBuilder self, Type notificationHandlerType, Type implementingHandlerType) + { + var descriptor = + new UniqueServiceDescriptor(notificationHandlerType, implementingHandlerType, ServiceLifetime.Transient); + if (!self.Services.Contains(descriptor)) { - var descriptor = new UniqueServiceDescriptor(notificationHandlerType, implementingHandlerType, ServiceLifetime.Transient); - if (!self.Services.Contains(descriptor)) - { - self.Services.Add(descriptor); - } + self.Services.Add(descriptor); } + } - private static bool IsAssignableToGenericType(this Type givenType, Type genericType) + private static bool IsAssignableToGenericType(this Type givenType, Type genericType) + { + Type[] interfaceTypes = givenType.GetInterfaces(); + + foreach (Type it in interfaceTypes) { - var interfaceTypes = givenType.GetInterfaces(); - - foreach (var it in interfaceTypes) - { - if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) - { - return true; - } - } - - if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) { return true; } - - var baseType = givenType.BaseType; - return baseType != null && IsAssignableToGenericType(baseType, genericType); } + + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + { + return true; + } + + Type? baseType = givenType.BaseType; + return baseType != null && IsAssignableToGenericType(baseType, genericType); } } diff --git a/src/Umbraco.Core/Extensions/UmbracoContextAccessorExtensions.cs b/src/Umbraco.Core/Extensions/UmbracoContextAccessorExtensions.cs index 794c206db8..b0256ad9e6 100644 --- a/src/Umbraco.Core/Extensions/UmbracoContextAccessorExtensions.cs +++ b/src/Umbraco.Core/Extensions/UmbracoContextAccessorExtensions.cs @@ -1,21 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoContextAccessorExtensions { - public static class UmbracoContextAccessorExtensions + public static IUmbracoContext GetRequiredUmbracoContext(this IUmbracoContextAccessor umbracoContextAccessor) { - public static IUmbracoContext GetRequiredUmbracoContext(this IUmbracoContextAccessor umbracoContextAccessor) + if (umbracoContextAccessor == null) { - if (umbracoContextAccessor == null) throw new ArgumentNullException(nameof(umbracoContextAccessor)); - if(!umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - throw new InvalidOperationException("Wasn't able to get an UmbracoContext"); - } - return umbracoContext!; + throw new ArgumentNullException(nameof(umbracoContextAccessor)); } + + if (!umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + throw new InvalidOperationException("Wasn't able to get an UmbracoContext"); + } + + return umbracoContext; } } diff --git a/src/Umbraco.Core/Extensions/UmbracoContextExtensions.cs b/src/Umbraco.Core/Extensions/UmbracoContextExtensions.cs index 7d0e31f285..e5ec62530d 100644 --- a/src/Umbraco.Core/Extensions/UmbracoContextExtensions.cs +++ b/src/Umbraco.Core/Extensions/UmbracoContextExtensions.cs @@ -3,13 +3,13 @@ using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoContextExtensions { - public static class UmbracoContextExtensions - { - /// - /// Boolean value indicating whether the current request is a front-end umbraco request - /// - public static bool IsFrontEndUmbracoRequest(this IUmbracoContext umbracoContext) => umbracoContext.PublishedRequest != null; - } + /// + /// Boolean value indicating whether the current request is a front-end umbraco request + /// + public static bool IsFrontEndUmbracoRequest(this IUmbracoContext umbracoContext) => + umbracoContext.PublishedRequest != null; } diff --git a/src/Umbraco.Core/Extensions/UriExtensions.cs b/src/Umbraco.Core/Extensions/UriExtensions.cs index 52adbc6b67..60ef7b6a7e 100644 --- a/src/Umbraco.Core/Extensions/UriExtensions.cs +++ b/src/Umbraco.Core/Extensions/UriExtensions.cs @@ -1,192 +1,206 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +using System.Net; +using System.Web; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to . +/// +public static class UriExtensions { /// - /// Provides extension methods to . + /// Rewrites the path of uri. /// - public static class UriExtensions + /// The uri. + /// The new path, which must begin with a slash. + /// The rewritten uri. + /// Everything else remains unchanged, except for the fragment which is removed. + public static Uri Rewrite(this Uri uri, string path) { - /// - /// Rewrites the path of uri. - /// - /// The uri. - /// The new path, which must begin with a slash. - /// The rewritten uri. - /// Everything else remains unchanged, except for the fragment which is removed. - public static Uri Rewrite(this Uri uri, string path) + if (path.StartsWith("/") == false) { - if (path.StartsWith("/") == false) - throw new ArgumentException("Path must start with a slash.", "path"); - - return uri.IsAbsoluteUri - ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + uri.Query) - : new Uri(path + uri.GetSafeQuery(), UriKind.Relative); + throw new ArgumentException("Path must start with a slash.", "path"); } - /// - /// Rewrites the path and query of a uri. - /// - /// The uri. - /// The new path, which must begin with a slash. - /// The new query, which must be empty or begin with a question mark. - /// The rewritten uri. - /// Everything else remains unchanged, except for the fragment which is removed. - public static Uri Rewrite(this Uri uri, string path, string query) - { - if (path.StartsWith("/") == false) - throw new ArgumentException("Path must start with a slash.", "path"); - if (query.Length > 0 && query.StartsWith("?") == false) - throw new ArgumentException("Query must start with a question mark.", "query"); - if (query == "?") - query = ""; - - return uri.IsAbsoluteUri - ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + query) - : new Uri(path + query, UriKind.Relative); - } - - /// - /// Gets the absolute path of the uri, even if the uri is relative. - /// - /// The uri. - /// The absolute path of the uri. - /// Default uri.AbsolutePath does not support relative uris. - public static string GetSafeAbsolutePath(this Uri uri) - { - if (uri.IsAbsoluteUri) - { - return uri.AbsolutePath; - } - - // cannot get .AbsolutePath on relative uri (InvalidOperation) - var s = uri.OriginalString; - - // TODO: Shouldn't this just use Uri.GetLeftPart? - var posq = s.IndexOf("?", StringComparison.Ordinal); - var posf = s.IndexOf("#", StringComparison.Ordinal); - var pos = posq > 0 ? posq : (posf > 0 ? posf : 0); - var path = pos > 0 ? s.Substring(0, pos) : s; - return path; - } - - /// - /// Gets the decoded, absolute path of the uri. - /// - /// The uri. - /// The absolute path of the uri. - /// Only for absolute uris. - public static string GetAbsolutePathDecoded(this Uri uri) - { - return System.Web.HttpUtility.UrlDecode(uri.AbsolutePath); - } - - /// - /// Gets the decoded, absolute path of the uri, even if the uri is relative. - /// - /// The uri. - /// The absolute path of the uri. - /// Default uri.AbsolutePath does not support relative uris. - public static string GetSafeAbsolutePathDecoded(this Uri uri) - { - return System.Net.WebUtility.UrlDecode(uri.GetSafeAbsolutePath()); - } - - /// - /// Rewrites the path of the uri so it ends with a slash. - /// - /// The uri. - /// The rewritten uri. - /// Everything else remains unchanged. - public static Uri EndPathWithSlash(this Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) - { - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); - return uri; - } - - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(path + "/" + uri.Query, UriKind.Relative); - - return uri; - } - - /// - /// Rewrites the path of the uri so it does not end with a slash. - /// - /// The uri. - /// The rewritten uri. - /// Everything else remains unchanged. - public static Uri TrimPathEndSlash(this Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) - { - if (path != "/") - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query); - } - else - { - if (path != "/") - uri = new Uri(path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query, UriKind.Relative); - } - return uri; - } - - /// - /// Transforms a relative uri into an absolute uri. - /// - /// The relative uri. - /// The base absolute uri. - /// The absolute uri. - public static Uri MakeAbsolute(this Uri uri, Uri baseUri) - { - if (uri.IsAbsoluteUri) - throw new ArgumentException("Uri is already absolute.", "uri"); - - return new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); - } - - static string? GetSafeQuery(this Uri uri) - { - if (uri.IsAbsoluteUri) - return uri.Query; - - // cannot get .Query on relative uri (InvalidOperation) - var s = uri.OriginalString; - var posq = s.IndexOf("?", StringComparison.Ordinal); - var posf = s.IndexOf("#", StringComparison.Ordinal); - var query = posq < 0 ? null : (posf < 0 ? s.Substring(posq) : s.Substring(posq, posf - posq)); - - return query; - } - - /// - /// Removes the port from the uri. - /// - /// The uri. - /// The same uri, without its port. - public static Uri WithoutPort(this Uri uri) - { - return new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); - } - - /// - /// Replaces the host of a uri. - /// - /// The uri. - /// A replacement host. - /// The same uri, with its host replaced. - public static Uri ReplaceHost(this Uri uri, string host) - { - return new UriBuilder(uri) { Host = host }.Uri; - } + return uri.IsAbsoluteUri + ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + uri.Query) + : new Uri(path + uri.GetSafeQuery(), UriKind.Relative); } + + /// + /// Rewrites the path and query of a uri. + /// + /// The uri. + /// The new path, which must begin with a slash. + /// The new query, which must be empty or begin with a question mark. + /// The rewritten uri. + /// Everything else remains unchanged, except for the fragment which is removed. + public static Uri Rewrite(this Uri uri, string path, string query) + { + if (path.StartsWith("/") == false) + { + throw new ArgumentException("Path must start with a slash.", "path"); + } + + if (query.Length > 0 && query.StartsWith("?") == false) + { + throw new ArgumentException("Query must start with a question mark.", "query"); + } + + if (query == "?") + { + query = string.Empty; + } + + return uri.IsAbsoluteUri + ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + query) + : new Uri(path + query, UriKind.Relative); + } + + /// + /// Gets the absolute path of the uri, even if the uri is relative. + /// + /// The uri. + /// The absolute path of the uri. + /// Default uri.AbsolutePath does not support relative uris. + public static string GetSafeAbsolutePath(this Uri uri) + { + if (uri.IsAbsoluteUri) + { + return uri.AbsolutePath; + } + + // cannot get .AbsolutePath on relative uri (InvalidOperation) + var s = uri.OriginalString; + + // TODO: Shouldn't this just use Uri.GetLeftPart? + var posq = s.IndexOf("?", StringComparison.Ordinal); + var posf = s.IndexOf("#", StringComparison.Ordinal); + var pos = posq > 0 ? posq : posf > 0 ? posf : 0; + var path = pos > 0 ? s.Substring(0, pos) : s; + return path; + } + + /// + /// Gets the decoded, absolute path of the uri. + /// + /// The uri. + /// The absolute path of the uri. + /// Only for absolute uris. + public static string GetAbsolutePathDecoded(this Uri uri) => HttpUtility.UrlDecode(uri.AbsolutePath); + + /// + /// Gets the decoded, absolute path of the uri, even if the uri is relative. + /// + /// The uri. + /// The absolute path of the uri. + /// Default uri.AbsolutePath does not support relative uris. + public static string GetSafeAbsolutePathDecoded(this Uri uri) => WebUtility.UrlDecode(uri.GetSafeAbsolutePath()); + + /// + /// Rewrites the path of the uri so it ends with a slash. + /// + /// The uri. + /// The rewritten uri. + /// Everything else remains unchanged. + public static Uri EndPathWithSlash(this Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (uri.IsAbsoluteUri) + { + if (path != "/" && path.EndsWith("/") == false) + { + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); + } + + return uri; + } + + if (path != "/" && path.EndsWith("/") == false) + { + uri = new Uri(path + "/" + uri.Query, UriKind.Relative); + } + + return uri; + } + + /// + /// Rewrites the path of the uri so it does not end with a slash. + /// + /// The uri. + /// The rewritten uri. + /// Everything else remains unchanged. + public static Uri TrimPathEndSlash(this Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (uri.IsAbsoluteUri) + { + if (path != "/") + { + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd(Constants.CharArrays.ForwardSlash) + + uri.Query); + } + } + else + { + if (path != "/") + { + uri = new Uri(path.TrimEnd(Constants.CharArrays.ForwardSlash) + uri.Query, UriKind.Relative); + } + } + + return uri; + } + + /// + /// Transforms a relative uri into an absolute uri. + /// + /// The relative uri. + /// The base absolute uri. + /// The absolute uri. + public static Uri MakeAbsolute(this Uri uri, Uri baseUri) + { + if (uri.IsAbsoluteUri) + { + throw new ArgumentException("Uri is already absolute.", "uri"); + } + + return new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); + } + + /// + /// Removes the port from the uri. + /// + /// The uri. + /// The same uri, without its port. + public static Uri WithoutPort(this Uri uri) => + new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); + + private static string? GetSafeQuery(this Uri uri) + { + if (uri.IsAbsoluteUri) + { + return uri.Query; + } + + // cannot get .Query on relative uri (InvalidOperation) + var s = uri.OriginalString; + var posq = s.IndexOf("?", StringComparison.Ordinal); + var posf = s.IndexOf("#", StringComparison.Ordinal); + var query = posq < 0 ? null : (posf < 0 ? s.Substring(posq) : s.Substring(posq, posf - posq)); + + return query; + } + + /// + /// Replaces the host of a uri. + /// + /// The uri. + /// A replacement host. + /// The same uri, with its host replaced. + public static Uri ReplaceHost(this Uri uri, string host) => new UriBuilder(uri) { Host = host }.Uri; } diff --git a/src/Umbraco.Core/Extensions/VersionExtensions.cs b/src/Umbraco.Core/Extensions/VersionExtensions.cs index 24326ef327..4e9309da45 100644 --- a/src/Umbraco.Core/Extensions/VersionExtensions.cs +++ b/src/Umbraco.Core/Extensions/VersionExtensions.cs @@ -1,87 +1,91 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class VersionExtensions { - public static class VersionExtensions + public static Version GetVersion(this SemVersion semVersion, int maxParts = 4) { - public static Version GetVersion(this SemVersion semVersion, int maxParts = 4) + int.TryParse(semVersion.Build, NumberStyles.Integer, CultureInfo.InvariantCulture, out int build); + + if (maxParts >= 4) { - int build = 0; - int.TryParse(semVersion.Build, NumberStyles.Integer, CultureInfo.InvariantCulture, out build); - - if (maxParts >= 4) - { - return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch, build); - } - if (maxParts == 3) - { - return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch); - } - - return new Version(semVersion.Major, semVersion.Minor); + return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch, build); } - public static Version SubtractRevision(this Version version) + if (maxParts == 3) { - var parts = new List(new[] {version.Major, version.Minor, version.Build, version.Revision}); + return new Version(semVersion.Major, semVersion.Minor, semVersion.Patch); + } - //remove all prefixed zero parts - while (parts[0] <= 0) + return new Version(semVersion.Major, semVersion.Minor); + } + + public static Version SubtractRevision(this Version version) + { + var parts = new List(new[] { version.Major, version.Minor, version.Build, version.Revision }); + + // remove all prefixed zero parts + while (parts[0] <= 0) + { + parts.RemoveAt(0); + if (parts.Count == 0) { - parts.RemoveAt(0); - if (parts.Count == 0) break; + break; } + } - for (int index = 0; index < parts.Count; index++) + for (var index = 0; index < parts.Count; index++) + { + var part = parts[index]; + if (part <= 0) { - var part = parts[index]; - if (part <= 0) - { - parts.RemoveAt(index); - index++; - } - else - { - //break when there isn't a zero part - break; - } + parts.RemoveAt(index); + index++; } - - if (parts.Count == 0) throw new InvalidOperationException("Cannot subtract a revision from a zero version"); - - var lastNonZero = parts.FindLastIndex(i => i > 0); - - //subtract 1 from the last non-zero - parts[lastNonZero] = parts[lastNonZero] - 1; - - //the last non zero is actually the revision so we can just return - if (lastNonZero == (parts.Count -1)) + else { - return FromList(parts); + // break when there isn't a zero part + break; } + } - //the last non zero isn't the revision so the remaining zero's need to be replaced with int.max - for (var i = lastNonZero + 1; i < parts.Count; i++) - { - parts[i] = int.MaxValue; - } + if (parts.Count == 0) + { + throw new InvalidOperationException("Cannot subtract a revision from a zero version"); + } + var lastNonZero = parts.FindLastIndex(i => i > 0); + + // subtract 1 from the last non-zero + parts[lastNonZero] = parts[lastNonZero] - 1; + + // the last non zero is actually the revision so we can just return + if (lastNonZero == parts.Count - 1) + { return FromList(parts); } - private static Version FromList(IList parts) + // the last non zero isn't the revision so the remaining zero's need to be replaced with int.max + for (var i = lastNonZero + 1; i < parts.Count; i++) { - while (parts.Count < 4) - { - parts.Insert(0, 0); - } - return new Version(parts[0], parts[1], parts[2], parts[3]); + parts[i] = int.MaxValue; } + + return FromList(parts); + } + + private static Version FromList(IList parts) + { + while (parts.Count < 4) + { + parts.Insert(0, 0); + } + + return new Version(parts[0], parts[1], parts[2], parts[3]); } } diff --git a/src/Umbraco.Core/Extensions/WaitHandleExtensions.cs b/src/Umbraco.Core/Extensions/WaitHandleExtensions.cs index 5cb7639497..b0058dd798 100644 --- a/src/Umbraco.Core/Extensions/WaitHandleExtensions.cs +++ b/src/Umbraco.Core/Extensions/WaitHandleExtensions.cs @@ -1,48 +1,43 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +public static class WaitHandleExtensions { - public static class WaitHandleExtensions + // http://stackoverflow.com/questions/25382583/waiting-on-a-named-semaphore-with-waitone100-vs-waitone0-task-delay100 + // http://blog.nerdbank.net/2011/07/c-await-for-waithandle.html + // F# has a AwaitWaitHandle method that accepts a time out... and seems pretty complex... + // version below should be OK + public static Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout = Timeout.Infinite) { - // http://stackoverflow.com/questions/25382583/waiting-on-a-named-semaphore-with-waitone100-vs-waitone0-task-delay100 - // http://blog.nerdbank.net/2011/07/c-await-for-waithandle.html - // F# has a AwaitWaitHandle method that accepts a time out... and seems pretty complex... - // version below should be OK - - public static Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout = Timeout.Infinite) + var tcs = new TaskCompletionSource(); + var callbackHandleInitLock = new object(); + lock (callbackHandleInitLock) { - var tcs = new TaskCompletionSource(); - var callbackHandleInitLock = new object(); - lock (callbackHandleInitLock) - { - RegisteredWaitHandle? callbackHandle = null; - // ReSharper disable once RedundantAssignment - callbackHandle = ThreadPool.RegisterWaitForSingleObject( - handle, - (state, timedOut) => + RegisteredWaitHandle? callbackHandle = null; + + // ReSharper disable once RedundantAssignment + callbackHandle = ThreadPool.RegisterWaitForSingleObject( + handle, + (state, timedOut) => + { + // TODO: We aren't checking if this is timed out + tcs.SetResult(null); + + // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. + lock (callbackHandleInitLock) { - //TODO: We aren't checking if this is timed out - - tcs.SetResult(null); - - // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. - lock (callbackHandleInitLock) - { - // ReSharper disable once PossibleNullReferenceException - // ReSharper disable once AccessToModifiedClosure - callbackHandle?.Unregister(null); - } - }, - /*state:*/ null, - /*millisecondsTimeOutInterval:*/ millisecondsTimeout, - /*executeOnlyOnce:*/ true); - } - - return tcs.Task; + // ReSharper disable once PossibleNullReferenceException + // ReSharper disable once AccessToModifiedClosure + callbackHandle?.Unregister(null); + } + }, + /*state:*/ null, + /*millisecondsTimeOutInterval:*/ millisecondsTimeout, + /*executeOnlyOnce:*/ true); } + + return tcs.Task; } } diff --git a/src/Umbraco.Core/Extensions/XmlExtensions.cs b/src/Umbraco.Core/Extensions/XmlExtensions.cs index 141f4a0c19..34e2b7b2aa 100644 --- a/src/Umbraco.Core/Extensions/XmlExtensions.cs +++ b/src/Umbraco.Core/Extensions/XmlExtensions.cs @@ -1,9 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Xml; using System.Xml.Linq; @@ -11,337 +8,402 @@ using System.Xml.XPath; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for xml objects +/// +public static class XmlExtensions { + public static bool HasAttribute(this XmlAttributeCollection attributes, string attributeName) => + attributes.Cast().Any(x => x.Name == attributeName); + /// - /// Extension methods for xml objects + /// Selects a list of XmlNode matching an XPath expression. /// - public static class XmlExtensions + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The list of XmlNode matching the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNodeList? SelectNodes(this XmlNode source, string expression, IEnumerable? variables) { - public static bool HasAttribute(this XmlAttributeCollection attributes, string attributeName) + XPathVariable[]? av = variables?.ToArray(); + return SelectNodes(source, expression, av); + } + + /// + /// Selects a list of XmlNode matching an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The list of XmlNode matching the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNodeList? SelectNodes(this XmlNode source, XPathExpression expression, IEnumerable? variables) + { + XPathVariable[]? av = variables?.ToArray(); + return SelectNodes(source, expression, av); + } + + /// + /// Selects a list of XmlNode matching an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The list of XmlNode matching the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNodeList? SelectNodes(this XmlNode source, string? expression, params XPathVariable[]? variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) { - return attributes.Cast().Any(x => x.Name == attributeName); + return source.SelectNodes(expression ?? string.Empty); } - /// - /// Selects a list of XmlNode matching an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The list of XmlNode matching the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNodeList? SelectNodes(this XmlNode source, string expression, IEnumerable? variables) + XPathNodeIterator? iterator = source.CreateNavigator()?.Select(expression ?? string.Empty, variables); + return XmlNodeListFactory.CreateNodeList(iterator); + } + + /// + /// Selects a list of XmlNode matching an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The list of XmlNode matching the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNodeList SelectNodes(this XmlNode source, XPathExpression expression, params XPathVariable[]? variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) { - var av = variables == null ? null : variables.ToArray(); - return SelectNodes(source, expression, av); + return source.SelectNodes(expression); } - /// - /// Selects a list of XmlNode matching an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The list of XmlNode matching the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNodeList? SelectNodes(this XmlNode source, XPathExpression expression, IEnumerable? variables) + XPathNodeIterator? iterator = source.CreateNavigator()?.Select(expression, variables); + return XmlNodeListFactory.CreateNodeList(iterator); + } + + /// + /// Selects the first XmlNode that matches an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The first XmlNode that matches the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNode? SelectSingleNode(this XmlNode source, string expression, IEnumerable? variables) + { + XPathVariable[]? av = variables?.ToArray(); + return SelectSingleNode(source, expression, av); + } + + /// + /// Selects the first XmlNode that matches an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The first XmlNode that matches the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNode? SelectSingleNode(this XmlNode source, XPathExpression expression, IEnumerable? variables) + { + XPathVariable[]? av = variables?.ToArray(); + return SelectSingleNode(source, expression, av); + } + + /// + /// Selects the first XmlNode that matches an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The first XmlNode that matches the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNode? SelectSingleNode(this XmlNode source, string expression, params XPathVariable[]? variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) { - var av = variables == null ? null : variables.ToArray(); - return SelectNodes(source, expression, av); + return source.SelectSingleNode(expression); } - /// - /// Selects a list of XmlNode matching an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The list of XmlNode matching the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNodeList? SelectNodes(this XmlNode source, string? expression, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectNodes(expression ?? ""); + return SelectNodes(source, expression, variables)?.Cast().FirstOrDefault(); + } - var iterator = source.CreateNavigator()?.Select(expression ?? "", variables); - return XmlNodeListFactory.CreateNodeList(iterator); + /// + /// Selects the first XmlNode that matches an XPath expression. + /// + /// A source XmlNode. + /// An XPath expression. + /// A set of XPathVariables. + /// The first XmlNode that matches the XPath expression. + /// + /// + /// If + /// + /// is null, or is empty, or contains only one single + /// value which itself is null, then variables are ignored. + /// + /// The XPath expression should reference variables as $var. + /// + public static XmlNode? SelectSingleNode(this XmlNode source, XPathExpression expression, params XPathVariable[]? variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) + { + return source.SelectSingleNode(expression); } - /// - /// Selects a list of XmlNode matching an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The list of XmlNode matching the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNodeList SelectNodes(this XmlNode source, XPathExpression expression, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectNodes(expression); + return SelectNodes(source, expression, variables).Cast().FirstOrDefault(); + } - var iterator = source.CreateNavigator()?.Select(expression, variables); - return XmlNodeListFactory.CreateNodeList(iterator); + /// + /// Converts from an XDocument to an XmlDocument + /// + /// + /// + public static XmlDocument ToXmlDocument(this XDocument xDocument) + { + var xmlDocument = new XmlDocument(); + using (XmlReader xmlReader = xDocument.CreateReader()) + { + xmlDocument.Load(xmlReader); } - /// - /// Selects the first XmlNode that matches an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The first XmlNode that matches the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNode? SelectSingleNode(this XmlNode source, string expression, IEnumerable? variables) + return xmlDocument; + } + + /// + /// Converts from an XmlDocument to an XDocument + /// + /// + /// + public static XDocument ToXDocument(this XmlDocument xmlDocument) + { + using (var nodeReader = new XmlNodeReader(xmlDocument)) { - var av = variables == null ? null : variables.ToArray(); - return SelectSingleNode(source, expression, av); - } - - /// - /// Selects the first XmlNode that matches an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The first XmlNode that matches the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNode? SelectSingleNode(this XmlNode source, XPathExpression expression, IEnumerable? variables) - { - var av = variables == null ? null : variables.ToArray(); - return SelectSingleNode(source, expression, av); - } - - /// - /// Selects the first XmlNode that matches an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The first XmlNode that matches the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNode? SelectSingleNode(this XmlNode source, string expression, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectSingleNode(expression); - - return SelectNodes(source, expression, variables)?.Cast().FirstOrDefault(); - } - - /// - /// Selects the first XmlNode that matches an XPath expression. - /// - /// A source XmlNode. - /// An XPath expression. - /// A set of XPathVariables. - /// The first XmlNode that matches the XPath expression. - /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. - /// The XPath expression should reference variables as $var. - /// - public static XmlNode? SelectSingleNode(this XmlNode source, XPathExpression expression, params XPathVariable[]? variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return source.SelectSingleNode(expression); - - return SelectNodes(source, expression, variables).Cast().FirstOrDefault(); - } - - /// - /// Converts from an XDocument to an XmlDocument - /// - /// - /// - public static XmlDocument ToXmlDocument(this XDocument xDocument) - { - var xmlDocument = new XmlDocument(); - using (var xmlReader = xDocument.CreateReader()) - { - xmlDocument.Load(xmlReader); - } - return xmlDocument; - } - - /// - /// Converts from an XmlDocument to an XDocument - /// - /// - /// - public static XDocument ToXDocument(this XmlDocument xmlDocument) - { - using (var nodeReader = new XmlNodeReader(xmlDocument)) - { - nodeReader.MoveToContent(); - return XDocument.Load(nodeReader); - } - } - - ///// - ///// Converts from an XElement to an XmlElement - ///// - ///// - ///// - public static XmlNode? ToXmlElement(this XContainer xElement) - { - var xmlDocument = new XmlDocument(); - using (var xmlReader = xElement.CreateReader()) - { - xmlDocument.Load(xmlReader); - } - return xmlDocument.DocumentElement; - } - - /// - /// Converts from an XmlElement to an XElement - /// - /// - /// - public static XElement ToXElement(this XmlNode xmlElement) - { - using (var nodeReader = new XmlNodeReader(xmlElement)) - { - nodeReader.MoveToContent(); - return XElement.Load(nodeReader); - } - } - - public static T? RequiredAttributeValue(this XElement xml, string attributeName) - { - if (xml == null) - { - throw new ArgumentNullException(nameof(xml)); - } - - if (xml.HasAttributes == false) - { - throw new InvalidOperationException($"{attributeName} not found in xml"); - } - - XAttribute? attribute = xml.Attribute(attributeName); - if (attribute is null) - { - throw new InvalidOperationException($"{attributeName} not found in xml"); - } - - Attempt result = attribute.Value.TryConvertTo(); - if (result.Success) - { - return result.Result; - } - - throw new InvalidOperationException($"{attribute.Value} attribute value cannot be converted to {typeof(T)}"); - } - - public static T? AttributeValue(this XElement xml, string attributeName) - { - if (xml == null) throw new ArgumentNullException("xml"); - if (xml.HasAttributes == false) return default(T); - - if (xml.Attribute(attributeName) == null) - return default(T); - - var val = xml.Attribute(attributeName)?.Value; - var result = val.TryConvertTo(); - if (result.Success) - return result.Result; - - return default(T); - } - - public static T? AttributeValue(this XmlNode xml, string attributeName) - { - if (xml == null) throw new ArgumentNullException("xml"); - if (xml.Attributes == null) return default(T); - - if (xml.Attributes[attributeName] == null) - return default(T); - - var val = xml.Attributes[attributeName]?.Value; - var result = val.TryConvertTo(); - if (result.Success) - return result.Result; - - return default(T); - } - - public static XElement? GetXElement(this XmlNode node) - { - XDocument xDoc = new XDocument(); - using (XmlWriter xmlWriter = xDoc.CreateWriter()) - node.WriteTo(xmlWriter); - return xDoc.Root; - } - - public static XmlNode? GetXmlNode(this XContainer element) - { - using (var xmlReader = element.CreateReader()) - { - var xmlDoc = new XmlDocument(); - xmlDoc.Load(xmlReader); - return xmlDoc.DocumentElement; - } - } - - public static XmlNode? GetXmlNode(this XContainer element, XmlDocument xmlDoc) - { - var node = element.GetXmlNode(); - if (node is not null) - { - return xmlDoc.ImportNode(node, true); - } - - return null; - } - - // this exists because - // new XElement("root", "a\nb").Value is "a\nb" but - // .ToString(SaveOptions.*) is "a\r\nb" and cannot figure out how to get rid of "\r" - // and when saving data we want nothing to change - // this method will produce a string that respects the \r and \n in the data value - public static string ToDataString(this XElement xml) - { - var settings = new XmlWriterSettings - { - OmitXmlDeclaration = true, - NewLineHandling = NewLineHandling.None, - Indent = false - }; - var output = new StringBuilder(); - using (var writer = XmlWriter.Create(output, settings)) - { - xml.WriteTo(writer); - } - return output.ToString(); + nodeReader.MoveToContent(); + return XDocument.Load(nodeReader); } } + + ///// + ///// Converts from an XElement to an XmlElement + ///// + ///// + ///// + public static XmlNode? ToXmlElement(this XContainer xElement) + { + var xmlDocument = new XmlDocument(); + using (XmlReader xmlReader = xElement.CreateReader()) + { + xmlDocument.Load(xmlReader); + } + + return xmlDocument.DocumentElement; + } + + /// + /// Converts from an XmlElement to an XElement + /// + /// + /// + public static XElement ToXElement(this XmlNode xmlElement) + { + using (var nodeReader = new XmlNodeReader(xmlElement)) + { + nodeReader.MoveToContent(); + return XElement.Load(nodeReader); + } + } + + public static T? RequiredAttributeValue(this XElement xml, string attributeName) + { + if (xml == null) + { + throw new ArgumentNullException(nameof(xml)); + } + + if (xml.HasAttributes == false) + { + throw new InvalidOperationException($"{attributeName} not found in xml"); + } + + XAttribute? attribute = xml.Attribute(attributeName); + if (attribute is null) + { + throw new InvalidOperationException($"{attributeName} not found in xml"); + } + + Attempt result = attribute.Value.TryConvertTo(); + if (result.Success) + { + return result.Result; + } + + throw new InvalidOperationException($"{attribute.Value} attribute value cannot be converted to {typeof(T)}"); + } + + public static T? AttributeValue(this XElement xml, string attributeName) + { + if (xml == null) + { + throw new ArgumentNullException("xml"); + } + + if (xml.HasAttributes == false) + { + return default; + } + + if (xml.Attribute(attributeName) == null) + { + return default; + } + + var val = xml.Attribute(attributeName)?.Value; + Attempt result = val.TryConvertTo(); + if (result.Success) + { + return result.Result; + } + + return default; + } + + public static T? AttributeValue(this XmlNode xml, string attributeName) + { + if (xml == null) + { + throw new ArgumentNullException("xml"); + } + + if (xml.Attributes == null) + { + return default; + } + + if (xml.Attributes[attributeName] == null) + { + return default; + } + + var val = xml.Attributes[attributeName]?.Value; + Attempt result = val.TryConvertTo(); + if (result.Success) + { + return result.Result; + } + + return default; + } + + public static XElement? GetXElement(this XmlNode node) + { + var xDoc = new XDocument(); + using (XmlWriter xmlWriter = xDoc.CreateWriter()) + { + node.WriteTo(xmlWriter); + } + + return xDoc.Root; + } + + public static XmlNode? GetXmlNode(this XContainer element) + { + using (XmlReader xmlReader = element.CreateReader()) + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(xmlReader); + return xmlDoc.DocumentElement; + } + } + + public static XmlNode? GetXmlNode(this XContainer element, XmlDocument xmlDoc) + { + XmlNode? node = element.GetXmlNode(); + if (node is not null) + { + return xmlDoc.ImportNode(node, true); + } + + return null; + } + + // this exists because + // new XElement("root", "a\nb").Value is "a\nb" but + // .ToString(SaveOptions.*) is "a\r\nb" and cannot figure out how to get rid of "\r" + // and when saving data we want nothing to change + // this method will produce a string that respects the \r and \n in the data value + public static string ToDataString(this XElement xml) + { + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + NewLineHandling = NewLineHandling.None, + Indent = false, + }; + var output = new StringBuilder(); + using (var writer = XmlWriter.Create(output, settings)) + { + xml.WriteTo(writer); + } + + return output.ToString(); + } } diff --git a/src/Umbraco.Core/Features/DisabledFeatures.cs b/src/Umbraco.Core/Features/DisabledFeatures.cs index e572818baf..e7f9eeb83d 100644 --- a/src/Umbraco.Core/Features/DisabledFeatures.cs +++ b/src/Umbraco.Core/Features/DisabledFeatures.cs @@ -1,34 +1,29 @@ using Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Features +namespace Umbraco.Cms.Core.Features; + +/// +/// Represents disabled features. +/// +public class DisabledFeatures { /// - /// Represents disabled features. + /// Initializes a new instance of the class. /// - public class DisabledFeatures - { - /// - /// Initializes a new instance of the class. - /// - public DisabledFeatures() - { - Controllers = new TypeList(); - } + public DisabledFeatures() => Controllers = new TypeList(); - /// - /// Gets the disabled controllers. - /// - public TypeList Controllers { get; } + /// + /// Gets the disabled controllers. + /// + public TypeList Controllers { get; } - /// - /// Disables the device preview feature of previewing. - /// - public bool DisableDevicePreview { get; set; } + /// + /// Disables the device preview feature of previewing. + /// + public bool DisableDevicePreview { get; set; } - /// - /// If true, all references to templates will be removed in the back office and routing - /// - public bool DisableTemplates { get; set; } - - } + /// + /// If true, all references to templates will be removed in the back office and routing + /// + public bool DisableTemplates { get; set; } } diff --git a/src/Umbraco.Core/Features/EnabledFeatures.cs b/src/Umbraco.Core/Features/EnabledFeatures.cs index 5fb7a581dc..aee19e2f14 100644 --- a/src/Umbraco.Core/Features/EnabledFeatures.cs +++ b/src/Umbraco.Core/Features/EnabledFeatures.cs @@ -1,16 +1,15 @@ -namespace Umbraco.Cms.Core.Features +namespace Umbraco.Cms.Core.Features; + +/// +/// Represents enabled features. +/// +public class EnabledFeatures { /// - /// Represents enabled features. + /// This allows us to inject a razor view into the Umbraco preview view to extend it /// - public class EnabledFeatures - { - /// - /// This allows us to inject a razor view into the Umbraco preview view to extend it - /// - /// - /// This is set to a virtual path of a razor view file - /// - public string? PreviewExtendedView { get; set; } - } + /// + /// This is set to a virtual path of a razor view file + /// + public string? PreviewExtendedView { get; set; } } diff --git a/src/Umbraco.Core/Features/IUmbracoFeature.cs b/src/Umbraco.Core/Features/IUmbracoFeature.cs index efb5337a00..8beaeef321 100644 --- a/src/Umbraco.Core/Features/IUmbracoFeature.cs +++ b/src/Umbraco.Core/Features/IUmbracoFeature.cs @@ -1,10 +1,8 @@ -namespace Umbraco.Cms.Core.Features -{ - /// - /// This is a marker interface to allow controllers to be disabled if also marked with FeatureAuthorizeAttribute. - /// - public interface IUmbracoFeature - { +namespace Umbraco.Cms.Core.Features; - } +/// +/// This is a marker interface to allow controllers to be disabled if also marked with FeatureAuthorizeAttribute. +/// +public interface IUmbracoFeature +{ } diff --git a/src/Umbraco.Core/Features/UmbracoFeatures.cs b/src/Umbraco.Core/Features/UmbracoFeatures.cs index 5b6bfd7bfb..0f971d8ba1 100644 --- a/src/Umbraco.Core/Features/UmbracoFeatures.cs +++ b/src/Umbraco.Core/Features/UmbracoFeatures.cs @@ -1,40 +1,39 @@ -using System; +namespace Umbraco.Cms.Core.Features; -namespace Umbraco.Cms.Core.Features +/// +/// Represents the Umbraco features. +/// +public class UmbracoFeatures { /// - /// Represents the Umbraco features. + /// Initializes a new instance of the class. /// - public class UmbracoFeatures + public UmbracoFeatures() { - /// - /// Initializes a new instance of the class. - /// - public UmbracoFeatures() + Disabled = new DisabledFeatures(); + Enabled = new EnabledFeatures(); + } + + /// + /// Gets the disabled features. + /// + public DisabledFeatures Disabled { get; } + + /// + /// Gets the enabled features. + /// + public EnabledFeatures Enabled { get; } + + /// + /// Determines whether a controller is enabled. + /// + public bool IsControllerEnabled(Type? feature) + { + if (typeof(IUmbracoFeature).IsAssignableFrom(feature)) { - Disabled = new DisabledFeatures(); - Enabled = new EnabledFeatures(); + return Disabled.Controllers.Contains(feature) == false; } - /// - /// Gets the disabled features. - /// - public DisabledFeatures Disabled { get; } - - /// - /// Gets the enabled features. - /// - public EnabledFeatures Enabled { get; } - - /// - /// Determines whether a controller is enabled. - /// - public bool IsControllerEnabled(Type? feature) - { - if (typeof(IUmbracoFeature).IsAssignableFrom(feature)) - return Disabled.Controllers.Contains(feature) == false; - - throw new NotSupportedException("Not a supported feature type."); - } + throw new NotSupportedException("Not a supported feature type."); } } diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs index 53c495ba87..e8280bceb6 100644 --- a/src/Umbraco.Core/GuidUdi.cs +++ b/src/Umbraco.Core/GuidUdi.cs @@ -1,66 +1,60 @@ -using System; using System.ComponentModel; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a guid-based entity identifier. +/// +[TypeConverter(typeof(UdiTypeConverter))] +public class GuidUdi : Udi { /// - /// Represents a guid-based entity identifier. + /// Initializes a new instance of the GuidUdi class with an entity type and a guid. /// - [TypeConverter(typeof(UdiTypeConverter))] - public class GuidUdi : Udi + /// The entity type part of the udi. + /// The guid part of the udi. + public GuidUdi(string entityType, Guid guid) + : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) => + Guid = guid; + + /// + /// Initializes a new instance of the GuidUdi class with an uri value. + /// + /// The uri value of the udi. + public GuidUdi(Uri uriValue) + : base(uriValue) { - /// - /// The guid part of the identifier. - /// - public Guid Guid { get; private set; } - - /// - /// Initializes a new instance of the GuidUdi class with an entity type and a guid. - /// - /// The entity type part of the udi. - /// The guid part of the udi. - public GuidUdi(string entityType, Guid guid) - : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) + if (Guid.TryParse(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash), out Guid guid) == false) { - Guid = guid; + throw new FormatException("URI \"" + uriValue + "\" is not a GUID entity ID."); } - /// - /// Initializes a new instance of the GuidUdi class with an uri value. - /// - /// The uri value of the udi. - public GuidUdi(Uri uriValue) - : base(uriValue) - { - Guid guid; - if (Guid.TryParse(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash), out guid) == false) - throw new FormatException("URI \"" + uriValue + "\" is not a GUID entity ID."); + Guid = guid; + } - Guid = guid; + /// + /// The guid part of the identifier. + /// + public Guid Guid { get; } + + /// + public override bool IsRoot => Guid == Guid.Empty; + + public override bool Equals(object? obj) + { + if (obj is not GuidUdi other) + { + return false; } - public override bool Equals(object? obj) - { - var other = obj as GuidUdi; - if (other is null) return false; - return EntityType == other.EntityType && Guid == other.Guid; - } + return EntityType == other.EntityType && Guid == other.Guid; + } - public override int GetHashCode() - { - return base.GetHashCode(); - } + public override int GetHashCode() => base.GetHashCode(); - /// - public override bool IsRoot - { - get { return Guid == Guid.Empty; } - } - - public GuidUdi EnsureClosed() - { - EnsureNotRoot(); - return this; - } + public GuidUdi EnsureClosed() + { + EnsureNotRoot(); + return this; } } diff --git a/src/Umbraco.Core/GuidUtils.cs b/src/Umbraco.Core/GuidUtils.cs index e6ccd6b27f..290f36cdcf 100644 --- a/src/Umbraco.Core/GuidUtils.cs +++ b/src/Umbraco.Core/GuidUtils.cs @@ -1,112 +1,154 @@ -using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Utility methods for the struct. +/// +public static class GuidUtils { - /// - /// Utility methods for the struct. - /// - public static class GuidUtils + private static readonly char[] Base32Table = { - /// - /// Combines two guid instances utilizing an exclusive disjunction. - /// The resultant guid is not guaranteed to be unique since the number of unique bits is halved. - /// - /// The first guid. - /// The seconds guid. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid Combine(Guid a, Guid b) + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', + 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + }; + + /// + /// Combines two guid instances utilizing an exclusive disjunction. + /// The resultant guid is not guaranteed to be unique since the number of unique bits is halved. + /// + /// The first guid. + /// The seconds guid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid Combine(Guid a, Guid b) + { + var ad = new DecomposedGuid(a); + var bd = new DecomposedGuid(b); + + ad.Hi ^= bd.Hi; + ad.Lo ^= bd.Lo; + + return ad.Value; + } + + /// + /// Converts a Guid into a base-32 string. + /// + /// A Guid. + /// The string length. + /// A base-32 encoded string. + /// + /// + /// A base-32 string representation of a Guid is the shortest, efficient, representation + /// that is case insensitive (base-64 is case sensitive). + /// + /// Length must be 1-26, anything else becomes 26. + /// + public static string ToBase32String(Guid guid, int length = 26) + { + if (length <= 0 || length > 26) { - var ad = new DecomposedGuid(a); - var bd = new DecomposedGuid(b); - - ad.Hi ^= bd.Hi; - ad.Lo ^= bd.Lo; - - return ad.Value; + length = 26; } - /// - /// A decomposed guid. Allows access to the high and low bits without unsafe code. - /// - [StructLayout(LayoutKind.Explicit)] - private struct DecomposedGuid + var bytes = guid.ToByteArray(); // a Guid is 128 bits ie 16 bytes + + // this could be optimized by making it unsafe, + // and fixing the table + bytes + chars (see Convert.ToBase64CharArray) + + // each block of 5 bytes = 5*8 = 40 bits + // becomes 40 bits = 8*5 = 8 byte-32 chars + // a Guid is 3 blocks + 8 bits + + // so it turns into a 3*8+2 = 26 chars string + var chars = new char[length]; + + var i = 0; + var j = 0; + + while (i < 15) { - [FieldOffset(00)] public Guid Value; - [FieldOffset(00)] public long Hi; - [FieldOffset(08)] public long Lo; - - public DecomposedGuid(Guid value) : this() => this.Value = value; - } - - private static readonly char[] Base32Table = - { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', - 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5' - }; - - /// - /// Converts a Guid into a base-32 string. - /// - /// A Guid. - /// The string length. - /// A base-32 encoded string. - /// - /// A base-32 string representation of a Guid is the shortest, efficient, representation - /// that is case insensitive (base-64 is case sensitive). - /// Length must be 1-26, anything else becomes 26. - /// - public static string ToBase32String(Guid guid, int length = 26) - { - - if (length <= 0 || length > 26) - length = 26; - - var bytes = guid.ToByteArray(); // a Guid is 128 bits ie 16 bytes - - // this could be optimized by making it unsafe, - // and fixing the table + bytes + chars (see Convert.ToBase64CharArray) - - // each block of 5 bytes = 5*8 = 40 bits - // becomes 40 bits = 8*5 = 8 byte-32 chars - // a Guid is 3 blocks + 8 bits - - // so it turns into a 3*8+2 = 26 chars string - var chars = new char[length]; - - var i = 0; - var j = 0; - - while (i < 15) + if (j == length) { - if (j == length) break; - chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; - if (j == length) break; - chars[j++] = Base32Table[((bytes[i] & 0b0000_0111) << 2) | ((bytes[i + 1] & 0b1100_0000) >> 6)]; - if (j == length) break; - chars[j++] = Base32Table[(bytes[i + 1] & 0b0011_1110) >> 1]; - if (j == length) break; - chars[j++] = Base32Table[(bytes[i + 1] & 0b0000_0001) | ((bytes[i + 2] & 0b1111_0000) >> 4)]; - if (j == length) break; - chars[j++] = Base32Table[((bytes[i + 2] & 0b0000_1111) << 1) | ((bytes[i + 3] & 0b1000_0000) >> 7)]; - if (j == length) break; - chars[j++] = Base32Table[(bytes[i + 3] & 0b0111_1100) >> 2]; - if (j == length) break; - chars[j++] = Base32Table[((bytes[i + 3] & 0b0000_0011) << 3) | ((bytes[i + 4] & 0b1110_0000) >> 5)]; - if (j == length) break; - chars[j++] = Base32Table[bytes[i + 4] & 0b0001_1111]; - - i += 5; + break; } - if (j < length) - chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; - if (j < length) - chars[j] = Base32Table[(bytes[i] & 0b0000_0111) << 2]; + chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; + if (j == length) + { + break; + } - return new string(chars); + chars[j++] = Base32Table[((bytes[i] & 0b0000_0111) << 2) | ((bytes[i + 1] & 0b1100_0000) >> 6)]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[(bytes[i + 1] & 0b0011_1110) >> 1]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[(bytes[i + 1] & 0b0000_0001) | ((bytes[i + 2] & 0b1111_0000) >> 4)]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[((bytes[i + 2] & 0b0000_1111) << 1) | ((bytes[i + 3] & 0b1000_0000) >> 7)]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[(bytes[i + 3] & 0b0111_1100) >> 2]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[((bytes[i + 3] & 0b0000_0011) << 3) | ((bytes[i + 4] & 0b1110_0000) >> 5)]; + if (j == length) + { + break; + } + + chars[j++] = Base32Table[bytes[i + 4] & 0b0001_1111]; + + i += 5; } + + if (j < length) + { + chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3]; + } + + if (j < length) + { + chars[j] = Base32Table[(bytes[i] & 0b0000_0111) << 2]; + } + + return new string(chars); + } + + /// + /// A decomposed guid. Allows access to the high and low bits without unsafe code. + /// + [StructLayout(LayoutKind.Explicit)] + private struct DecomposedGuid + { + [FieldOffset(00)] + public readonly Guid Value; + [FieldOffset(00)] + public long Hi; + [FieldOffset(08)] + public long Lo; + + public DecomposedGuid(Guid value) + : this() => Value = value; } } diff --git a/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs index f15edfa1be..28fbea027b 100644 --- a/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs +++ b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs @@ -1,10 +1,9 @@ -using System; -using System.Linq; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Notifications; @@ -12,228 +11,295 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Handlers +namespace Umbraco.Cms.Core.Handlers; + +public sealed class AuditNotificationsHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { - public sealed class AuditNotificationsHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + private readonly IAuditService _auditService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IEntityService _entityService; + private readonly GlobalSettings _globalSettings; + private readonly IIpResolver _ipResolver; + private readonly IMemberService _memberService; + private readonly IUserService _userService; + + public AuditNotificationsHandler( + IAuditService auditService, + IUserService userService, + IEntityService entityService, + IIpResolver ipResolver, + IOptionsMonitor globalSettings, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IMemberService memberService) { - private readonly IAuditService _auditService; - private readonly IUserService _userService; - private readonly IEntityService _entityService; - private readonly IIpResolver _ipResolver; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly GlobalSettings _globalSettings; - private readonly IMemberService _memberService; + _auditService = auditService; + _userService = userService; + _entityService = entityService; + _ipResolver = ipResolver; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _memberService = memberService; + _globalSettings = globalSettings.CurrentValue; + } - public AuditNotificationsHandler( - IAuditService auditService, - IUserService userService, - IEntityService entityService, - IIpResolver ipResolver, - IOptionsMonitor globalSettings, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IMemberService memberService) + private IUser CurrentPerformingUser + { + get { - _auditService = auditService; - _userService = userService; - _entityService = entityService; - _ipResolver = ipResolver; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _memberService = memberService; - _globalSettings = globalSettings.CurrentValue; + IUser? identity = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + IUser? user = identity == null ? null : _userService.GetUserById(Convert.ToInt32(identity.Id)); + return user ?? UnknownUser(_globalSettings); } + } - private IUser CurrentPerformingUser + private string PerformingIp => _ipResolver.GetCurrentRequestIpAddress(); + + public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) + { + Id = Constants.Security.UnknownUserId, + Name = Constants.Security.UnknownUserName, + Email = string.Empty, + }; + + public void Handle(AssignedMemberRolesNotification notification) + { + IUser performingUser = CurrentPerformingUser; + var roles = string.Join(", ", notification.Roles); + var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); + foreach (var id in notification.MemberIds) { - get - { - var identity = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; - var user = identity == null ? null : _userService.GetUserById(Convert.ToInt32(identity.Id)); - return user ?? UnknownUser(_globalSettings); - } - } - - public static IUser UnknownUser(GlobalSettings globalSettings) => new User(globalSettings) { Id = Constants.Security.UnknownUserId, Name = Constants.Security.UnknownUserName, Email = "" }; - - private string PerformingIp => _ipResolver.GetCurrentRequestIpAddress(); - - private string FormatEmail(IMember? member) => member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>"; - - private string FormatEmail(IUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>"; - - public void Handle(MemberSavedNotification notification) - { - var performingUser = CurrentPerformingUser; - var members = notification.SavedEntities; - foreach (var member in members) - { - var dp = string.Join(", ", ((Member)member).GetWereDirtyProperties()); - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); - } - } - - public void Handle(MemberDeletedNotification notification) - { - var performingUser = CurrentPerformingUser; - var members = notification.DeletedEntities; - foreach (var member in members) - { - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/delete", $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}"); - } - } - - public void Handle(AssignedMemberRolesNotification notification) - { - var performingUser = CurrentPerformingUser; - var roles = string.Join(", ", notification.Roles); - var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); - foreach (var id in notification.MemberIds) - { - members.TryGetValue(id, out var member); - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", - "umbraco/member/roles/assigned", $"roles modified, assigned {roles}"); - } - } - - public void Handle(RemovedMemberRolesNotification notification) - { - var performingUser = CurrentPerformingUser; - var roles = string.Join(", ", notification.Roles); - var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); - foreach (var id in notification.MemberIds) - { - members.TryGetValue(id, out var member); - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", - "umbraco/member/roles/removed", $"roles modified, removed {roles}"); - } - } - - public void Handle(ExportedMemberNotification notification) - { - var performingUser = CurrentPerformingUser; - var member = notification.Member; - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + members.TryGetValue(id, out IMember? member); + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, DateTime.UtcNow, - -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", - "umbraco/member/exported", "exported member data"); + -1, + $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", + "umbraco/member/roles/assigned", + $"roles modified, assigned {roles}"); } + } - public void Handle(UserSavedNotification notification) + public void Handle(AssignedUserGroupPermissionsNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable perms = notification.EntityPermissions; + foreach (EntityPermission perm in perms) { - var performingUser = CurrentPerformingUser; - var affectedUsers = notification.SavedEntities; - foreach (var affectedUser in affectedUsers) + IUserGroup? group = _userService.GetUserGroupById(perm.UserGroupId); + var assigned = string.Join(", ", perm.AssignedPermissions ?? Array.Empty()); + IEntitySlim? entity = _entityService.Get(perm.EntityId); + + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"User Group {group?.Id} \"{group?.Name}\" ({group?.Alias})", + "umbraco/user-group/permissions-change", + $"assigning {(string.IsNullOrWhiteSpace(assigned) ? "(nothing)" : assigned)} on id:{perm.EntityId} \"{entity?.Name}\""); + } + } + + public void Handle(ExportedMemberNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IMember member = notification.Member; + + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/exported", + "exported member data"); + } + + public void Handle(MemberDeletedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable members = notification.DeletedEntities; + foreach (IMember member in members) + { + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/delete", + $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}"); + } + } + + public void Handle(MemberSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable members = notification.SavedEntities; + foreach (IMember member in members) + { + var dp = string.Join(", ", ((Member)member).GetWereDirtyProperties()); + + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}", + "umbraco/member/save", + $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}"); + } + } + + public void Handle(RemovedMemberRolesNotification notification) + { + IUser performingUser = CurrentPerformingUser; + var roles = string.Join(", ", notification.Roles); + var members = _memberService.GetAllMembers(notification.MemberIds).ToDictionary(x => x.Id, x => x); + foreach (var id in notification.MemberIds) + { + members.TryGetValue(id, out IMember? member); + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}", + "umbraco/member/roles/removed", + $"roles modified, removed {roles}"); + } + } + + public void Handle(UserDeletedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable affectedUsers = notification.DeletedEntities; + foreach (IUser affectedUser in affectedUsers) + { + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + affectedUser.Id, + $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/delete", + "delete user"); + } + } + + public void Handle(UserGroupWithUsersSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + foreach (UserGroupWithUsers groupWithUser in notification.SavedEntities) + { + IUserGroup group = groupWithUser.UserGroup; + + var dp = string.Join(", ", ((UserGroup)group).GetWereDirtyProperties()); + var sections = ((UserGroup)group).WasPropertyDirty("AllowedSections") + ? string.Join(", ", group.AllowedSections) + : null; + var perms = ((UserGroup)group).WasPropertyDirty("Permissions") && group.Permissions is not null + ? string.Join(", ", group.Permissions) + : null; + + var sb = new StringBuilder(); + sb.Append($"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)};"); + if (sections != null) { - var groups = affectedUser.WasPropertyDirty("Groups") - ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) - : null; - - var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties()); - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", - "umbraco/user/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? "" : "; groups assigned: " + groups)}"); + sb.Append($", assigned sections: {sections}"); } - } - public void Handle(UserDeletedNotification notification) - { - var performingUser = CurrentPerformingUser; - var affectedUsers = notification.DeletedEntities; - foreach (var affectedUser in affectedUsers) - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", - "umbraco/user/delete", "delete user"); - } - - public void Handle(UserGroupWithUsersSavedNotification notification) - { - var performingUser = CurrentPerformingUser; - foreach (var groupWithUser in notification.SavedEntities) + if (perms != null) { - var group = groupWithUser.UserGroup; - - var dp = string.Join(", ", ((UserGroup)group).GetWereDirtyProperties()); - var sections = ((UserGroup)group).WasPropertyDirty("AllowedSections") - ? string.Join(", ", group.AllowedSections) - : null; - var perms = ((UserGroup)group).WasPropertyDirty("Permissions") && group.Permissions is not null - ? string.Join(", ", group.Permissions) - : null; - - var sb = new StringBuilder(); - sb.Append($"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)};"); if (sections != null) - sb.Append($", assigned sections: {sections}"); - if (perms != null) { - if (sections != null) - sb.Append(", "); - sb.Append($"default perms: {perms}"); + sb.Append(", "); } - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - -1, $"User Group {group.Id} \"{group.Name}\" ({group.Alias})", - "umbraco/user-group/save", $"{sb}"); - - // now audit the users that have changed - - foreach (var user in groupWithUser.RemovedUsers) - { - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - user.Id, $"User \"{user.Name}\" {FormatEmail(user)}", - "umbraco/user-group/save", $"Removed user \"{user.Name}\" {FormatEmail(user)} from group {group.Id} \"{group.Name}\" ({group.Alias})"); - } - - foreach (var user in groupWithUser.AddedUsers) - { - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, - DateTime.UtcNow, - user.Id, $"User \"{user.Name}\" {FormatEmail(user)}", - "umbraco/user-group/save", $"Added user \"{user.Name}\" {FormatEmail(user)} to group {group.Id} \"{group.Name}\" ({group.Alias})"); - } + sb.Append($"default perms: {perms}"); } - } - public void Handle(AssignedUserGroupPermissionsNotification notification) - { - var performingUser = CurrentPerformingUser; - var perms = notification.EntityPermissions; - foreach (EntityPermission perm in perms) + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + -1, + $"User Group {group.Id} \"{group.Name}\" ({group.Alias})", + "umbraco/user-group/save", + $"{sb}"); + + // now audit the users that have changed + foreach (IUser user in groupWithUser.RemovedUsers) { - var group = _userService.GetUserGroupById(perm.UserGroupId); - var assigned = string.Join(", ", perm.AssignedPermissions ?? Array.Empty()); - var entity = _entityService.Get(perm.EntityId); - - _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp, + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, DateTime.UtcNow, - -1, $"User Group {group?.Id} \"{group?.Name}\" ({group?.Alias})", - "umbraco/user-group/permissions-change", $"assigning {(string.IsNullOrWhiteSpace(assigned) ? "(nothing)" : assigned)} on id:{perm.EntityId} \"{entity?.Name}\""); + user.Id, + $"User \"{user.Name}\" {FormatEmail(user)}", + "umbraco/user-group/save", + $"Removed user \"{user.Name}\" {FormatEmail(user)} from group {group.Id} \"{group.Name}\" ({group.Alias})"); + } + + foreach (IUser user in groupWithUser.AddedUsers) + { + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + user.Id, + $"User \"{user.Name}\" {FormatEmail(user)}", + "umbraco/user-group/save", + $"Added user \"{user.Name}\" {FormatEmail(user)} to group {group.Id} \"{group.Name}\" ({group.Alias})"); } } } + + public void Handle(UserSavedNotification notification) + { + IUser performingUser = CurrentPerformingUser; + IEnumerable affectedUsers = notification.SavedEntities; + foreach (IUser affectedUser in affectedUsers) + { + var groups = affectedUser.WasPropertyDirty("Groups") + ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias)) + : null; + + var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties()); + + _auditService.Write( + performingUser.Id, + $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", + PerformingIp, + DateTime.UtcNow, + affectedUser.Id, + $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}", + "umbraco/user/save", + $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? string.Empty : "; groups assigned: " + groups)}"); + } + } + + private string FormatEmail(IMember? member) => + member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? string.Empty : $"<{member.Email}>"; + + private string FormatEmail(IUser user) => user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? string.Empty : $"<{user.Email}>"; } diff --git a/src/Umbraco.Core/Handlers/PublicAccessHandler.cs b/src/Umbraco.Core/Handlers/PublicAccessHandler.cs index 466e09e3f1..d441509a85 100644 --- a/src/Umbraco.Core/Handlers/PublicAccessHandler.cs +++ b/src/Umbraco.Core/Handlers/PublicAccessHandler.cs @@ -1,38 +1,37 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Handlers +namespace Umbraco.Cms.Core.Handlers; + +public sealed class PublicAccessHandler : + INotificationHandler, + INotificationHandler { - public sealed class PublicAccessHandler : - INotificationHandler, - INotificationHandler + private readonly IPublicAccessService _publicAccessService; + + public PublicAccessHandler(IPublicAccessService publicAccessService) => + _publicAccessService = publicAccessService ?? throw new ArgumentNullException(nameof(publicAccessService)); + + public void Handle(MemberGroupDeletedNotification notification) => Handle(notification.DeletedEntities); + + public void Handle(MemberGroupSavedNotification notification) => Handle(notification.SavedEntities); + + private void Handle(IEnumerable affectedEntities) { - private readonly IPublicAccessService _publicAccessService; - - public PublicAccessHandler(IPublicAccessService publicAccessService) => - _publicAccessService = publicAccessService ?? throw new ArgumentNullException(nameof(publicAccessService)); - - public void Handle(MemberGroupSavedNotification notification) => Handle(notification.SavedEntities); - - public void Handle(MemberGroupDeletedNotification notification) => Handle(notification.DeletedEntities); - - private void Handle(IEnumerable affectedEntities) + foreach (IMemberGroup grp in affectedEntities) { - foreach (var grp in affectedEntities) + // check if the name has changed + if ((grp.AdditionalData?.ContainsKey("previousName") ?? false) + && grp.AdditionalData["previousName"] != null + && grp.AdditionalData["previousName"]?.ToString().IsNullOrWhiteSpace() == false + && grp.AdditionalData["previousName"]?.ToString() != grp.Name) { - //check if the name has changed - if ((grp.AdditionalData?.ContainsKey("previousName") ?? false) - && grp.AdditionalData["previousName"] != null - && grp.AdditionalData["previousName"]?.ToString().IsNullOrWhiteSpace() == false - && grp.AdditionalData["previousName"]?.ToString() != grp.Name) - { - _publicAccessService.RenameMemberGroupRoleRules(grp.AdditionalData["previousName"]?.ToString(), grp.Name); - } + _publicAccessService.RenameMemberGroupRoleRules( + grp.AdditionalData["previousName"]?.ToString(), + grp.Name); } } } diff --git a/src/Umbraco.Core/HashCodeCombiner.cs b/src/Umbraco.Core/HashCodeCombiner.cs index d8c1ac2a07..3506d335b8 100644 --- a/src/Umbraco.Core/HashCodeCombiner.cs +++ b/src/Umbraco.Core/HashCodeCombiner.cs @@ -1,100 +1,83 @@ -using System; using System.Globalization; -using System.IO; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Used to create a .NET HashCode from multiple objects. +/// +/// +/// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things +/// which we've not included here as we just need a quick easy class for this in order to create a unique +/// hash of directories/files to see if they have changed. +/// NOTE: It's probably best to not relying on the hashing result across AppDomains! If you need a constant/reliable +/// hash value +/// between AppDomains use SHA1. This is perfect for hashing things in a very fast way for a single AppDomain. +/// +public class HashCodeCombiner { - /// - /// Used to create a .NET HashCode from multiple objects. - /// - /// - /// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things - /// which we've not included here as we just need a quick easy class for this in order to create a unique - /// hash of directories/files to see if they have changed. - /// - /// NOTE: It's probably best to not relying on the hashing result across AppDomains! If you need a constant/reliable hash value - /// between AppDomains use SHA1. This is perfect for hashing things in a very fast way for a single AppDomain. - /// - public class HashCodeCombiner + private long _combinedHash = 5381L; + + public void AddInt(int i) => _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + + public void AddObject(object o) => AddInt(o.GetHashCode()); + + public void AddDateTime(DateTime d) => AddInt(d.GetHashCode()); + + public void AddString(string s) { - private long _combinedHash = 5381L; - - public void AddInt(int i) + if (s != null) { - _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + AddInt(StringComparer.InvariantCulture.GetHashCode(s)); } - - public void AddObject(object o) - { - AddInt(o.GetHashCode()); - } - - public void AddDateTime(DateTime d) - { - AddInt(d.GetHashCode()); - } - - public void AddString(string s) - { - if (s != null) - AddInt((StringComparer.InvariantCulture).GetHashCode(s)); - } - - public void AddCaseInsensitiveString(string s) - { - if (s != null) - AddInt((StringComparer.InvariantCultureIgnoreCase).GetHashCode(s)); - } - - public void AddFileSystemItem(FileSystemInfo f) - { - //if it doesn't exist, don't proceed. - if (!f.Exists) - return; - - AddCaseInsensitiveString(f.FullName); - AddDateTime(f.CreationTimeUtc); - AddDateTime(f.LastWriteTimeUtc); - - //check if it is a file or folder - var fileInfo = f as FileInfo; - if (fileInfo != null) - { - AddInt(fileInfo.Length.GetHashCode()); - } - - var dirInfo = f as DirectoryInfo; - if (dirInfo != null) - { - foreach (var d in dirInfo.GetFiles()) - { - AddFile(d); - } - foreach (var s in dirInfo.GetDirectories()) - { - AddFolder(s); - } - } - } - - public void AddFile(FileInfo f) - { - AddFileSystemItem(f); - } - - public void AddFolder(DirectoryInfo d) - { - AddFileSystemItem(d); - } - - /// - /// Returns the hex code of the combined hash code - /// - /// - public string GetCombinedHashCode() - { - return _combinedHash.ToString("x", CultureInfo.InvariantCulture); - } - } + + public void AddCaseInsensitiveString(string s) + { + if (s != null) + { + AddInt(StringComparer.InvariantCultureIgnoreCase.GetHashCode(s)); + } + } + + public void AddFileSystemItem(FileSystemInfo f) + { + // if it doesn't exist, don't proceed. + if (!f.Exists) + { + return; + } + + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); + + // check if it is a file or folder + if (f is FileInfo fileInfo) + { + AddInt(fileInfo.Length.GetHashCode()); + } + + if (f is DirectoryInfo dirInfo) + { + foreach (FileInfo d in dirInfo.GetFiles()) + { + AddFile(d); + } + + foreach (DirectoryInfo s in dirInfo.GetDirectories()) + { + AddFolder(s); + } + } + } + + public void AddFile(FileInfo f) => AddFileSystemItem(f); + + public void AddFolder(DirectoryInfo d) => AddFileSystemItem(d); + + /// + /// Returns the hex code of the combined hash code + /// + /// + public string GetCombinedHashCode() => _combinedHash.ToString("x", CultureInfo.InvariantCulture); } diff --git a/src/Umbraco.Core/HashCodeHelper.cs b/src/Umbraco.Core/HashCodeHelper.cs index 6d98ec57b8..ecf209c532 100644 --- a/src/Umbraco.Core/HashCodeHelper.cs +++ b/src/Umbraco.Core/HashCodeHelper.cs @@ -1,104 +1,115 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Borrowed from http://stackoverflow.com/a/2575444/694494 +/// +public static class HashCodeHelper { - /// - /// Borrowed from http://stackoverflow.com/a/2575444/694494 - /// - public static class HashCodeHelper + public static int GetHashCode(T1 arg1, T2 arg2) { - public static int GetHashCode(T1 arg1, T2 arg2) + unchecked { - unchecked - { - return 31 * arg1!.GetHashCode() + arg2!.GetHashCode(); - } + return (31 * arg1!.GetHashCode()) + arg2!.GetHashCode(); } + } - public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3) + public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3) + { + unchecked { - unchecked - { - int hash = arg1!.GetHashCode(); - hash = 31 * hash + arg2!.GetHashCode(); - return 31 * hash + arg3!.GetHashCode(); - } + var hash = arg1!.GetHashCode(); + hash = (31 * hash) + arg2!.GetHashCode(); + return (31 * hash) + arg3!.GetHashCode(); } + } - public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3, - T4 arg4) + public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + unchecked { - unchecked - { - int hash = arg1!.GetHashCode(); - hash = 31 * hash + arg2!.GetHashCode(); - hash = 31 * hash + arg3!.GetHashCode(); - return 31 * hash + arg4!.GetHashCode(); - } + var hash = arg1!.GetHashCode(); + hash = (31 * hash) + arg2!.GetHashCode(); + hash = (31 * hash) + arg3!.GetHashCode(); + return (31 * hash) + arg4!.GetHashCode(); } + } - public static int GetHashCode(T[] list) + public static int GetHashCode(T[] list) + { + unchecked { - unchecked + var hash = 0; + foreach (T item in list) { - int hash = 0; - foreach (var item in list) + if (item == null) { - if (item == null) continue; - hash = 31 * hash + item.GetHashCode(); + continue; } - return hash; - } - } - public static int GetHashCode(IEnumerable list) + hash = (31 * hash) + item.GetHashCode(); + } + + return hash; + } + } + + public static int GetHashCode(IEnumerable list) + { + unchecked { - unchecked + var hash = 0; + foreach (T item in list) { - int hash = 0; - foreach (var item in list) + if (item == null) { - if (item == null) continue; - hash = 31 * hash + item.GetHashCode(); + continue; } - return hash; - } - } - /// - /// Gets a hashcode for a collection for that the order of items - /// does not matter. - /// So {1, 2, 3} and {3, 2, 1} will get same hash code. - /// - public static int GetHashCodeForOrderNoMatterCollection( - IEnumerable list) + hash = (31 * hash) + item.GetHashCode(); + } + + return hash; + } + } + + /// + /// Gets a hashcode for a collection for that the order of items + /// does not matter. + /// So {1, 2, 3} and {3, 2, 1} will get same hash code. + /// + public static int GetHashCodeForOrderNoMatterCollection( + IEnumerable list) + { + unchecked { - unchecked + var hash = 0; + var count = 0; + foreach (T item in list) { - int hash = 0; - int count = 0; - foreach (var item in list) + if (item == null) { - if (item == null) continue; - hash += item.GetHashCode(); - count++; + continue; } - return 31 * hash + count.GetHashCode(); - } - } - /// - /// Alternative way to get a hashcode is to use a fluent - /// interface like this:
- /// return 0.CombineHashCode(field1).CombineHashCode(field2). - /// CombineHashCode(field3); - ///
- public static int CombineHashCode(this int hashCode, T arg) - { - unchecked - { - return 31 * hashCode + arg!.GetHashCode(); + hash += item.GetHashCode(); + count++; } + + return (31 * hash) + count.GetHashCode(); + } + } + + /// + /// Alternative way to get a hashcode is to use a fluent + /// interface like this:
+ /// return 0.CombineHashCode(field1).CombineHashCode(field2). + /// CombineHashCode(field3); + ///
+ public static int CombineHashCode(this int hashCode, T arg) + { + unchecked + { + return (31 * hashCode) + arg!.GetHashCode(); } } } diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 944e0bdf49..cad3d4b6b8 100644 --- a/src/Umbraco.Core/HashGenerator.cs +++ b/src/Umbraco.Core/HashGenerator.cs @@ -1,151 +1,137 @@ -using System; -using System.IO; using System.Security.Cryptography; using System.Text; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Used to generate a string hash using crypto libraries over multiple objects +/// +/// +/// This should be used to generate a reliable hash that survives AppDomain restarts. +/// This will use the crypto libs to generate the hash and will try to ensure that +/// strings, etc... are not re-allocated so it's not consuming much memory. +/// +public class HashGenerator : DisposableObjectSlim { - /// - /// Used to generate a string hash using crypto libraries over multiple objects - /// - /// - /// This should be used to generate a reliable hash that survives AppDomain restarts. - /// This will use the crypto libs to generate the hash and will try to ensure that - /// strings, etc... are not re-allocated so it's not consuming much memory. - /// - public class HashGenerator : DisposableObjectSlim + private readonly MemoryStream _ms = new(); + private StreamWriter _writer; + + public HashGenerator() => _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, true); + + public void AddInt(int i) => _writer.Write(i); + + public void AddLong(long i) => _writer.Write(i); + + public void AddObject(object o) => _writer.Write(o); + + public void AddDateTime(DateTime d) => _writer.Write(d.Ticks); + + public void AddString(string s) { - public HashGenerator() + if (s != null) { - _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, leaveOpen: true); - } - - private readonly MemoryStream _ms = new MemoryStream(); - private StreamWriter _writer; - - public void AddInt(int i) - { - _writer.Write(i); - } - - public void AddLong(long i) - { - _writer.Write(i); - } - - public void AddObject(object o) - { - _writer.Write(o); - } - - public void AddDateTime(DateTime d) - { - _writer.Write(d.Ticks); - } - - public void AddString(string s) - { - if (s != null) - _writer.Write(s); - } - - public void AddCaseInsensitiveString(string s) - { - //I've tried to no allocate a new string with this which can be done if we use the CompareInfo.GetSortKey method which will create a new - //byte array that we can use to write to the output, however this also allocates new objects so i really don't think the performance - //would be much different. In any case, I'll leave this here for reference. We could write the bytes out based on the sort key, - //this is how we could deal with case insensitivity without allocating another string - //for reference see: https://stackoverflow.com/a/10452967/694494 - //we could go a step further and s.Normalize() but we're not really dealing with crazy unicode with this class so far. - - if (s != null) - _writer.Write(s.ToUpperInvariant()); - } - - public void AddFileSystemItem(FileSystemInfo f) - { - //if it doesn't exist, don't proceed. - if (f.Exists == false) - return; - - AddCaseInsensitiveString(f.FullName); - AddDateTime(f.CreationTimeUtc); - AddDateTime(f.LastWriteTimeUtc); - - //check if it is a file or folder - if (f is FileInfo fileInfo) - { - AddLong(fileInfo.Length); - } - - if (f is DirectoryInfo dirInfo) - { - foreach (var d in dirInfo.GetFiles()) - { - AddFile(d); - } - foreach (var s in dirInfo.GetDirectories()) - { - AddFolder(s); - } - } - } - - public void AddFile(FileInfo f) - { - AddFileSystemItem(f); - } - - public void AddFolder(DirectoryInfo d) - { - AddFileSystemItem(d); - } - - /// - /// Returns the generated hash output of all added objects - /// - /// - public string GenerateHash() - { - //flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called. - - _writer.Flush(); - _writer.Close(); - _writer.Dispose(); - _writer = new StreamWriter(_ms, Encoding.UTF8, 1024, leaveOpen: true); - - var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5"; - - //create an instance of the correct hashing provider based on the type passed in - var hasher = HashAlgorithm.Create(hashType); - if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType); - using (hasher) - { - var buffer = _ms.GetBuffer(); - //get the hashed values created by our selected provider - var hashedByteArray = hasher.ComputeHash(buffer); - - //create a StringBuilder object - var stringBuilder = new StringBuilder(); - - //loop to each byte - foreach (var b in hashedByteArray) - { - //append it to our StringBuilder - stringBuilder.Append(b.ToString("x2")); - } - - //return the hashed value - return stringBuilder.ToString(); - } - } - - protected override void DisposeResources() - { - _writer.Close(); - _writer.Dispose(); - _ms.Close(); - _ms.Dispose(); + _writer.Write(s); } } + + public void AddCaseInsensitiveString(string s) + { + // I've tried to no allocate a new string with this which can be done if we use the CompareInfo.GetSortKey method which will create a new + // byte array that we can use to write to the output, however this also allocates new objects so i really don't think the performance + // would be much different. In any case, I'll leave this here for reference. We could write the bytes out based on the sort key, + // this is how we could deal with case insensitivity without allocating another string + // for reference see: https://stackoverflow.com/a/10452967/694494 + // we could go a step further and s.Normalize() but we're not really dealing with crazy unicode with this class so far. + if (s != null) + { + _writer.Write(s.ToUpperInvariant()); + } + } + + public void AddFileSystemItem(FileSystemInfo f) + { + // if it doesn't exist, don't proceed. + if (f.Exists == false) + { + return; + } + + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); + + // check if it is a file or folder + if (f is FileInfo fileInfo) + { + AddLong(fileInfo.Length); + } + + if (f is DirectoryInfo dirInfo) + { + foreach (FileInfo d in dirInfo.GetFiles()) + { + AddFile(d); + } + + foreach (DirectoryInfo s in dirInfo.GetDirectories()) + { + AddFolder(s); + } + } + } + + public void AddFile(FileInfo f) => AddFileSystemItem(f); + + public void AddFolder(DirectoryInfo d) => AddFileSystemItem(d); + + /// + /// Returns the generated hash output of all added objects + /// + /// + public string GenerateHash() + { + // flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called. + _writer.Flush(); + _writer.Close(); + _writer.Dispose(); + _writer = new StreamWriter(_ms, Encoding.UTF8, 1024, true); + + var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5"; + + // create an instance of the correct hashing provider based on the type passed in + var hasher = HashAlgorithm.Create(hashType); + if (hasher == null) + { + throw new InvalidOperationException("No hashing type found by name " + hashType); + } + + using (hasher) + { + var buffer = _ms.GetBuffer(); + + // get the hashed values created by our selected provider + var hashedByteArray = hasher.ComputeHash(buffer); + + // create a StringBuilder object + var stringBuilder = new StringBuilder(); + + // loop to each byte + foreach (var b in hashedByteArray) + { + // append it to our StringBuilder + stringBuilder.Append(b.ToString("x2")); + } + + // return the hashed value + return stringBuilder.ToString(); + } + } + + protected override void DisposeResources() + { + _writer.Close(); + _writer.Dispose(); + _ms.Close(); + _ms.Dispose(); + } } diff --git a/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs b/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs index 93cdea7c0b..42420b8954 100644 --- a/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs +++ b/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class AcceptableConfiguration { - public class AcceptableConfiguration - { - public string? Value { get; set; } - public bool IsRecommended { get; set; } - } + public string? Value { get; set; } + + public bool IsRecommended { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs index 7123255b0d..4dddf34270 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs @@ -1,101 +1,93 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks +namespace Umbraco.Cms.Core.HealthChecks.Checks; + +/// +/// Provides a base class for health checks of configuration values. +/// +public abstract class AbstractSettingsCheck : HealthCheck { /// - /// Provides a base class for health checks of configuration values. + /// Initializes a new instance of the class. /// - public abstract class AbstractSettingsCheck : HealthCheck + protected AbstractSettingsCheck(ILocalizedTextService textService) => LocalizedTextService = textService; + + /// + /// Gets key within the JSON to check, in the colon-delimited format + /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1 + /// + public abstract string ItemPath { get; } + + /// + /// Gets the localized text service. + /// + protected ILocalizedTextService LocalizedTextService { get; } + + /// + /// Gets a link to an external resource with more information. + /// + public abstract string ReadMoreLink { get; } + + /// + /// Gets the values to compare against. + /// + public abstract IEnumerable Values { get; } + + /// + /// Gets the current value of the config setting + /// + public abstract string CurrentValue { get; } + + /// + /// Gets the comparison type for checking the value. + /// + public abstract ValueComparisonType ValueComparisonType { get; } + + /// + /// Gets the message for when the check has succeeded. + /// + public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck", "checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); + + /// + /// Gets the message for when the check has failed. + /// + public virtual string CheckErrorMessage => + ValueComparisonType == ValueComparisonType.ShouldEqual + ? LocalizedTextService.Localize( + "healthcheck", "checkErrorMessageDifferentExpectedValue", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }) + : LocalizedTextService.Localize( + "healthcheck", "checkErrorMessageUnexpectedValue", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); + + /// + public override Task> GetStatus() { - /// - /// Initializes a new instance of the class. - /// - protected AbstractSettingsCheck(ILocalizedTextService textService) => LocalizedTextService = textService; + // update the successMessage with the CurrentValue + var successMessage = string.Format(CheckSuccessMessage, ItemPath, Values, CurrentValue); + var valueFound = Values.Any(value => + string.Equals(CurrentValue, value.Value, StringComparison.InvariantCultureIgnoreCase)); - /// - /// Gets the localized text service. - /// - protected ILocalizedTextService LocalizedTextService { get; } - - /// - /// Gets key within the JSON to check, in the colon-delimited format - /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1 - /// - public abstract string ItemPath { get; } - - /// - /// Gets a link to an external resource with more information. - /// - public abstract string ReadMoreLink { get; } - - /// - /// Gets the values to compare against. - /// - public abstract IEnumerable Values { get; } - - /// - /// Gets the current value of the config setting - /// - public abstract string CurrentValue { get; } - - /// - /// Gets the comparison type for checking the value. - /// - public abstract ValueComparisonType ValueComparisonType { get; } - - /// - /// Gets the message for when the check has succeeded. - /// - public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck", "checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); - - /// - /// Gets the message for when the check has failed. - /// - public virtual string CheckErrorMessage => - ValueComparisonType == ValueComparisonType.ShouldEqual - ? LocalizedTextService.Localize( - "healthcheck", "checkErrorMessageDifferentExpectedValue", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }) - : LocalizedTextService.Localize( - "healthcheck", "checkErrorMessageUnexpectedValue", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); - - /// - public override Task> GetStatus() + if ((ValueComparisonType == ValueComparisonType.ShouldEqual && valueFound) + || (ValueComparisonType == ValueComparisonType.ShouldNotEqual && valueFound == false)) { - // update the successMessage with the CurrentValue - var successMessage = string.Format(CheckSuccessMessage, ItemPath, Values, CurrentValue); - bool valueFound = Values.Any(value => string.Equals(CurrentValue, value.Value, StringComparison.InvariantCultureIgnoreCase)); - - if ((ValueComparisonType == ValueComparisonType.ShouldEqual && valueFound) - || (ValueComparisonType == ValueComparisonType.ShouldNotEqual && valueFound == false)) - { - return Task.FromResult(new HealthCheckStatus(successMessage) - { - ResultType = StatusResultType.Success, - }.Yield()); - } - - string resultMessage = string.Format(CheckErrorMessage, ItemPath, Values, CurrentValue); - var healthCheckStatus = new HealthCheckStatus(resultMessage) - { - ResultType = StatusResultType.Error, - ReadMoreLink = ReadMoreLink - }; - - return Task.FromResult(healthCheckStatus.Yield()); + return Task.FromResult( + new HealthCheckStatus(successMessage) { ResultType = StatusResultType.Success }.Yield()); } - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new NotSupportedException("Configuration cannot be automatically fixed."); + var resultMessage = string.Format(CheckErrorMessage, ItemPath, Values, CurrentValue); + var healthCheckStatus = new HealthCheckStatus(resultMessage) + { + ResultType = StatusResultType.Error, + ReadMoreLink = ReadMoreLink, + }; + + return Task.FromResult(healthCheckStatus.Yield()); } + + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new NotSupportedException("Configuration cannot be automatically fixed."); } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs index 2ded5a0659..a212a69a3e 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs @@ -1,92 +1,79 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Macros; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration +namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration; + +/// +/// Health check for the recommended production configuration for Macro Errors. +/// +[HealthCheck( + "D0F7599E-9B2A-4D9E-9883-81C7EDC5616F", + "Macro errors", + Description = "Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.", + Group = "Configuration")] +public class MacroErrorsCheck : AbstractSettingsCheck { + private readonly IOptionsMonitor _contentSettings; + private readonly ILocalizedTextService _textService; + /// - /// Health check for the recommended production configuration for Macro Errors. + /// Initializes a new instance of the class. /// - [HealthCheck( - "D0F7599E-9B2A-4D9E-9883-81C7EDC5616F", - "Macro errors", - Description = "Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.", - Group = "Configuration")] - public class MacroErrorsCheck : AbstractSettingsCheck + public MacroErrorsCheck( + ILocalizedTextService textService, + IOptionsMonitor contentSettings) + : base(textService) { - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _contentSettings; - - /// - /// Initializes a new instance of the class. - /// - public MacroErrorsCheck( - ILocalizedTextService textService, - IOptionsMonitor contentSettings) - : base(textService) - { - _textService = textService; - _contentSettings = contentSettings; - } - - /// - public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.MacroErrorsCheck; - - /// - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - - /// - public override string ItemPath => Constants.Configuration.ConfigContentMacroErrors; - - /// - /// Gets the values to compare against. - /// - public override IEnumerable Values - { - get - { - var values = new List - { - new AcceptableConfiguration - { - IsRecommended = true, - Value = MacroErrorBehaviour.Inline.ToString() - }, - new AcceptableConfiguration - { - IsRecommended = false, - Value = MacroErrorBehaviour.Silent.ToString() - } - }; - - return values; - } - } - - /// - public override string CurrentValue => _contentSettings.CurrentValue.MacroErrors.ToString(); - - /// - /// Gets the message for when the check has succeeded. - /// - public override string CheckSuccessMessage => - _textService.Localize( - "healthcheck","macroErrorModeCheckSuccessMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); - - /// - /// Gets the message for when the check has failed. - /// - public override string CheckErrorMessage => - _textService.Localize( - "healthcheck","macroErrorModeCheckErrorMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); + _textService = textService; + _contentSettings = contentSettings; } + + /// + public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.MacroErrorsCheck; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; + + /// + public override string ItemPath => Constants.Configuration.ConfigContentMacroErrors; + + /// + /// Gets the values to compare against. + /// + public override IEnumerable Values + { + get + { + var values = new List + { + new() { IsRecommended = true, Value = MacroErrorBehaviour.Inline.ToString() }, + new() { IsRecommended = false, Value = MacroErrorBehaviour.Silent.ToString() }, + }; + + return values; + } + } + + /// + public override string CurrentValue => _contentSettings.CurrentValue.MacroErrors.ToString(); + + /// + /// Gets the message for when the check has succeeded. + /// + public override string CheckSuccessMessage => + _textService.Localize( + "healthcheck", "macroErrorModeCheckSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); + + /// + /// Gets the message for when the check has failed. + /// + public override string CheckErrorMessage => + _textService.Localize( + "healthcheck", "macroErrorModeCheckErrorMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs index 9cb5639205..9629aa8917 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs @@ -1,62 +1,61 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration +namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration; + +/// +/// Health check for the recommended production configuration for Notification Email. +/// +[HealthCheck( + "3E2F7B14-4B41-452B-9A30-E67FBC8E1206", + "Notification Email Settings", + Description = "If notifications are used, the 'from' email address should be specified and changed from the default value.", + Group = "Configuration")] +public class NotificationEmailCheck : AbstractSettingsCheck { + private const string DefaultFromEmail = "your@email.here"; + private readonly IOptionsMonitor _contentSettings; /// - /// Health check for the recommended production configuration for Notification Email. + /// Initializes a new instance of the class. /// - [HealthCheck( - "3E2F7B14-4B41-452B-9A30-E67FBC8E1206", - "Notification Email Settings", - Description = "If notifications are used, the 'from' email address should be specified and changed from the default value.", - Group = "Configuration")] - public class NotificationEmailCheck : AbstractSettingsCheck + public NotificationEmailCheck( + ILocalizedTextService textService, + IOptionsMonitor contentSettings) + : base(textService) => + _contentSettings = contentSettings; + + /// + public override string ItemPath => Constants.Configuration.ConfigContentNotificationsEmail; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldNotEqual; + + /// + public override IEnumerable Values => new List { - private readonly IOptionsMonitor _contentSettings; - private const string DefaultFromEmail = "your@email.here"; + new() { IsRecommended = false, Value = DefaultFromEmail }, new() { IsRecommended = false, Value = string.Empty }, + }; - /// - /// Initializes a new instance of the class. - /// - public NotificationEmailCheck( - ILocalizedTextService textService, - IOptionsMonitor contentSettings) - : base(textService) => - _contentSettings = contentSettings; + /// + public override string CurrentValue => _contentSettings.CurrentValue.Notifications.Email ?? string.Empty; - /// - public override string ItemPath => Constants.Configuration.ConfigContentNotificationsEmail; + /// + public override string CheckSuccessMessage => + LocalizedTextService.Localize("healthcheck", "notificationEmailsCheckSuccessMessage", new[] { CurrentValue ?? "<null>" }); - /// - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldNotEqual; + /// + public override string CheckErrorMessage => LocalizedTextService.Localize( + "healthcheck", + "notificationEmailsCheckErrorMessage", + new[] { DefaultFromEmail }); - /// - public override IEnumerable Values => new List - { - new AcceptableConfiguration { IsRecommended = false, Value = DefaultFromEmail }, - new AcceptableConfiguration { IsRecommended = false, Value = string.Empty } - }; - - /// - public override string CurrentValue => _contentSettings.CurrentValue.Notifications.Email ?? string.Empty; - - /// - public override string CheckSuccessMessage => - LocalizedTextService.Localize("healthcheck","notificationEmailsCheckSuccessMessage", - new[] { CurrentValue ?? "<null>" }); - - /// - public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck","notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); - - /// - public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.NotificationEmailCheck; - } + /// + public override string ReadMoreLink => + Constants.HealthChecks.DocumentationLinks.Configuration.NotificationEmailCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs index dda7fb2e6e..4c3936f6cb 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs @@ -1,138 +1,127 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Data +namespace Umbraco.Cms.Core.HealthChecks.Checks.Data; + +/// +/// Health check for the integrity of the data in the database. +/// +[HealthCheck( + "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", + "Database data integrity check", + Description = "Checks for various data integrity issues in the Umbraco database.", + Group = "Data Integrity")] +public class DatabaseIntegrityCheck : HealthCheck { + private const string SSsFixMediaPaths = "fixMediaPaths"; + private const string SFixContentPaths = "fixContentPaths"; + private const string SFixMediaPathsTitle = "Fix media paths"; + private const string SFixContentPathsTitle = "Fix content paths"; + private readonly IContentService _contentService; + private readonly IMediaService _mediaService; + /// - /// Health check for the integrity of the data in the database. + /// Initializes a new instance of the class. /// - [HealthCheck( - "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", - "Database data integrity check", - Description = "Checks for various data integrity issues in the Umbraco database.", - Group = "Data Integrity")] - public class DatabaseIntegrityCheck : HealthCheck + public DatabaseIntegrityCheck( + IContentService contentService, + IMediaService mediaService) { - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; - private const string SSsFixMediaPaths = "fixMediaPaths"; - private const string SFixContentPaths = "fixContentPaths"; - private const string SFixMediaPathsTitle = "Fix media paths"; - private const string SFixContentPathsTitle = "Fix content paths"; + _contentService = contentService; + _mediaService = mediaService; + } - /// - /// Initializes a new instance of the class. - /// - public DatabaseIntegrityCheck( - IContentService contentService, - IMediaService mediaService) + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult((IEnumerable)new[] { CheckDocuments(false), CheckMedia(false) }); + + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + switch (action.Alias) { - _contentService = contentService; - _mediaService = mediaService; - } - - /// - /// Get the status for this health check - /// - public override Task> GetStatus() => - Task.FromResult((IEnumerable)new[] - { - CheckDocuments(false), - CheckMedia(false) - }); - - private HealthCheckStatus CheckMedia(bool fix) => - CheckPaths( - SSsFixMediaPaths, - SFixMediaPathsTitle, - Constants.UdiEntityType.Media, - fix, - () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); - - private HealthCheckStatus CheckDocuments(bool fix) => - CheckPaths( - SFixContentPaths, - SFixContentPathsTitle, - Constants.UdiEntityType.Document, - fix, - () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); - - private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func doCheck) - { - ContentDataIntegrityReport report = doCheck(); - - var actions = new List(); - if (!report.Ok) - { - actions.Add(new HealthCheckAction(actionAlias, Id) - { - Name = actionName - }); - } - - return new HealthCheckStatus(GetReport(report, entityType, detailedReport)) - { - ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error, - Actions = actions - }; - } - - private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed) - { - var sb = new StringBuilder(); - - 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 (IGrouping> 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) - { - switch (action.Alias) - { - case SFixContentPaths: - return CheckDocuments(true); - case SSsFixMediaPaths: - return CheckMedia(true); - default: - throw new InvalidOperationException("Action not supported"); - } + case SFixContentPaths: + return CheckDocuments(true); + case SSsFixMediaPaths: + return CheckMedia(true); + default: + throw new InvalidOperationException("Action not supported"); } } + + private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed) + { + var sb = new StringBuilder(); + + 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 (IGrouping> + 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(); + } + + private HealthCheckStatus CheckMedia(bool fix) => + CheckPaths( + SSsFixMediaPaths, + SFixMediaPathsTitle, + Constants.UdiEntityType.Media, + fix, + () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); + + private HealthCheckStatus CheckDocuments(bool fix) => + CheckPaths( + SFixContentPaths, + SFixContentPathsTitle, + Constants.UdiEntityType.Document, + fix, + () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); + + private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func doCheck) + { + ContentDataIntegrityReport report = doCheck(); + + var actions = new List(); + if (!report.Ok) + { + actions.Add(new HealthCheckAction(actionAlias, Id) { Name = actionName }); + } + + return new HealthCheckStatus(GetReport(report, entityType, detailedReport)) + { + ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error, + Actions = actions, + }; + } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs index d28c3ca8f5..ee4d9fe788 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs @@ -1,59 +1,56 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.LiveEnvironment +namespace Umbraco.Cms.Core.HealthChecks.Checks.LiveEnvironment; + +/// +/// Health check for the configuration of debug-flag. +/// +[HealthCheck( + "61214FF3-FC57-4B31-B5CF-1D095C977D6D", + "Debug Compilation Mode", + Description = "Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.", + Group = "Live Environment")] +public class CompilationDebugCheck : AbstractSettingsCheck { + private readonly IOptionsMonitor _hostingSettings; + /// - /// Health check for the configuration of debug-flag. + /// Initializes a new instance of the class. /// - [HealthCheck( - "61214FF3-FC57-4B31-B5CF-1D095C977D6D", - "Debug Compilation Mode", - Description = "Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.", - Group = "Live Environment")] - public class CompilationDebugCheck : AbstractSettingsCheck + public CompilationDebugCheck(ILocalizedTextService textService, IOptionsMonitor hostingSettings) + : base(textService) => + _hostingSettings = hostingSettings; + + /// + public override string ItemPath => Constants.Configuration.ConfigHostingDebug; + + /// + public override string ReadMoreLink => + Constants.HealthChecks.DocumentationLinks.LiveEnvironment.CompilationDebugCheck; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; + + /// + public override IEnumerable Values => new List { - private readonly IOptionsMonitor _hostingSettings; + new() { IsRecommended = true, Value = bool.FalseString.ToLower() }, + }; - /// - /// Initializes a new instance of the class. - /// - public CompilationDebugCheck(ILocalizedTextService textService, IOptionsMonitor hostingSettings) - : base(textService) => - _hostingSettings = hostingSettings; + /// + public override string CurrentValue => _hostingSettings.CurrentValue.Debug.ToString(); - /// - public override string ItemPath => Constants.Configuration.ConfigHostingDebug; + /// + public override string CheckSuccessMessage => + LocalizedTextService.Localize("healthcheck", "compilationDebugCheckSuccessMessage"); - /// - public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.LiveEnvironment.CompilationDebugCheck; - - /// - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - - /// - public override IEnumerable Values => new List - { - new AcceptableConfiguration - { - IsRecommended = true, - Value = bool.FalseString.ToLower() - } - }; - - /// - public override string CurrentValue => _hostingSettings.CurrentValue.Debug.ToString(); - - /// - public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck","compilationDebugCheckSuccessMessage"); - - /// - public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck","compilationDebugCheckErrorMessage"); - } + /// + public override string CheckErrorMessage => + LocalizedTextService.Localize("healthcheck", "compilationDebugCheckErrorMessage"); } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs index d10dc8fedd..13a45c169c 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs @@ -1,102 +1,100 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Permissions +namespace Umbraco.Cms.Core.HealthChecks.Checks.Permissions; + +/// +/// Health check for the folder and file permissions. +/// +[HealthCheck( + "53DBA282-4A79-4B67-B958-B29EC40FCC23", + "Folder & File Permissions", + Description = "Checks that the web server folder and file permissions are set correctly for Umbraco to run.", + Group = "Permissions")] +public class FolderAndFilePermissionsCheck : HealthCheck { + private readonly IFilePermissionHelper _filePermissionHelper; + private readonly ILocalizedTextService _textService; + /// - /// Health check for the folder and file permissions. + /// Initializes a new instance of the class. /// - [HealthCheck( - "53DBA282-4A79-4B67-B958-B29EC40FCC23", - "Folder & File Permissions", - Description = "Checks that the web server folder and file permissions are set correctly for Umbraco to run.", - Group = "Permissions")] - public class FolderAndFilePermissionsCheck : HealthCheck + public FolderAndFilePermissionsCheck( + ILocalizedTextService textService, + IFilePermissionHelper filePermissionHelper) { - private readonly ILocalizedTextService _textService; - private readonly IFilePermissionHelper _filePermissionHelper; + _textService = textService; + _filePermissionHelper = filePermissionHelper; + } - /// - /// Initializes a new instance of the class. - /// - public FolderAndFilePermissionsCheck( - ILocalizedTextService textService, - IFilePermissionHelper filePermissionHelper) + /// + /// Get the status for this health check + /// + public override Task> GetStatus() + { + _filePermissionHelper.RunFilePermissionTestSuite( + out Dictionary> errors); + + return Task.FromResult(errors.Select(x => new HealthCheckStatus(GetMessage(x)) { - _textService = textService; - _filePermissionHelper = filePermissionHelper; + ResultType = x.Value.Any() ? StatusResultType.Error : StatusResultType.Success, + ReadMoreLink = GetReadMoreLink(x), + Description = GetErrorDescription(x), + })); + } + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => + throw new InvalidOperationException("FolderAndFilePermissionsCheck has no executable actions"); + + private string? GetErrorDescription(KeyValuePair> status) + { + if (!status.Value.Any()) + { + return null; } - /// - /// Get the status for this health check - /// - public override Task> GetStatus() - { - _filePermissionHelper.RunFilePermissionTestSuite(out Dictionary> errors); + var sb = new StringBuilder("The following failed:"); - return Task.FromResult(errors.Select(x => new HealthCheckStatus(GetMessage(x)) - { - ResultType = x.Value.Any() ? StatusResultType.Error : StatusResultType.Success, - ReadMoreLink = GetReadMoreLink(x), - Description = GetErrorDescription(x) - })); + sb.AppendLine("
    "); + foreach (var error in status.Value) + { + sb.Append("
  • " + error + "
  • "); } - private string? GetErrorDescription(KeyValuePair> status) + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GetMessage(KeyValuePair> status) + => _textService.Localize("permissions", status.Key); + + private string? GetReadMoreLink(KeyValuePair> status) + { + if (!status.Value.Any()) { - if (!status.Value.Any()) - { + return null; + } + + switch (status.Key) + { + case FilePermissionTest.FileWriting: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWriting; + case FilePermissionTest.FolderCreation: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FolderCreation; + case FilePermissionTest.FileWritingForPackages: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWritingForPackages; + case FilePermissionTest.MediaFolderCreation: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.MediaFolderCreation; + default: return null; - } - - var sb = new StringBuilder("The following failed:"); - - sb.AppendLine("
    "); - foreach (var error in status.Value) - { - sb.Append("
  • " + error + "
  • "); - } - - sb.AppendLine("
"); - return sb.ToString(); } - - private string GetMessage(KeyValuePair> status) - => _textService.Localize("permissions", status.Key); - - private string? GetReadMoreLink(KeyValuePair> status) - { - if (!status.Value.Any()) - { - return null; - } - - switch (status.Key) - { - case FilePermissionTest.FileWriting: - return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWriting; - case FilePermissionTest.FolderCreation: - return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FolderCreation; - case FilePermissionTest.FileWritingForPackages: - return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWritingForPackages; - case FilePermissionTest.MediaFolderCreation: - return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.MediaFolderCreation; - default: return null; - } - } - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("FolderAndFilePermissionsCheck has no executable actions"); } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/ProvidedValueValidation.cs b/src/Umbraco.Core/HealthChecks/Checks/ProvidedValueValidation.cs index d99f05d738..041ace503f 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/ProvidedValueValidation.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/ProvidedValueValidation.cs @@ -1,12 +1,11 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.HealthChecks.Checks +namespace Umbraco.Cms.Core.HealthChecks.Checks; + +public enum ProvidedValueValidation { - public enum ProvidedValueValidation - { - None = 1, - Email = 2, - Regex = 3 - } + None = 1, + Email = 2, + Regex = 3, } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs index daeea79f02..5e830e1f61 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs @@ -1,150 +1,143 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Provides a base class for health checks of http header values. +/// +public abstract class BaseHttpHeaderCheck : HealthCheck { - /// - /// Provides a base class for health checks of http header values. - /// - public abstract class BaseHttpHeaderCheck : HealthCheck + private static HttpClient? httpClient; + private readonly string _header; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly string _localizedTextPrefix; + private readonly bool _metaTagOptionAvailable; + + [Obsolete("Use ctor without value.")] + protected BaseHttpHeaderCheck( + IHostingEnvironment hostingEnvironment, + ILocalizedTextService textService, + string header, + string value, + string localizedTextPrefix, + bool metaTagOptionAvailable) + : this(hostingEnvironment, textService, header, localizedTextPrefix, metaTagOptionAvailable) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILocalizedTextService _textService; - private readonly string _header; - private readonly string _localizedTextPrefix; - private readonly bool _metaTagOptionAvailable; - private static HttpClient? s_httpClient; + } - [Obsolete("Use ctor without value.")] - protected BaseHttpHeaderCheck( - IHostingEnvironment hostingEnvironment, - ILocalizedTextService textService, - string header, - string value, - string localizedTextPrefix, - bool metaTagOptionAvailable) :this(hostingEnvironment, textService, header, localizedTextPrefix, metaTagOptionAvailable) + /// + /// Initializes a new instance of the class. + /// + protected BaseHttpHeaderCheck( + IHostingEnvironment hostingEnvironment, + ILocalizedTextService textService, + string header, + string localizedTextPrefix, + bool metaTagOptionAvailable) + { + LocalizedTextService = textService ?? throw new ArgumentNullException(nameof(textService)); + _hostingEnvironment = hostingEnvironment; + _header = header; + _localizedTextPrefix = localizedTextPrefix; + _metaTagOptionAvailable = metaTagOptionAvailable; + } + + [Obsolete("Save ILocalizedTextService in a field on the super class instead of using this")] + protected ILocalizedTextService LocalizedTextService { get; } + + /// + /// Gets a link to an external read more page. + /// + protected abstract string ReadMoreLink { get; } + + private static HttpClient HttpClient => httpClient ??= new HttpClient(); + + /// + /// Get the status for this health check + /// + public override async Task> GetStatus() => + await Task.WhenAll(CheckForHeader()); + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException( + "HTTP Header action requested is either not executable or does not exist"); + + /// + /// The actual health check method. + /// + protected async Task CheckForHeader() + { + string message; + var success = false; + + // Access the site home page and check for the click-jack protection header or meta tag + var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); + + try { + using HttpResponseMessage response = await HttpClient.GetAsync(url); - } + // Check first for header + success = HasMatchingHeader(response.Headers.Select(x => x.Key)); - [Obsolete("Save ILocalizedTextService in a field on the super class instead of using this")] - protected ILocalizedTextService LocalizedTextService => _textService; - /// - /// Initializes a new instance of the class. - /// - protected BaseHttpHeaderCheck( - IHostingEnvironment hostingEnvironment, - ILocalizedTextService textService, - string header, - string localizedTextPrefix, - bool metaTagOptionAvailable) - { - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - _hostingEnvironment = hostingEnvironment; - _header = header; - _localizedTextPrefix = localizedTextPrefix; - _metaTagOptionAvailable = metaTagOptionAvailable; - } - - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(); - - /// - /// Gets a link to an external read more page. - /// - protected abstract string ReadMoreLink { get; } - - /// - /// Get the status for this health check - /// - public override async Task> GetStatus() => - await Task.WhenAll(CheckForHeader()); - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new InvalidOperationException("HTTP Header action requested is either not executable or does not exist"); - - /// - /// The actual health check method. - /// - protected async Task CheckForHeader() - { - string message; - var success = false; - - // Access the site home page and check for the click-jack protection header or meta tag - var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); - - try + // If not found, and available, check for meta-tag + if (success == false && _metaTagOptionAvailable) { - using HttpResponseMessage response = await HttpClient.GetAsync(url); - - // Check first for header - success = HasMatchingHeader(response.Headers.Select(x => x.Key)); - - // If not found, and available, check for meta-tag - if (success == false && _metaTagOptionAvailable) - { - success = await DoMetaTagsContainKeyForHeader(response); - } - - message = success - ? _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound") - : _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound"); - } - catch (Exception ex) - { - message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url?.ToString(), ex.Message }); + success = await DoMetaTagsContainKeyForHeader(response); } - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - ReadMoreLink = success ? null : ReadMoreLink - }; + message = success + ? LocalizedTextService.Localize("healthcheck", $"{_localizedTextPrefix}CheckHeaderFound") + : LocalizedTextService.Localize("healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound"); + } + catch (Exception ex) + { + message = LocalizedTextService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url, ex.Message }); } - private bool HasMatchingHeader(IEnumerable headerKeys) - => headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase); - - private async Task DoMetaTagsContainKeyForHeader(HttpResponseMessage response) - { - using (Stream stream = await response.Content.ReadAsStreamAsync()) + return + new HealthCheckStatus(message) { - if (stream == null) - { - return false; - } + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success ? null : ReadMoreLink, + }; + } - using (var reader = new StreamReader(stream)) - { - var html = reader.ReadToEnd(); - Dictionary metaTags = ParseMetaTags(html); - return HasMatchingHeader(metaTags.Keys); - } - } - } + private static Dictionary ParseMetaTags(string html) + { + var regex = new Regex(" ParseMetaTags(string html) + return regex.Matches(html) + .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value); + } + + private bool HasMatchingHeader(IEnumerable headerKeys) + => headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase); + + private async Task DoMetaTagsContainKeyForHeader(HttpResponseMessage response) + { + using (Stream stream = await response.Content.ReadAsStreamAsync()) { - var regex = new Regex("() - .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value); + using (var reader = new StreamReader(stream)) + { + var html = reader.ReadToEnd(); + Dictionary metaTags = ParseMetaTags(html); + return HasMatchingHeader(metaTags.Keys); + } } } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs index 8586989f32..2d15e49e6a 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs @@ -4,27 +4,26 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding the X-Frame-Options header. +/// +[HealthCheck( + "ED0D7E40-971E-4BE8-AB6D-8CC5D0A6A5B0", + "Click-Jacking Protection", + Description = "Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.", + Group = "Security")] +public class ClickJackingCheck : BaseHttpHeaderCheck { /// - /// Health check for the recommended production setup regarding the X-Frame-Options header. + /// Initializes a new instance of the class. /// - [HealthCheck( - "ED0D7E40-971E-4BE8-AB6D-8CC5D0A6A5B0", - "Click-Jacking Protection", - Description = "Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.", - Group = "Security")] - public class ClickJackingCheck : BaseHttpHeaderCheck + public ClickJackingCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-Frame-Options", "clickJacking", true) { - /// - /// Initializes a new instance of the class. - /// - public ClickJackingCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) - : base(hostingEnvironment, textService, "X-Frame-Options", "clickJacking", true) - { - } - - /// - protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.ClickJackingCheck; } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.ClickJackingCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs index 99729286c5..e211d7c257 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs @@ -1,94 +1,93 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding unnecessary headers. +/// +[HealthCheck( + "92ABBAA2-0586-4089-8AE2-9A843439D577", + "Excessive Headers", + Description = "Checks to see if your site is revealing information in its headers that gives away unnecessary details about the technology used to build and host it.", + Group = "Security")] +public class ExcessiveHeadersCheck : HealthCheck { + private static HttpClient? httpClient; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizedTextService _textService; + /// - /// Health check for the recommended production setup regarding unnecessary headers. + /// Initializes a new instance of the class. /// - [HealthCheck( - "92ABBAA2-0586-4089-8AE2-9A843439D577", - "Excessive Headers", - Description = "Checks to see if your site is revealing information in its headers that gives away unnecessary details about the technology used to build and host it.", - Group = "Security")] - public class ExcessiveHeadersCheck : HealthCheck + public ExcessiveHeadersCheck(ILocalizedTextService textService, IHostingEnvironment hostingEnvironment) { - private readonly ILocalizedTextService _textService; - private readonly IHostingEnvironment _hostingEnvironment; - private static HttpClient? s_httpClient; + _textService = textService; + _hostingEnvironment = hostingEnvironment; + } - /// - /// Initializes a new instance of the class. - /// - public ExcessiveHeadersCheck(ILocalizedTextService textService, IHostingEnvironment hostingEnvironment) + private static HttpClient HttpClient => httpClient ??= new HttpClient(); + + /// + /// Get the status for this health check + /// + public override async Task> GetStatus() => + await Task.WhenAll(CheckForHeaders()); + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException("ExcessiveHeadersCheck has no executable actions"); + + private async Task CheckForHeaders() + { + string message; + var success = false; + var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); + + // Access the site home page and check for the headers + var request = new HttpRequestMessage(HttpMethod.Head, url); + try { - _textService = textService; - _hostingEnvironment = hostingEnvironment; - } + using HttpResponseMessage response = await HttpClient.SendAsync(request); - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(); + IEnumerable allHeaders = response.Headers.Select(x => x.Key); + var headersToCheckFor = + new List { "Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version" }; - /// - /// Get the status for this health check - /// - public override async Task> GetStatus() => - await Task.WhenAll(CheckForHeaders()); - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new InvalidOperationException("ExcessiveHeadersCheck has no executable actions"); - - private async Task CheckForHeaders() - { - string message; - var success = false; - var url = _hostingEnvironment.ApplicationMainUrl?.GetLeftPart(UriPartial.Authority); - - // Access the site home page and check for the headers - var request = new HttpRequestMessage(HttpMethod.Head, url); - try + // Ignore if server header is present and it's set to cloudflare + if (allHeaders.InvariantContains("Server") && + response.Headers.TryGetValues("Server", out IEnumerable? serverHeaders) && + (serverHeaders.FirstOrDefault()?.InvariantEquals("cloudflare") ?? false)) { - using HttpResponseMessage response = await HttpClient.SendAsync(request); - - IEnumerable allHeaders = response.Headers.Select(x => x.Key); - var headersToCheckFor = new List {"Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version" }; - - // Ignore if server header is present and it's set to cloudflare - if (allHeaders.InvariantContains("Server") && response.Headers.TryGetValues("Server", out var serverHeaders) && (serverHeaders.FirstOrDefault()?.InvariantEquals("cloudflare") ?? false)) - { - headersToCheckFor.Remove("Server"); - } - - var headersFound = allHeaders - .Intersect(headersToCheckFor) - .ToArray(); - success = headersFound.Any() == false; - message = success - ? _textService.Localize("healthcheck","excessiveHeadersNotFound") - : _textService.Localize("healthcheck","excessiveHeadersFound", new[] { string.Join(", ", headersFound) }); - } - catch (Exception ex) - { - message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url?.ToString(), ex.Message }); + headersToCheckFor.Remove("Server"); } - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Warning, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.ExcessiveHeadersCheck, - }; + var headersFound = allHeaders + .Intersect(headersToCheckFor) + .ToArray(); + success = headersFound.Any() == false; + message = success + ? _textService.Localize("healthcheck", "excessiveHeadersNotFound") + : _textService.Localize("healthcheck", "excessiveHeadersFound", new[] { string.Join(", ", headersFound) }); } - } + catch (Exception ex) + { + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url, ex.Message }); + } + + return + new HealthCheckStatus(message) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Warning, + ReadMoreLink = success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.ExcessiveHeadersCheck, + }; + } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs index 7902f4e3f8..229999472e 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs @@ -4,34 +4,33 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding the Strict-Transport-Security header. +/// +[HealthCheck( + "E2048C48-21C5-4BE1-A80B-8062162DF124", + "Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))", + Description = "Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS).", + Group = "Security")] +public class HstsCheck : BaseHttpHeaderCheck { /// - /// Health check for the recommended production setup regarding the Strict-Transport-Security header. + /// Initializes a new instance of the class. /// - [HealthCheck( - "E2048C48-21C5-4BE1-A80B-8062162DF124", - "Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))", - Description = "Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS).", - Group = "Security")] - public class HstsCheck : BaseHttpHeaderCheck + /// + /// The check is mostly based on the instructions in the OWASP CheatSheet + /// (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.md) + /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) + /// If you want do to it perfectly, you have to submit it https://hstspreload.org/, + /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. + /// + public HstsCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "Strict-Transport-Security", "hSTS", true) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The check is mostly based on the instructions in the OWASP CheatSheet - /// (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.md) - /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) - /// If you want do to it perfectly, you have to submit it https://hstspreload.org/, - /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. - /// - public HstsCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) - : base(hostingEnvironment, textService, "Strict-Transport-Security", "hSTS", true) - { - } - - /// - protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.HstsCheck; } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.HstsCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs index 0b58ca4b40..dbff50c480 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs @@ -1,193 +1,196 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Net; -using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health checks for the recommended production setup regarding HTTPS. +/// +[HealthCheck( + "EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7", + "HTTPS Configuration", + Description = "Checks if your site is configured to work over HTTPS and if the Umbraco related configuration for that is correct.", + Group = "Security")] +public class HttpsCheck : HealthCheck { + private const int NumberOfDaysForExpiryWarning = 14; + private const string HttpPropertyKeyCertificateDaysToExpiry = "CertificateDaysToExpiry"; + + private static HttpClient? _httpClient; + private readonly IOptionsMonitor _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + + private readonly ILocalizedTextService _textService; + /// - /// Health checks for the recommended production setup regarding HTTPS. + /// Initializes a new instance of the class. /// - [HealthCheck( - "EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7", - "HTTPS Configuration", - Description = "Checks if your site is configured to work over HTTPS and if the Umbraco related configuration for that is correct.", - Group = "Security")] - public class HttpsCheck : HealthCheck + /// The text service. + /// The global settings. + /// The hosting environment. + public HttpsCheck( + ILocalizedTextService textService, + IOptionsMonitor globalSettings, + IHostingEnvironment hostingEnvironment) { - private const int NumberOfDaysForExpiryWarning = 14; - private const string HttpPropertyKeyCertificateDaysToExpiry = "CertificateDaysToExpiry"; + _textService = textService; + _globalSettings = globalSettings; + _hostingEnvironment = hostingEnvironment; + } - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; + private static HttpClient _httpClientEnsureInitialized => _httpClient ??= new HttpClient(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation, + }); - private static HttpClient? s_httpClient; + /// + public override async Task> GetStatus() => + await Task.WhenAll( + CheckIfCurrentSchemeIsHttps(), + CheckHttpsConfigurationSetting(), + CheckForValidCertificate()); - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(new HttpClientHandler() + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException( + "HttpsCheck action requested is either not executable or does not exist"); + + private static bool ServerCertificateCustomValidation( + HttpRequestMessage requestMessage, + X509Certificate2? certificate, + X509Chain? chain, + SslPolicyErrors sslErrors) + { + if (certificate is not null) { - ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation - }); - - /// - /// Initializes a new instance of the class. - /// - /// The text service. - /// The global settings. - /// The hosting environment. - public HttpsCheck( - ILocalizedTextService textService, - IOptionsMonitor globalSettings, - IHostingEnvironment hostingEnvironment) - { - _textService = textService; - _globalSettings = globalSettings; - _hostingEnvironment = hostingEnvironment; + requestMessage.Properties[HttpPropertyKeyCertificateDaysToExpiry] = + (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays); } - /// - public override async Task> GetStatus() => - await Task.WhenAll( - CheckIfCurrentSchemeIsHttps(), - CheckHttpsConfigurationSetting(), - CheckForValidCertificate()); + return sslErrors == SslPolicyErrors.None; + } - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist"); + private async Task CheckForValidCertificate() + { + string message; + StatusResultType result; - private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2? certificate, X509Chain? chain, SslPolicyErrors sslErrors) + // Attempt to access the site over HTTPS to see if it HTTPS is supported and a valid certificate has been configured + var urlBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl) { Scheme = Uri.UriSchemeHttps }; + Uri url = urlBuilder.Uri; + + var request = new HttpRequestMessage(HttpMethod.Head, url); + + try { - if (certificate is not null) + using HttpResponseMessage response = await _httpClientEnsureInitialized.SendAsync(request); + + if (response.StatusCode == HttpStatusCode.OK) { - requestMessage.Properties[HttpPropertyKeyCertificateDaysToExpiry] = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays); - } - - return sslErrors == SslPolicyErrors.None; - } - - private async Task CheckForValidCertificate() - { - string message; - StatusResultType result; - - // Attempt to access the site over HTTPS to see if it HTTPS is supported and a valid certificate has been configured - var urlBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl) - { - Scheme = Uri.UriSchemeHttps - }; - var url = urlBuilder.Uri; - - var request = new HttpRequestMessage(HttpMethod.Head, url); - - try - { - using HttpResponseMessage response = await HttpClient.SendAsync(request); - - if (response.StatusCode == HttpStatusCode.OK) + // Got a valid response, check now if the certificate is expiring within the specified amount of days + int? daysToExpiry = 0; + if (request.Properties.TryGetValue( + HttpPropertyKeyCertificateDaysToExpiry, + out var certificateDaysToExpiry)) { - // Got a valid response, check now if the certificate is expiring within the specified amount of days - int? daysToExpiry = 0; - if (request.Properties.TryGetValue(HttpPropertyKeyCertificateDaysToExpiry, out var certificateDaysToExpiry)) - { - daysToExpiry = (int?)certificateDaysToExpiry; - } - - if (daysToExpiry <= 0) - { - result = StatusResultType.Error; - message = _textService.Localize("healthcheck","httpsCheckExpiredCertificate"); - } - else if (daysToExpiry < NumberOfDaysForExpiryWarning) - { - result = StatusResultType.Warning; - message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() }); - } - else - { - result = StatusResultType.Success; - message = _textService.Localize("healthcheck","httpsCheckValidCertificate"); - } + daysToExpiry = (int?)certificateDaysToExpiry; } - else + + if (daysToExpiry <= 0) { result = StatusResultType.Error; - message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.AbsoluteUri, response.ReasonPhrase }); + message = _textService.Localize("healthcheck", "httpsCheckExpiredCertificate"); } - } - catch (Exception ex) - { - if (ex is WebException exception) + else if (daysToExpiry < NumberOfDaysForExpiryWarning) { - message = exception.Status == WebExceptionStatus.TrustFailure - ? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new[] { exception.Message }) - : _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, exception.Message }); + result = StatusResultType.Warning; + message = _textService.Localize("healthcheck", "httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() }); } else { - message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, ex.Message }); + result = StatusResultType.Success; + message = _textService.Localize("healthcheck", "httpsCheckValidCertificate"); } - - result = StatusResultType.Error; - } - - return new HealthCheckStatus(message) - { - ResultType = result, - ReadMoreLink = result == StatusResultType.Success - ? null - : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps - }; - } - - private Task CheckIfCurrentSchemeIsHttps() - { - Uri uri = _hostingEnvironment.ApplicationMainUrl; - var success = uri.Scheme == Uri.UriSchemeHttps; - - return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck","httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps - }); - } - - private Task CheckHttpsConfigurationSetting() - { - bool httpsSettingEnabled = _globalSettings.CurrentValue.UseHttps; - Uri uri = _hostingEnvironment.ApplicationMainUrl; - - string resultMessage; - StatusResultType resultType; - if (uri.Scheme != Uri.UriSchemeHttps) - { - resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationRectifyNotPossible"); - resultType = StatusResultType.Info; } else { - resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); - resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error; + result = StatusResultType.Error; + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, response.ReasonPhrase }); + } + } + catch (Exception ex) + { + if (ex is WebException exception) + { + message = exception.Status == WebExceptionStatus.TrustFailure + ? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new[] { exception.Message }) + : _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, exception.Message }); + } + else + { + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, ex.Message }); } - return Task.FromResult(new HealthCheckStatus(resultMessage) - { - ResultType = resultType, - ReadMoreLink = resultType == StatusResultType.Success - ? null - : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckHttpsConfigurationSetting - }); + result = StatusResultType.Error; } + + return new HealthCheckStatus(message) + { + ResultType = result, + ReadMoreLink = result == StatusResultType.Success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps, + }; + } + + private Task CheckIfCurrentSchemeIsHttps() + { + Uri uri = _hostingEnvironment.ApplicationMainUrl; + var success = uri.Scheme == Uri.UriSchemeHttps; + + return Task.FromResult( + new HealthCheckStatus(_textService.Localize("healthcheck", "httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps, + }); + } + + private Task CheckHttpsConfigurationSetting() + { + var httpsSettingEnabled = _globalSettings.CurrentValue.UseHttps; + Uri uri = _hostingEnvironment.ApplicationMainUrl; + + string resultMessage; + StatusResultType resultType; + if (uri.Scheme != Uri.UriSchemeHttps) + { + resultMessage = _textService.Localize("healthcheck", "httpsCheckConfigurationRectifyNotPossible"); + resultType = StatusResultType.Info; + } + else + { + resultMessage = _textService.Localize("healthcheck", "httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); + resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error; + } + + return Task.FromResult(new HealthCheckStatus(resultMessage) + { + ResultType = resultType, + ReadMoreLink = resultType == StatusResultType.Success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckHttpsConfigurationSetting, + }); } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs index 78ee2c0e12..b36201d5aa 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs @@ -4,27 +4,26 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding the X-Content-Type-Options header. +/// +[HealthCheck( + "1CF27DB3-EFC0-41D7-A1BB-EA912064E071", + "Content/MIME Sniffing Protection", + Description = "Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.", + Group = "Security")] +public class NoSniffCheck : BaseHttpHeaderCheck { /// - /// Health check for the recommended production setup regarding the X-Content-Type-Options header. + /// Initializes a new instance of the class. /// - [HealthCheck( - "1CF27DB3-EFC0-41D7-A1BB-EA912064E071", - "Content/MIME Sniffing Protection", - Description = "Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.", - Group = "Security")] - public class NoSniffCheck : BaseHttpHeaderCheck + public NoSniffCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-Content-Type-Options", "noSniff", false) { - /// - /// Initializes a new instance of the class. - /// - public NoSniffCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) - : base(hostingEnvironment, textService, "X-Content-Type-Options", "noSniff", false) - { - } - - /// - protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.NoSniffCheck; } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.NoSniffCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs index 44b10ba0e3..55406b9c0a 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs @@ -1,68 +1,69 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +[HealthCheck( + "6708CA45-E96E-40B8-A40A-0607C1CA7F28", + "Application URL Configuration", + Description = "Checks if the Umbraco application URL is configured for your site.", + Group = "Security")] +public class UmbracoApplicationUrlCheck : HealthCheck { - [HealthCheck( - "6708CA45-E96E-40B8-A40A-0607C1CA7F28", - "Application URL Configuration", - Description = "Checks if the Umbraco application URL is configured for your site.", - Group = "Security")] - public class UmbracoApplicationUrlCheck : HealthCheck + private readonly ILocalizedTextService _textService; + private readonly IOptionsMonitor _webRoutingSettings; + + public UmbracoApplicationUrlCheck( + ILocalizedTextService textService, + IOptionsMonitor webRoutingSettings) { - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _webRoutingSettings; + _textService = textService; + _webRoutingSettings = webRoutingSettings; + } - public UmbracoApplicationUrlCheck(ILocalizedTextService textService, IOptionsMonitor webRoutingSettings) + /// + /// Executes the action and returns its status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => + throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); + + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult(CheckUmbracoApplicationUrl().Yield()); + + private HealthCheckStatus CheckUmbracoApplicationUrl() + { + var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; + + string resultMessage; + StatusResultType resultType; + var success = false; + + if (url.IsNullOrWhiteSpace()) { - _textService = textService; - _webRoutingSettings = webRoutingSettings; + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); + resultType = StatusResultType.Warning; + } + else + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); + resultType = StatusResultType.Success; + success = true; } - /// - /// Executes the action and returns its status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); - - /// - /// Get the status for this health check - /// - public override Task> GetStatus() => - Task.FromResult(CheckUmbracoApplicationUrl().Yield()); - - private HealthCheckStatus CheckUmbracoApplicationUrl() + return new HealthCheckStatus(resultMessage) { - var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; - - string resultMessage; - StatusResultType resultType; - var success = false; - - if (url.IsNullOrWhiteSpace()) - { - resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); - resultType = StatusResultType.Warning; - } - else - { - resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); - resultType = StatusResultType.Success; - success = true; - } - - return new HealthCheckStatus(resultMessage) - { - ResultType = resultType, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck - }; - } + ResultType = resultType, + ReadMoreLink = success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck, + }; } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs index 570ca8002d..ca988fe45a 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs @@ -4,34 +4,33 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security; + +/// +/// Health check for the recommended production setup regarding the X-XSS-Protection header. +/// +[HealthCheck( + "F4D2B02E-28C5-4999-8463-05759FA15C3A", + "Cross-site scripting Protection (X-XSS-Protection header)", + Description = "This header enables the Cross-site scripting (XSS) filter in your browser. It checks for the presence of the X-XSS-Protection-header.", + Group = "Security")] +public class XssProtectionCheck : BaseHttpHeaderCheck { /// - /// Health check for the recommended production setup regarding the X-XSS-Protection header. + /// Initializes a new instance of the class. /// - [HealthCheck( - "F4D2B02E-28C5-4999-8463-05759FA15C3A", - "Cross-site scripting Protection (X-XSS-Protection header)", - Description = "This header enables the Cross-site scripting (XSS) filter in your browser. It checks for the presence of the X-XSS-Protection-header.", - Group = "Security")] - public class XssProtectionCheck : BaseHttpHeaderCheck + /// + /// The check is mostly based on the instructions in the OWASP CheatSheet + /// (https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet) + /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) + /// If you want do to it perfectly, you have to submit it https://hstspreload.appspot.com/, + /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. + /// + public XssProtectionCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-XSS-Protection", "xssProtection", true) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The check is mostly based on the instructions in the OWASP CheatSheet - /// (https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet) - /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) - /// If you want do to it perfectly, you have to submit it https://hstspreload.appspot.com/, - /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. - /// - public XssProtectionCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) - : base(hostingEnvironment, textService, "X-XSS-Protection", "xssProtection", true) - { - } - - /// - protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.XssProtectionCheck; } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.XssProtectionCheck; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs index 302a5829f6..6119f4c715 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs @@ -1,112 +1,106 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.IO; using System.Net.Sockets; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.Checks.Services +namespace Umbraco.Cms.Core.HealthChecks.Checks.Services; + +/// +/// Health check for the recommended setup regarding SMTP. +/// +[HealthCheck( + "1B5D221B-CE99-4193-97CB-5F3261EC73DF", + "SMTP Settings", + Description = "Checks that valid settings for sending emails are in place.", + Group = "Services")] +public class SmtpCheck : HealthCheck { + private readonly IOptionsMonitor _globalSettings; + private readonly ILocalizedTextService _textService; + /// - /// Health check for the recommended setup regarding SMTP. + /// Initializes a new instance of the class. /// - [HealthCheck( - "1B5D221B-CE99-4193-97CB-5F3261EC73DF", - "SMTP Settings", - Description = "Checks that valid settings for sending emails are in place.", - Group = "Services")] - public class SmtpCheck : HealthCheck + public SmtpCheck(ILocalizedTextService textService, IOptionsMonitor globalSettings) { - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _globalSettings; + _textService = textService; + _globalSettings = globalSettings; + } - /// - /// Initializes a new instance of the class. - /// - public SmtpCheck(ILocalizedTextService textService, IOptionsMonitor globalSettings) + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult(CheckSmtpSettings().Yield()); + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException("SmtpCheck has no executable actions"); + + private static bool CanMakeSmtpConnection(string host, int port) + { + try { - _textService = textService; - _globalSettings = globalSettings; - } - - /// - /// Get the status for this health check - /// - public override Task> GetStatus() => - Task.FromResult(CheckSmtpSettings().Yield()); - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - => throw new InvalidOperationException("SmtpCheck has no executable actions"); - - private HealthCheckStatus CheckSmtpSettings() - { - var success = false; - - SmtpSettings? smtpSettings = _globalSettings.CurrentValue.Smtp; - - string message; - if (smtpSettings == null) + using (var client = new TcpClient()) { - message = _textService.Localize("healthcheck", "smtpMailSettingsNotFound"); - } - else - { - if (string.IsNullOrEmpty(smtpSettings.Host)) + client.Connect(host, port); + using (NetworkStream stream = client.GetStream()) { - message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured"); - } - else - { - success = CanMakeSmtpConnection(smtpSettings.Host, smtpSettings.Port); - message = success - ? _textService.Localize("healthcheck", "smtpMailSettingsConnectionSuccess") - : _textService.Localize( - "healthcheck", "smtpMailSettingsConnectionFail", - new[] { smtpSettings.Host, smtpSettings.Port.ToString() }); - } - } - - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.SmtpCheck - }; - } - - private static bool CanMakeSmtpConnection(string host, int port) - { - try - { - using (var client = new TcpClient()) - { - client.Connect(host, port); - using (NetworkStream stream = client.GetStream()) + using (var writer = new StreamWriter(stream)) + using (var reader = new StreamReader(stream)) { - using (var writer = new StreamWriter(stream)) - using (var reader = new StreamReader(stream)) - { - writer.WriteLine("EHLO " + host); - writer.Flush(); - reader.ReadLine(); - return true; - } + writer.WriteLine("EHLO " + host); + writer.Flush(); + reader.ReadLine(); + return true; } } } - catch - { - return false; - } + } + catch + { + return false; } } + + private HealthCheckStatus CheckSmtpSettings() + { + var success = false; + + SmtpSettings? smtpSettings = _globalSettings.CurrentValue.Smtp; + + string message; + if (smtpSettings == null) + { + message = _textService.Localize("healthcheck", "smtpMailSettingsNotFound"); + } + else + { + if (string.IsNullOrEmpty(smtpSettings.Host)) + { + message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured"); + } + else + { + success = CanMakeSmtpConnection(smtpSettings.Host, smtpSettings.Port); + message = success + ? _textService.Localize("healthcheck", "smtpMailSettingsConnectionSuccess") + : _textService.Localize( + "healthcheck", "smtpMailSettingsConnectionFail", new[] { smtpSettings.Host, smtpSettings.Port.ToString() }); + } + } + + return + new HealthCheckStatus(message) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.SmtpCheck, + }; + } } diff --git a/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs b/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs index a5d3ae82da..564bcc59a5 100644 --- a/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs +++ b/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class ConfigurationServiceResult { - public class ConfigurationServiceResult - { - public bool Success { get; set; } - public string? Result { get; set; } - } + public bool Success { get; set; } + + public string? Result { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheck.cs b/src/Umbraco.Core/HealthChecks/HealthCheck.cs index 59d6f912fa..06a1bd27f3 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheck.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheck.cs @@ -1,60 +1,57 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -using System.Threading.Tasks; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks -{ - /// - /// Provides a base class for health checks, filling in the healthcheck metadata on construction - /// - [DataContract(Name = "healthCheck", Namespace = "")] - public abstract class HealthCheck : IDiscoverable - { - protected HealthCheck() - { - var thisType = GetType(); - var meta = thisType.GetCustomAttribute(false); - if (meta == null) - { - throw new InvalidOperationException($"The health check {thisType} requires a {typeof(HealthCheckAttribute)}"); - } +namespace Umbraco.Cms.Core.HealthChecks; - Name = meta.Name; - Description = meta.Description; - Group = meta.Group; - Id = meta.Id; +/// +/// Provides a base class for health checks, filling in the healthcheck metadata on construction +/// +[DataContract(Name = "healthCheck", Namespace = "")] +public abstract class HealthCheck : IDiscoverable +{ + protected HealthCheck() + { + Type thisType = GetType(); + HealthCheckAttribute? meta = thisType.GetCustomAttribute(false); + if (meta == null) + { + throw new InvalidOperationException( + $"The health check {thisType} requires a {typeof(HealthCheckAttribute)}"); } - [DataMember(Name = "id")] - public Guid Id { get; private set; } - - [DataMember(Name = "name")] - public string Name { get; private set; } - - [DataMember(Name = "description")] - public string? Description { get; private set; } - - [DataMember(Name = "group")] - public string? Group { get; private set; } - - /// - /// Get the status for this health check - /// - /// - /// - /// If there are possible actions to take to rectify this check, this method must be overridden by a sub class - /// in order to explicitly provide those actions. - /// - public abstract Task> GetStatus(); - - /// - /// Executes the action and returns it's status - /// - /// - /// - public abstract HealthCheckStatus ExecuteAction(HealthCheckAction action); + Name = meta.Name; + Description = meta.Description; + Group = meta.Group; + Id = meta.Id; } + + [DataMember(Name = "id")] + public Guid Id { get; private set; } + + [DataMember(Name = "name")] + public string Name { get; private set; } + + [DataMember(Name = "description")] + public string? Description { get; private set; } + + [DataMember(Name = "group")] + public string? Group { get; private set; } + + /// + /// Get the status for this health check + /// + /// + /// + /// If there are possible actions to take to rectify this check, this method must be overridden by a sub class + /// in order to explicitly provide those actions. + /// + public abstract Task> GetStatus(); + + /// + /// Executes the action and returns it's status + /// + /// + /// + public abstract HealthCheckStatus ExecuteAction(HealthCheckAction action); } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs b/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs index 06bc05f44a..7593a54cc2 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs @@ -1,89 +1,89 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +[DataContract(Name = "healthCheckAction", Namespace = "")] +public class HealthCheckAction { - [DataContract(Name = "healthCheckAction", Namespace = "")] - public class HealthCheckAction + /// + /// The name of the action - this is used to name the fix button + /// + [DataMember(Name = "name")] + private string? _name; + + /// + /// Empty ctor used for serialization + /// + public HealthCheckAction() { - /// - /// Empty ctor used for serialization - /// - public HealthCheckAction() { } - - /// - /// Default ctor - /// - /// - /// - public HealthCheckAction(string alias, Guid healthCheckId) - { - Alias = alias; - HealthCheckId = healthCheckId; - } - - /// - /// The alias of the action - this is used by the Health Check instance to execute the action - /// - [DataMember(Name = "alias")] - public string? Alias { get; set; } - - /// - /// The Id of the Health Check instance - /// - /// - /// This is used to find the Health Check instance to execute this action - /// - [DataMember(Name = "healthCheckId")] - public Guid? HealthCheckId { get; set; } - - /// - /// This could be used if the status has a custom view that specifies some parameters to be sent to the server - /// when an action needs to be executed - /// - [DataMember(Name = "actionParameters")] - public Dictionary? ActionParameters { get; set; } - - /// - /// The name of the action - this is used to name the fix button - /// - [DataMember(Name = "name")] - private string? _name; - public string? Name - { - get { return _name; } - set { _name = value; } - } - - /// - /// The description of the action - this is used to give a description before executing the action - /// - [DataMember(Name = "description")] - public string? Description { get; set; } - - /// - /// Indicates if a value is required to rectify the issue - /// - [DataMember(Name = "valueRequired")] - public bool ValueRequired { get; set; } - - /// - /// Indicates if a value required, how it is validated - /// - [DataMember(Name = "providedValueValidation")] - public string? ProvidedValueValidation { get; set; } - - /// - /// Indicates if a value required, and is validated by a regex, what the regex to use is - /// - [DataMember(Name = "providedValueValidationRegex")] - public string? ProvidedValueValidationRegex { get; set; } - - /// - /// Provides a value to rectify the issue - /// - [DataMember(Name = "providedValue")] - public string? ProvidedValue { get; set; } } + + /// + /// Default ctor + /// + /// + /// + public HealthCheckAction(string alias, Guid healthCheckId) + { + Alias = alias; + HealthCheckId = healthCheckId; + } + + /// + /// The alias of the action - this is used by the Health Check instance to execute the action + /// + [DataMember(Name = "alias")] + public string? Alias { get; set; } + + /// + /// The Id of the Health Check instance + /// + /// + /// This is used to find the Health Check instance to execute this action + /// + [DataMember(Name = "healthCheckId")] + public Guid? HealthCheckId { get; set; } + + /// + /// This could be used if the status has a custom view that specifies some parameters to be sent to the server + /// when an action needs to be executed + /// + [DataMember(Name = "actionParameters")] + public Dictionary? ActionParameters { get; set; } + + public string? Name + { + get => _name; + set => _name = value; + } + + /// + /// The description of the action - this is used to give a description before executing the action + /// + [DataMember(Name = "description")] + public string? Description { get; set; } + + /// + /// Indicates if a value is required to rectify the issue + /// + [DataMember(Name = "valueRequired")] + public bool ValueRequired { get; set; } + + /// + /// Indicates if a value required, how it is validated + /// + [DataMember(Name = "providedValueValidation")] + public string? ProvidedValueValidation { get; set; } + + /// + /// Indicates if a value required, and is validated by a regex, what the regex to use is + /// + [DataMember(Name = "providedValueValidationRegex")] + public string? ProvidedValueValidationRegex { get; set; } + + /// + /// Provides a value to rectify the issue + /// + [DataMember(Name = "providedValue")] + public string? ProvidedValue { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs b/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs index 0fa6647971..718a689caf 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs @@ -1,26 +1,24 @@ -using System; +namespace Umbraco.Cms.Core.HealthChecks; -namespace Umbraco.Cms.Core.HealthChecks +/// +/// Metadata attribute for Health checks +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class HealthCheckAttribute : Attribute { - /// - /// Metadata attribute for Health checks - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public sealed class HealthCheckAttribute : Attribute + public HealthCheckAttribute(string id, string name) { - public HealthCheckAttribute(string id, string name) - { - Id = new Guid(id); - Name = name; - } - - public string Name { get; private set; } - public string? Description { get; set; } - - public string? Group { get; set; } - - public Guid Id { get; private set; } - - // TODO: Do we need more metadata? + Id = new Guid(id); + Name = name; } + + public string Name { get; } + + public string? Description { get; set; } + + public string? Group { get; set; } + + public Guid Id { get; } + + // TODO: Do we need more metadata? } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs b/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs index bcbee9036b..c2c47c1948 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class HealthCheckCollection : BuilderCollectionBase { - public class HealthCheckCollection : BuilderCollectionBase + public HealthCheckCollection(Func> items) + : base(items) { - public HealthCheckCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs b/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs index aee97647d9..ae67c192f5 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.HealthChecks -{ - [DataContract(Name = "healthCheckGroup", Namespace = "")] - public class HealthCheckGroup - { - [DataMember(Name = "name")] - public string? Name { get; set; } +namespace Umbraco.Cms.Core.HealthChecks; - [DataMember(Name = "checks")] - public List? Checks { get; set; } - } +[DataContract(Name = "healthCheckGroup", Namespace = "")] +public class HealthCheckGroup +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "checks")] + public List? Checks { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs index 6dd6df4b8b..128e6dabbe 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs @@ -1,18 +1,12 @@ -using System; +namespace Umbraco.Cms.Core.HealthChecks; -namespace Umbraco.Cms.Core.HealthChecks +/// +/// Metadata attribute for health check notification methods +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class HealthCheckNotificationMethodAttribute : Attribute { - /// - /// Metadata attribute for health check notification methods - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public sealed class HealthCheckNotificationMethodAttribute : Attribute - { - public HealthCheckNotificationMethodAttribute(string alias) - { - Alias = alias; - } + public HealthCheckNotificationMethodAttribute(string alias) => Alias = alias; - public string Alias { get; } - } + public string Alias { get; } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs index af964857d8..1d681690db 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs @@ -1,14 +1,12 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class HealthCheckNotificationMethodCollection : BuilderCollectionBase { - public class HealthCheckNotificationMethodCollection : BuilderCollectionBase + public HealthCheckNotificationMethodCollection(Func> items) + : base(items) { - public HealthCheckNotificationMethodCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs index 48f2629e2a..375ddc7e2e 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs @@ -1,10 +1,11 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class HealthCheckNotificationMethodCollectionBuilder : LazyCollectionBuilderBase< + HealthCheckNotificationMethodCollectionBuilder, HealthCheckNotificationMethodCollection, + IHealthCheckNotificationMethod> { - public class HealthCheckNotificationMethodCollectionBuilder : LazyCollectionBuilderBase - { - protected override HealthCheckNotificationMethodCollectionBuilder This => this; - } + protected override HealthCheckNotificationMethodCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs index cba8ab5c0f..1e7ea90532 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs @@ -1,9 +1,7 @@ -namespace Umbraco.Cms.Core.HealthChecks -{ - public enum HealthCheckNotificationVerbosity - { +namespace Umbraco.Cms.Core.HealthChecks; - Summary, - Detailed - } +public enum HealthCheckNotificationVerbosity +{ + Summary, + Detailed, } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs b/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs index bde90627c7..afeb8ba9fa 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs @@ -1,166 +1,168 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public class HealthCheckResults { - public class HealthCheckResults + public readonly bool AllChecksSuccessful; + + private HealthCheckResults(Dictionary> results, bool allChecksSuccessful) { - private readonly Dictionary> _results; - public readonly bool AllChecksSuccessful; + ResultsAsDictionary = results; + AllChecksSuccessful = allChecksSuccessful; + } - private static ILogger Logger => StaticApplicationLogging.Logger; // TODO: inject + internal Dictionary> ResultsAsDictionary { get; } - private HealthCheckResults(Dictionary> results, bool allChecksSuccessful) - { - _results = results; - AllChecksSuccessful = allChecksSuccessful; - } + private static ILogger Logger => StaticApplicationLogging.Logger; // TODO: inject - public static async Task Create(IEnumerable checks) - { - var results = await checks.ToDictionaryAsync( - t => t.Name, - async t => { - try - { - return await t.GetStatus(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error running scheduled health check: {HealthCheckName}", t.Name); - var message = $"Health check failed with exception: {ex.Message}. See logs for details."; - return new List - { - new HealthCheckStatus(message) - { - ResultType = StatusResultType.Error - } - }; - } - }); - - // find out if all checks pass or not - var allChecksSuccessful = true; - foreach (var result in results) + public static async Task Create(IEnumerable checks) + { + Dictionary> results = await checks.ToDictionaryAsync( + t => t.Name, + async t => { - var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success || x.ResultType == StatusResultType.Info || x.ResultType == StatusResultType.Warning); - if (checkIsSuccess == false) + try { - allChecksSuccessful = false; - break; + return await t.GetStatus(); } - } + catch (Exception ex) + { + Logger.LogError(ex, "Error running scheduled health check: {HealthCheckName}", t.Name); + var message = $"Health check failed with exception: {ex.Message}. See logs for details."; + return new List { new(message) { ResultType = StatusResultType.Error } }; + } + }); - return new HealthCheckResults(results, allChecksSuccessful); - } - - public void LogResults() + // find out if all checks pass or not + var allChecksSuccessful = true; + foreach (KeyValuePair> result in results) { - Logger.LogInformation("Scheduled health check results:"); - foreach (var result in _results) + var checkIsSuccess = result.Value.All(x => + x.ResultType == StatusResultType.Success || x.ResultType == StatusResultType.Info || + x.ResultType == StatusResultType.Warning); + if (checkIsSuccess == false) { - var checkName = result.Key; - var checkResults = result.Value; - var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); - if (checkIsSuccess) - { - Logger.LogInformation("Checks for '{HealthCheckName}' all completed successfully.", checkName); - } - else - { - Logger.LogWarning("Checks for '{HealthCheckName}' completed with errors.", checkName); - } - - foreach (var checkResult in checkResults) - { - Logger.LogInformation("Result for {HealthCheckName}: {HealthCheckResult}, Message: '{HealthCheckMessage}'", checkName, checkResult.ResultType, checkResult.Message); - } + allChecksSuccessful = false; + break; } } - public string ResultsAsMarkDown(HealthCheckNotificationVerbosity verbosity) + return new HealthCheckResults(results, allChecksSuccessful); + } + + public void LogResults() + { + Logger.LogInformation("Scheduled health check results:"); + foreach (KeyValuePair> result in ResultsAsDictionary) { - var newItem = "- "; - - var sb = new StringBuilder(); - - foreach (var result in _results) + var checkName = result.Key; + IEnumerable checkResults = result.Value; + var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); + if (checkIsSuccess) { - var checkName = result.Key; - var checkResults = result.Value; - var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); - - // add a new line if not the first check - if (result.Equals(_results.First()) == false) - { - sb.Append(Environment.NewLine); - } - - if (checkIsSuccess) - { - sb.AppendFormat("{0}Checks for '{1}' all completed successfully.{2}", newItem, checkName, Environment.NewLine); - } - else - { - sb.AppendFormat("{0}Checks for '{1}' completed with errors.{2}", newItem, checkName, Environment.NewLine); - } - - foreach (var checkResult in checkResults) - { - sb.AppendFormat("\t{0}Result: '{1}'", newItem, checkResult.ResultType); - - // With summary logging, only record details of warnings or errors - if (checkResult.ResultType != StatusResultType.Success || verbosity == HealthCheckNotificationVerbosity.Detailed) - { - sb.AppendFormat(", Message: '{0}'", SimpleHtmlToMarkDown(checkResult.Message)); - } - - sb.AppendLine(Environment.NewLine); - } + Logger.LogInformation("Checks for '{HealthCheckName}' all completed successfully.", checkName); + } + else + { + Logger.LogWarning("Checks for '{HealthCheckName}' completed with errors.", checkName); } - return sb.ToString(); - } - - - internal Dictionary> ResultsAsDictionary => _results; - - private string SimpleHtmlToMarkDown(string html) - { - return html.Replace("", "**") - .Replace("", "**") - .Replace("", "*") - .Replace("", "*"); - } - - public Dictionary>? GetResultsForStatus(StatusResultType resultType) - { - switch (resultType) + foreach (HealthCheckStatus checkResult in checkResults) { - case StatusResultType.Success: - // a check is considered a success status if all checks are successful or info - var successResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Success) && x.Value.All(y => y.ResultType == StatusResultType.Success || y.ResultType == StatusResultType.Info)); - return successResults.ToDictionary(x => x.Key, x => x.Value); - case StatusResultType.Warning: - // a check is considered warn status if one check is warn and all others are success or info - var warnResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Warning) && x.Value.All(y => y.ResultType == StatusResultType.Warning || y.ResultType == StatusResultType.Success || y.ResultType == StatusResultType.Info)); - return warnResults.ToDictionary(x => x.Key, x => x.Value); - case StatusResultType.Error: - // a check is considered error status if any check is error - var errorResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Error)); - return errorResults.ToDictionary(x => x.Key, x => x.Value); - case StatusResultType.Info: - // a check is considered info status if all checks are info - var infoResults = _results.Where(x => x.Value.All(y => y.ResultType == StatusResultType.Info)); - return infoResults.ToDictionary(x => x.Key, x => x.Value); + Logger.LogInformation( + "Result for {HealthCheckName}: {HealthCheckResult}, Message: '{HealthCheckMessage}'", + checkName, + checkResult.ResultType, + checkResult.Message); } - - return null; } } + + public string ResultsAsMarkDown(HealthCheckNotificationVerbosity verbosity) + { + var newItem = "- "; + + var sb = new StringBuilder(); + + foreach (KeyValuePair> result in ResultsAsDictionary) + { + var checkName = result.Key; + IEnumerable checkResults = result.Value; + var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success); + + // add a new line if not the first check + if (result.Equals(ResultsAsDictionary.First()) == false) + { + sb.Append(Environment.NewLine); + } + + if (checkIsSuccess) + { + sb.AppendFormat("{0}Checks for '{1}' all completed successfully.{2}", newItem, checkName, Environment.NewLine); + } + else + { + sb.AppendFormat("{0}Checks for '{1}' completed with errors.{2}", newItem, checkName, Environment.NewLine); + } + + foreach (HealthCheckStatus checkResult in checkResults) + { + sb.AppendFormat("\t{0}Result: '{1}'", newItem, checkResult.ResultType); + + // With summary logging, only record details of warnings or errors + if (checkResult.ResultType != StatusResultType.Success || + verbosity == HealthCheckNotificationVerbosity.Detailed) + { + sb.AppendFormat(", Message: '{0}'", SimpleHtmlToMarkDown(checkResult.Message)); + } + + sb.AppendLine(Environment.NewLine); + } + } + + return sb.ToString(); + } + + public Dictionary>? GetResultsForStatus(StatusResultType resultType) + { + switch (resultType) + { + case StatusResultType.Success: + // a check is considered a success status if all checks are successful or info + IEnumerable>> successResults = + ResultsAsDictionary.Where(x => + x.Value.Any(y => y.ResultType == StatusResultType.Success) && x.Value.All(y => + y.ResultType == StatusResultType.Success || y.ResultType == StatusResultType.Info)); + return successResults.ToDictionary(x => x.Key, x => x.Value); + case StatusResultType.Warning: + // a check is considered warn status if one check is warn and all others are success or info + IEnumerable>> warnResults = + ResultsAsDictionary.Where(x => + x.Value.Any(y => y.ResultType == StatusResultType.Warning) && x.Value.All(y => + y.ResultType == StatusResultType.Warning || y.ResultType == StatusResultType.Success || + y.ResultType == StatusResultType.Info)); + return warnResults.ToDictionary(x => x.Key, x => x.Value); + case StatusResultType.Error: + // a check is considered error status if any check is error + IEnumerable>> errorResults = + ResultsAsDictionary.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Error)); + return errorResults.ToDictionary(x => x.Key, x => x.Value); + case StatusResultType.Info: + // a check is considered info status if all checks are info + IEnumerable>> infoResults = + ResultsAsDictionary.Where(x => x.Value.All(y => y.ResultType == StatusResultType.Info)); + return infoResults.ToDictionary(x => x.Key, x => x.Value); + } + + return null; + } + + private string SimpleHtmlToMarkDown(string html) => + html.Replace("", "**") + .Replace("", "**") + .Replace("", "*") + .Replace("", "*"); } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs b/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs index 49428fe899..7f04e51541 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs @@ -1,58 +1,55 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +/// +/// The status returned for a health check when it performs it check +/// TODO: This model will be used in the WebApi result so needs attributes for JSON usage +/// +[DataContract(Name = "healthCheckStatus", Namespace = "")] +public class HealthCheckStatus { - /// - /// The status returned for a health check when it performs it check - /// TODO: This model will be used in the WebApi result so needs attributes for JSON usage - /// - [DataContract(Name = "healthCheckStatus", Namespace = "")] - public class HealthCheckStatus + public HealthCheckStatus(string message) { - public HealthCheckStatus(string message) - { - Message = message; - Actions = Enumerable.Empty(); - } - - /// - /// The status message - /// - [DataMember(Name = "message")] - public string Message { get; private set; } - - /// - /// The status description if one is necessary - /// - [DataMember(Name = "description")] - public string? Description { get; set; } - - /// - /// This is optional but would allow a developer to specify a path to an angular HTML view - /// in order to either show more advanced information and/or to provide input for the admin - /// to configure how an action is executed - /// - [DataMember(Name = "view")] - public string? View { get; set; } - - /// - /// The status type - /// - [DataMember(Name = "resultType")] - public StatusResultType ResultType { get; set; } - - /// - /// The potential actions to take (in any) - /// - [DataMember(Name = "actions")] - public IEnumerable Actions { get; set; } - - /// - /// This is optional but would allow a developer to specify a link that is shown as a "read more" button. - /// - [DataMember(Name = "readMoreLink")] - public string? ReadMoreLink { get; set; } + Message = message; + Actions = Enumerable.Empty(); } + + /// + /// The status message + /// + [DataMember(Name = "message")] + public string Message { get; private set; } + + /// + /// The status description if one is necessary + /// + [DataMember(Name = "description")] + public string? Description { get; set; } + + /// + /// This is optional but would allow a developer to specify a path to an angular HTML view + /// in order to either show more advanced information and/or to provide input for the admin + /// to configure how an action is executed + /// + [DataMember(Name = "view")] + public string? View { get; set; } + + /// + /// The status type + /// + [DataMember(Name = "resultType")] + public StatusResultType ResultType { get; set; } + + /// + /// The potential actions to take (in any) + /// + [DataMember(Name = "actions")] + public IEnumerable Actions { get; set; } + + /// + /// This is optional but would allow a developer to specify a link that is shown as a "read more" button. + /// + [DataMember(Name = "readMoreLink")] + public string? ReadMoreLink { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs b/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs index 495fc42cf1..1c026248c8 100644 --- a/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs +++ b/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs @@ -1,14 +1,15 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.HealthChecks -{ - public class HealthCheckCollectionBuilder : LazyCollectionBuilderBase - { - protected override HealthCheckCollectionBuilder This => this; +namespace Umbraco.Cms.Core.HealthChecks; - // note: in v7 they were per-request, not sure why? - // the collection is injected into the controller & there's only 1 controller per request anyways - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; // transient! - } +public class + HealthCheckCollectionBuilder : LazyCollectionBuilderBase +{ + protected override HealthCheckCollectionBuilder This => this; + + // note: in v7 they were per-request, not sure why? + // the collection is injected into the controller & there's only 1 controller per request anyways + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; // transient! } diff --git a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs index 94867d8882..022531c1ec 100644 --- a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; @@ -8,89 +6,91 @@ using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.HealthChecks.NotificationMethods +namespace Umbraco.Cms.Core.HealthChecks.NotificationMethods; + +[HealthCheckNotificationMethod("email")] +public class EmailNotificationMethod : NotificationMethodBase { - [HealthCheckNotificationMethod("email")] - public class EmailNotificationMethod : NotificationMethodBase + private readonly IEmailSender? _emailSender; + private readonly IHostingEnvironment? _hostingEnvironment; + private readonly IMarkdownToHtmlConverter? _markdownToHtmlConverter; + private readonly ILocalizedTextService? _textService; + private ContentSettings? _contentSettings; + + public EmailNotificationMethod( + ILocalizedTextService textService, + IHostingEnvironment hostingEnvironment, + IEmailSender emailSender, + IOptionsMonitor healthChecksSettings, + IOptionsMonitor contentSettings, + IMarkdownToHtmlConverter markdownToHtmlConverter) + : base(healthChecksSettings) { - private readonly ILocalizedTextService? _textService; - private readonly IHostingEnvironment? _hostingEnvironment; - private readonly IEmailSender? _emailSender; - private readonly IMarkdownToHtmlConverter? _markdownToHtmlConverter; - private ContentSettings? _contentSettings; - - public EmailNotificationMethod( - ILocalizedTextService textService, - IHostingEnvironment hostingEnvironment, - IEmailSender emailSender, - IOptionsMonitor healthChecksSettings, - IOptionsMonitor contentSettings, - IMarkdownToHtmlConverter markdownToHtmlConverter) - : base(healthChecksSettings) + var recipientEmail = Settings?["RecipientEmail"]; + if (string.IsNullOrWhiteSpace(recipientEmail)) { - var recipientEmail = Settings?["RecipientEmail"]; - if (string.IsNullOrWhiteSpace(recipientEmail)) - { - Enabled = false; - return; - } - - RecipientEmail = recipientEmail; - - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - _hostingEnvironment = hostingEnvironment; - _emailSender = emailSender; - _markdownToHtmlConverter = markdownToHtmlConverter; - _contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings)); - - contentSettings.OnChange(x => _contentSettings = x); + Enabled = false; + return; } - public string? RecipientEmail { get; } + RecipientEmail = recipientEmail; - public override async Task SendAsync(HealthCheckResults results) + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _hostingEnvironment = hostingEnvironment; + _emailSender = emailSender; + _markdownToHtmlConverter = markdownToHtmlConverter; + _contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings)); + + contentSettings.OnChange(x => _contentSettings = x); + } + + public string? RecipientEmail { get; } + + public override async Task SendAsync(HealthCheckResults results) + { + if (ShouldSend(results) == false) { - if (ShouldSend(results) == false) - { - return; - } + return; + } - if (string.IsNullOrEmpty(RecipientEmail)) - { - return; - } + if (string.IsNullOrEmpty(RecipientEmail)) + { + return; + } - var message = _textService?.Localize("healthcheck","scheduledHealthCheckEmailBody", new[] + var message = _textService?.Localize( + "healthcheck", + "scheduledHealthCheckEmailBody", + new[] { - DateTime.Now.ToShortDateString(), - DateTime.Now.ToShortTimeString(), - _markdownToHtmlConverter?.ToHtml(results, Verbosity) + DateTime.Now.ToShortDateString(), DateTime.Now.ToShortTimeString(), + _markdownToHtmlConverter?.ToHtml(results, Verbosity), }); - // Include the umbraco Application URL host in the message subject so that - // you can identify the site that these results are for. - var host = _hostingEnvironment?.ApplicationMainUrl?.ToString(); + // Include the umbraco Application URL host in the message subject so that + // you can identify the site that these results are for. + var host = _hostingEnvironment?.ApplicationMainUrl?.ToString(); - var subject = _textService?.Localize("healthcheck","scheduledHealthCheckEmailSubject", new[] { host }); + var subject = _textService?.Localize("healthcheck", "scheduledHealthCheckEmailSubject", new[] { host }); - - var mailMessage = CreateMailMessage(subject, message); - Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck); - if (task is not null) - { - await task; - } - } - - private EmailMessage CreateMailMessage(string? subject, string? message) + EmailMessage mailMessage = CreateMailMessage(subject, message); + Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck); + if (task is not null) { - var to = _contentSettings?.Notifications.Email; - - if (string.IsNullOrWhiteSpace(subject)) - subject = "Umbraco Health Check Status"; - - var isBodyHtml = message.IsNullOrWhiteSpace() == false && message!.Contains("<") && message.Contains(" healthCheckSettings) { - protected NotificationMethodBase(IOptionsMonitor healthCheckSettings) + Type type = GetType(); + HealthCheckNotificationMethodAttribute? attribute = type.GetCustomAttribute(); + if (attribute == null) { - var type = GetType(); - var attribute = type.GetCustomAttribute(); - if (attribute == null) - { - Enabled = false; - return; - } - - var notificationMethods = healthCheckSettings.CurrentValue.Notification.NotificationMethods; - if (!notificationMethods.TryGetValue(attribute.Alias, out var notificationMethod)) - { - Enabled = false; - return; - } - - Enabled = notificationMethod.Enabled; - FailureOnly = notificationMethod.FailureOnly; - Verbosity = notificationMethod.Verbosity; - Settings = notificationMethod.Settings; + Enabled = false; + return; } - public bool Enabled { get; protected set; } - - public bool FailureOnly { get; protected set; } - - public HealthCheckNotificationVerbosity Verbosity { get; protected set; } - - public IDictionary? Settings { get; } - - protected bool ShouldSend(HealthCheckResults results) + IDictionary notificationMethods = + healthCheckSettings.CurrentValue.Notification.NotificationMethods; + if (!notificationMethods.TryGetValue( + attribute.Alias, out HealthChecksNotificationMethodSettings? notificationMethod)) { - return Enabled && (!FailureOnly || !results.AllChecksSuccessful); + Enabled = false; + return; } - public abstract Task SendAsync(HealthCheckResults results); + Enabled = notificationMethod.Enabled; + FailureOnly = notificationMethod.FailureOnly; + Verbosity = notificationMethod.Verbosity; + Settings = notificationMethod.Settings; } + + public bool FailureOnly { get; protected set; } + + public HealthCheckNotificationVerbosity Verbosity { get; protected set; } + + public IDictionary? Settings { get; } + + public bool Enabled { get; protected set; } + + public abstract Task SendAsync(HealthCheckResults results); + + protected bool ShouldSend(HealthCheckResults results) => Enabled && (!FailureOnly || !results.AllChecksSuccessful); } diff --git a/src/Umbraco.Core/HealthChecks/StatusResultType.cs b/src/Umbraco.Core/HealthChecks/StatusResultType.cs index b06322a267..0516fc3544 100644 --- a/src/Umbraco.Core/HealthChecks/StatusResultType.cs +++ b/src/Umbraco.Core/HealthChecks/StatusResultType.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public enum StatusResultType { - public enum StatusResultType - { - Success, - Warning, - Error, - Info - } + Success, + Warning, + Error, + Info, } diff --git a/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs b/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs index 254a53c6fb..9269f905f4 100644 --- a/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs +++ b/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.HealthChecks +namespace Umbraco.Cms.Core.HealthChecks; + +public enum ValueComparisonType { - public enum ValueComparisonType - { - ShouldEqual, - ShouldNotEqual, - } + ShouldEqual, + ShouldNotEqual, } diff --git a/src/Umbraco.Core/HexEncoder.cs b/src/Umbraco.Core/HexEncoder.cs index ce4df997ab..b95376646b 100644 --- a/src/Umbraco.Core/HexEncoder.cs +++ b/src/Umbraco.Core/HexEncoder.cs @@ -1,84 +1,85 @@ -using System.Linq; using System.Runtime.CompilerServices; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Provides methods for encoding byte arrays into hexadecimal strings. +/// +public static class HexEncoder { - /// - /// Provides methods for encoding byte arrays into hexadecimal strings. - /// - public static class HexEncoder + // LUT's that provide the hexadecimal representation of each possible byte value. + private static readonly char[] HexLutBase = { - // LUT's that provide the hexadecimal representation of each possible byte value. - private static readonly char[] HexLutBase = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + }; - // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16 - private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray(); + // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16 + private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray(); - // The base LUT repeated 16x. - private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray(); + // The base LUT repeated 16x. + private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray(); - /// - /// Converts a to a hexadecimal formatted padded to 2 digits. - /// - /// The bytes. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Encode(byte[] bytes) + /// + /// Converts a to a hexadecimal formatted padded to 2 digits. + /// + /// The bytes. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Encode(byte[] bytes) + { + var length = bytes.Length; + var chars = new char[length * 2]; + + var index = 0; + for (var i = 0; i < length; i++) { - var length = bytes.Length; - var chars = new char[length * 2]; - - var index = 0; - for (var i = 0; i < length; i++) - { - var byteIndex = bytes[i]; - chars[index++] = HexLutHi[byteIndex]; - chars[index++] = HexLutLo[byteIndex]; - } - - return new string(chars, 0, chars.Length); + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; } - /// - /// Converts a to a hexadecimal formatted padded to 2 digits - /// and split into blocks with the given char separator. - /// - /// The bytes. - /// The separator. - /// The block size. - /// The block count. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount) + return new string(chars, 0, chars.Length); + } + + /// + /// Converts a to a hexadecimal formatted padded to 2 digits + /// and split into blocks with the given char separator. + /// + /// The bytes. + /// The separator. + /// The block size. + /// The block count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount) + { + var length = bytes.Length; + var chars = new char[(length * 2) + blockCount]; + var count = 0; + var size = 0; + var index = 0; + + for (var i = 0; i < length; i++) { - var length = bytes.Length; - var chars = new char[(length * 2) + blockCount]; - var count = 0; - var size = 0; - var index = 0; + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; - for (var i = 0; i < length; i++) + if (count == blockCount) { - var byteIndex = bytes[i]; - chars[index++] = HexLutHi[byteIndex]; - chars[index++] = HexLutLo[byteIndex]; - - if (count == blockCount) - { - continue; - } - - if (++size < blockSize) - { - continue; - } - - chars[index++] = separator; - size = 0; - count++; + continue; } - return new string(chars, 0, chars.Length); + if (++size < blockSize) + { + continue; + } + + chars[index++] = separator; + size = 0; + count++; } + + return new string(chars, 0, chars.Length); } } diff --git a/src/Umbraco.Core/Hosting/IApplicationShutdownRegistry.cs b/src/Umbraco.Core/Hosting/IApplicationShutdownRegistry.cs index 2d1336ab90..84b275714b 100644 --- a/src/Umbraco.Core/Hosting/IApplicationShutdownRegistry.cs +++ b/src/Umbraco.Core/Hosting/IApplicationShutdownRegistry.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.Hosting +namespace Umbraco.Cms.Core.Hosting; + +public interface IApplicationShutdownRegistry { - public interface IApplicationShutdownRegistry - { - void RegisterObject(IRegisteredObject registeredObject); - void UnregisterObject(IRegisteredObject registeredObject); - } + void RegisterObject(IRegisteredObject registeredObject); + + void UnregisterObject(IRegisteredObject registeredObject); } diff --git a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs index c2c7cfe792..b8960048f6 100644 --- a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs +++ b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs @@ -1,101 +1,108 @@ -using System; +namespace Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.Hosting +public interface IHostingEnvironment { - public interface IHostingEnvironment - { - string SiteName { get; } + string SiteName { get; } - /// - /// The unique application ID for this Umbraco website. - /// - /// - /// - /// The returned value will be the same consistent value for an Umbraco website on a specific server and will the same - /// between restarts of that Umbraco website/application on that specific server. - /// - /// - /// The value of this does not distinguish between unique workers/servers for this Umbraco application. - /// Usage of this must take into account that the same may be returned for the same - /// Umbraco website hosted on different servers.
- /// Similarly the usage of this must take into account that a different - /// may be returned for the same Umbraco website hosted on different servers. - ///
- /// - /// This returns a hash of the value of IApplicationDiscriminator.Discriminator (which is most likely just the value of unless an alternative implementation of IApplicationDiscriminator has been registered).
- /// However during ConfigureServices a temporary instance of IHostingEnvironment is constructed which guarantees that this will be the hash of , so the value may differ depend on when the property is used. - ///
- /// - /// If you require this value during ConfigureServices it is probably a code smell. - /// - ///
- [Obsolete("Please use IApplicationDiscriminator.Discriminator instead.")] - string ApplicationId { get; } + /// + /// The unique application ID for this Umbraco website. + /// + /// + /// + /// The returned value will be the same consistent value for an Umbraco website on a specific server and will the + /// same + /// between restarts of that Umbraco website/application on that specific server. + /// + /// + /// The value of this does not distinguish between unique workers/servers for this Umbraco application. + /// Usage of this must take into account that the same may be returned for the same + /// Umbraco website hosted on different servers.
+ /// Similarly the usage of this must take into account that a different + /// may be returned for the same Umbraco website hosted on different servers. + ///
+ /// + /// This returns a hash of the value of IApplicationDiscriminator.Discriminator (which is most likely just the + /// value of unless an alternative + /// implementation of IApplicationDiscriminator has been registered).
+ /// However during ConfigureServices a temporary instance of IHostingEnvironment is constructed which guarantees + /// that this will be the hash of , so + /// the value may differ depend on when the property is used. + ///
+ /// + /// If you require this value during ConfigureServices it is probably a code smell. + /// + ///
+ [Obsolete("Please use IApplicationDiscriminator.Discriminator instead.")] + string ApplicationId { get; } - /// - /// Will return the physical path to the root of the application - /// - string ApplicationPhysicalPath { get; } + /// + /// Will return the physical path to the root of the application + /// + string ApplicationPhysicalPath { get; } - string LocalTempPath { get; } + string LocalTempPath { get; } - /// - /// The web application's hosted path - /// - /// - /// In most cases this will return "/" but if the site is hosted in a virtual directory then this will return the virtual directory's path such as "/mysite". - /// This value must begin with a "/" and cannot end with "/". - /// - string ApplicationVirtualPath { get; } + /// + /// The web application's hosted path + /// + /// + /// In most cases this will return "/" but if the site is hosted in a virtual directory then this will return the + /// virtual directory's path such as "/mysite". + /// This value must begin with a "/" and cannot end with "/". + /// + string ApplicationVirtualPath { get; } - bool IsDebugMode { get; } + bool IsDebugMode { get; } - /// - /// Gets a value indicating whether Umbraco is hosted. - /// - bool IsHosted { get; } + /// + /// Gets a value indicating whether Umbraco is hosted. + /// + bool IsHosted { get; } - /// - /// Gets the main application url. - /// - Uri ApplicationMainUrl { get; } + /// + /// Gets the main application url. + /// + Uri ApplicationMainUrl { get; } - /// - /// 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. - /// - [Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")] - string MapPathWebRoot(string path); + /// + /// 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. + /// + [Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")] + string MapPathWebRoot(string path); - /// - /// Maps a virtual path to a physical path to the application's root (not always equal to the 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. - /// - [Obsolete("Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")] - string MapPathContentRoot(string path); + /// + /// Maps a virtual path to a physical path to the application's root (not always equal to the 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. + /// + [Obsolete( + "Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")] + string MapPathContentRoot(string path); - /// - /// 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. - /// - /// This maps the virtual path syntax to the web root. For example when hosting in a virtual directory called "site" and the value "~/pages/test" is passed in, it will - /// map to "/site/pages/test" where "/site" is the value of . - /// - /// - /// If virtualPath does not start with ~/ or / - /// - string ToAbsolute(string virtualPath); + /// + /// 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. + /// + /// This maps the virtual path syntax to the web root. For example when hosting in a virtual directory called "site" + /// and the value "~/pages/test" is passed in, it will + /// map to "/site/pages/test" where "/site" is the value of . + /// + /// + /// If virtualPath does not start with ~/ or / + /// + string ToAbsolute(string virtualPath); - /// - /// Ensures that the application know its main Url. - /// - void EnsureApplicationMainUrl(Uri? currentApplicationUrl); - } + /// + /// Ensures that the application know its main Url. + /// + void EnsureApplicationMainUrl(Uri? currentApplicationUrl); } diff --git a/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs b/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs index f55040f96a..493c3ab4dc 100644 --- a/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Hosting/IUmbracoApplicationLifetime.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Hosting -{ - public interface IUmbracoApplicationLifetime - { - /// - /// A value indicating whether the application is restarting after the current request. - /// - bool IsRestarting { get; } +namespace Umbraco.Cms.Core.Hosting; - /// - /// Terminates the current application. The application restarts the next time a request is received for it. - /// - void Restart(); - } +public interface IUmbracoApplicationLifetime +{ + /// + /// A value indicating whether the application is restarting after the current request. + /// + bool IsRestarting { get; } + + /// + /// Terminates the current application. The application restarts the next time a request is received for it. + /// + void Restart(); } diff --git a/src/Umbraco.Core/Hosting/NoopApplicationShutdownRegistry.cs b/src/Umbraco.Core/Hosting/NoopApplicationShutdownRegistry.cs index 15b08d1ac6..e821102f09 100644 --- a/src/Umbraco.Core/Hosting/NoopApplicationShutdownRegistry.cs +++ b/src/Umbraco.Core/Hosting/NoopApplicationShutdownRegistry.cs @@ -1,8 +1,12 @@ -namespace Umbraco.Cms.Core.Hosting +namespace Umbraco.Cms.Core.Hosting; + +internal class NoopApplicationShutdownRegistry : IApplicationShutdownRegistry { - internal class NoopApplicationShutdownRegistry : IApplicationShutdownRegistry + public void RegisterObject(IRegisteredObject registeredObject) + { + } + + public void UnregisterObject(IRegisteredObject registeredObject) { - public void RegisterObject(IRegisteredObject registeredObject) { } - public void UnregisterObject(IRegisteredObject registeredObject) { } } } diff --git a/src/Umbraco.Core/HybridAccessorBase.cs b/src/Umbraco.Core/HybridAccessorBase.cs index 3200f97d7d..fdee8e4ec5 100644 --- a/src/Umbraco.Core/HybridAccessorBase.cs +++ b/src/Umbraco.Core/HybridAccessorBase.cs @@ -1,78 +1,78 @@ -using System; -using System.Threading; using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Provides a base class for hybrid accessors. +/// +/// The type of the accessed object. +/// +/// +/// Hybrid accessors store the accessed object in HttpContext if they can, +/// otherwise they rely on the logical call context, to maintain an ambient +/// object that flows with async. +/// +/// +public abstract class HybridAccessorBase + where T : class { - /// - /// Provides a base class for hybrid accessors. - /// - /// The type of the accessed object. - /// - /// Hybrid accessors store the accessed object in HttpContext if they can, - /// otherwise they rely on the logical call context, to maintain an ambient - /// object that flows with async. - /// - public abstract class HybridAccessorBase - where T : class + private static readonly AsyncLocal AmbientContext = new(); + + private readonly IRequestCache _requestCache; + private string? _itemKey; + + protected HybridAccessorBase(IRequestCache requestCache) + => _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + + protected string ItemKey => _itemKey ??= GetType().FullName!; + + protected T? Value { - private static readonly AsyncLocal s_ambientContext = new AsyncLocal(); - - private readonly IRequestCache _requestCache; - private string? _itemKey; - protected string ItemKey => _itemKey ??= GetType().FullName!; - - // read - // http://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html - // http://stackoverflow.com/questions/14176028/why-does-logicalcallcontext-not-work-with-async - // http://stackoverflow.com/questions/854976/will-values-in-my-threadstatic-variables-still-be-there-when-cycled-via-threadpo - // https://msdn.microsoft.com/en-us/library/dd642243.aspx?f=255&MSPPError=-2147217396 ThreadLocal - // http://stackoverflow.com/questions/29001266/cleaning-up-callcontext-in-tpl clearing call context - // - // anything that is ThreadStatic will stay with the thread and NOT flow in async threads - // the only thing that flows is the logical call context (safe in 4.5+) - - // no! - //[ThreadStatic] - //private static T _value; - - // yes! flows with async! - private T? NonContextValue + get { - get => s_ambientContext.Value ?? default; - set => s_ambientContext.Value = value; - } - - protected HybridAccessorBase(IRequestCache requestCache) - => _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - - protected T? Value - { - get + if (!_requestCache.IsAvailable) { - if (!_requestCache.IsAvailable) - { - return NonContextValue; - } - return (T?) _requestCache.Get(ItemKey); + return NonContextValue; } - set + return (T?)_requestCache.Get(ItemKey); + } + + set + { + if (!_requestCache.IsAvailable) { - if (!_requestCache.IsAvailable) - { - NonContextValue = value; - } - else if (value == null) - { - _requestCache.Remove(ItemKey); - } - else - { - _requestCache.Set(ItemKey, value); - } + NonContextValue = value; + } + else if (value == null) + { + _requestCache.Remove(ItemKey); + } + else + { + _requestCache.Set(ItemKey, value); } } } + + // read + // http://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html + // http://stackoverflow.com/questions/14176028/why-does-logicalcallcontext-not-work-with-async + // http://stackoverflow.com/questions/854976/will-values-in-my-threadstatic-variables-still-be-there-when-cycled-via-threadpo + // https://msdn.microsoft.com/en-us/library/dd642243.aspx?f=255&MSPPError=-2147217396 ThreadLocal + // http://stackoverflow.com/questions/29001266/cleaning-up-callcontext-in-tpl clearing call context + // + // anything that is ThreadStatic will stay with the thread and NOT flow in async threads + // the only thing that flows is the logical call context (safe in 4.5+) + + // no! + // [ThreadStatic] + // private static T _value; + + // yes! flows with async! + private T? NonContextValue + { + get => AmbientContext.Value ?? default; + set => AmbientContext.Value = value; + } } diff --git a/src/Umbraco.Core/HybridEventMessagesAccessor.cs b/src/Umbraco.Core/HybridEventMessagesAccessor.cs index 14fa0433ce..d129b9a117 100644 --- a/src/Umbraco.Core/HybridEventMessagesAccessor.cs +++ b/src/Umbraco.Core/HybridEventMessagesAccessor.cs @@ -1,19 +1,18 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core -{ - public class HybridEventMessagesAccessor : HybridAccessorBase, IEventMessagesAccessor - { - public HybridEventMessagesAccessor(IRequestCache requestCache) - : base(requestCache) - { - } +namespace Umbraco.Cms.Core; - public EventMessages? EventMessages - { - get { return Value; } - set { Value = value; } - } +public class HybridEventMessagesAccessor : HybridAccessorBase, IEventMessagesAccessor +{ + public HybridEventMessagesAccessor(IRequestCache requestCache) + : base(requestCache) + { + } + + public EventMessages? EventMessages + { + get => Value; + set => Value = value; } } diff --git a/src/Umbraco.Core/IBackOfficeInfo.cs b/src/Umbraco.Core/IBackOfficeInfo.cs index 66f5d97bd9..bc27eb7f16 100644 --- a/src/Umbraco.Core/IBackOfficeInfo.cs +++ b/src/Umbraco.Core/IBackOfficeInfo.cs @@ -1,10 +1,10 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public interface IBackOfficeInfo { - public interface IBackOfficeInfo - { - /// - /// Gets the absolute url to the Umbraco Backoffice. This info can be used to build absolute urls for Backoffice to use in mails etc. - /// - string GetAbsoluteUrl { get; } - } + /// + /// Gets the absolute url to the Umbraco Backoffice. This info can be used to build absolute urls for Backoffice to use + /// in mails etc. + /// + string GetAbsoluteUrl { get; } } diff --git a/src/Umbraco.Core/ICompletable.cs b/src/Umbraco.Core/ICompletable.cs index 2061723575..b13000de22 100644 --- a/src/Umbraco.Core/ICompletable.cs +++ b/src/Umbraco.Core/ICompletable.cs @@ -1,9 +1,6 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public interface ICompletable : IDisposable { - public interface ICompletable : IDisposable - { - void Complete(); - } + void Complete(); } diff --git a/src/Umbraco.Core/IO/CleanFolderResult.cs b/src/Umbraco.Core/IO/CleanFolderResult.cs index d2bed317a6..76d1767eab 100644 --- a/src/Umbraco.Core/IO/CleanFolderResult.cs +++ b/src/Umbraco.Core/IO/CleanFolderResult.cs @@ -1,49 +1,33 @@ -using System; -using System.Collections.Generic; -using System.IO; +namespace Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.IO +public class CleanFolderResult { - public class CleanFolderResult + private CleanFolderResult() { - private CleanFolderResult() + } + + public CleanFolderResultStatus Status { get; private set; } + + public IReadOnlyCollection? Errors { get; private set; } + + public static CleanFolderResult Success() => new CleanFolderResult { Status = CleanFolderResultStatus.Success }; + + public static CleanFolderResult FailedAsDoesNotExist() => + new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist }; + + public static CleanFolderResult FailedWithErrors(List errors) => + new CleanFolderResult { Status = CleanFolderResultStatus.FailedWithException, Errors = errors.AsReadOnly() }; + + public class Error + { + public Error(Exception exception, FileInfo erroringFile) { + Exception = exception; + ErroringFile = erroringFile; } - public CleanFolderResultStatus Status { get; private set; } + public Exception Exception { get; set; } - public IReadOnlyCollection? Errors { get; private set; } - - public static CleanFolderResult Success() - { - return new CleanFolderResult { Status = CleanFolderResultStatus.Success }; - } - - public static CleanFolderResult FailedAsDoesNotExist() - { - return new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist }; - } - - public static CleanFolderResult FailedWithErrors(List errors) - { - return new CleanFolderResult - { - Status = CleanFolderResultStatus.FailedWithException, - Errors = errors.AsReadOnly(), - }; - } - - public class Error - { - public Error(Exception exception, FileInfo erroringFile) - { - Exception = exception; - ErroringFile = erroringFile; - } - - public Exception Exception { get; set; } - - public FileInfo ErroringFile { get; set; } - } + public FileInfo ErroringFile { get; set; } } } diff --git a/src/Umbraco.Core/IO/CleanFolderResultStatus.cs b/src/Umbraco.Core/IO/CleanFolderResultStatus.cs index 3180677acb..73d32982aa 100644 --- a/src/Umbraco.Core/IO/CleanFolderResultStatus.cs +++ b/src/Umbraco.Core/IO/CleanFolderResultStatus.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public enum CleanFolderResultStatus { - public enum CleanFolderResultStatus - { - Success, - FailedAsDoesNotExist, - FailedWithException - } + Success, + FailedAsDoesNotExist, + FailedWithException, } diff --git a/src/Umbraco.Core/IO/DefaultViewContentProvider.cs b/src/Umbraco.Core/IO/DefaultViewContentProvider.cs index e78118da62..5e0c10d80d 100644 --- a/src/Umbraco.Core/IO/DefaultViewContentProvider.cs +++ b/src/Umbraco.Core/IO/DefaultViewContentProvider.cs @@ -1,62 +1,66 @@ using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class DefaultViewContentProvider : IDefaultViewContentProvider { - public class DefaultViewContentProvider : IDefaultViewContentProvider + public string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null) { - public string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null) + var content = new StringBuilder(); + + if (string.IsNullOrWhiteSpace(modelNamespaceAlias)) { - var content = new StringBuilder(); - - if (string.IsNullOrWhiteSpace(modelNamespaceAlias)) - modelNamespaceAlias = "ContentModels"; - - // either - // @inherits Umbraco.Web.Mvc.UmbracoViewPage - // @inherits Umbraco.Web.Mvc.UmbracoViewPage - content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;"); - content.Append("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"); - if (modelClassName.IsNullOrWhiteSpace() == false) - { - content.Append("<"); - if (modelNamespace.IsNullOrWhiteSpace() == false) - { - content.Append(modelNamespaceAlias); - content.Append("."); - } - content.Append(modelClassName); - content.Append(">"); - } - content.Append("\r\n"); - - // if required, add - // @using ContentModels = ModelNamespace; - if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false) - { - content.Append("@using "); - content.Append(modelNamespaceAlias); - content.Append(" = "); - content.Append(modelNamespace); - content.Append(";\r\n"); - } - - // either - // Layout = null; - // Layout = "layoutPage.cshtml"; - content.Append("@{\r\n\tLayout = "); - if (layoutPageAlias.IsNullOrWhiteSpace()) - { - content.Append("null"); - } - else - { - content.Append("\""); - content.Append(layoutPageAlias); - content.Append(".cshtml\""); - } - content.Append(";\r\n}"); - return content.ToString(); + modelNamespaceAlias = "ContentModels"; } + + // either + // @inherits Umbraco.Web.Mvc.UmbracoViewPage + // @inherits Umbraco.Web.Mvc.UmbracoViewPage + content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;"); + content.Append("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"); + if (modelClassName.IsNullOrWhiteSpace() == false) + { + content.Append("<"); + if (modelNamespace.IsNullOrWhiteSpace() == false) + { + content.Append(modelNamespaceAlias); + content.Append("."); + } + + content.Append(modelClassName); + content.Append(">"); + } + + content.Append("\r\n"); + + // if required, add + // @using ContentModels = ModelNamespace; + if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false) + { + content.Append("@using "); + content.Append(modelNamespaceAlias); + content.Append(" = "); + content.Append(modelNamespace); + content.Append(";\r\n"); + } + + // either + // Layout = null; + // Layout = "layoutPage.cshtml"; + content.Append("@{\r\n\tLayout = "); + if (layoutPageAlias.IsNullOrWhiteSpace()) + { + content.Append("null"); + } + else + { + content.Append("\""); + content.Append(layoutPageAlias); + content.Append(".cshtml\""); + } + + content.Append(";\r\n}"); + return content.ToString(); } } diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 16ac1b0041..44bc1ac2ad 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -1,112 +1,109 @@ -using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Security.Cryptography; using System.Text; -using System.Threading; using Microsoft.Extensions.FileProviders; using Umbraco.Cms.Core.IO; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class FileSystemExtensions { - public static class FileSystemExtensions + public static string GetStreamHash(this Stream fileStream) { - public static string GetStreamHash(this Stream fileStream) + if (fileStream.CanSeek) { - if (fileStream.CanSeek) - { - fileStream.Seek(0, SeekOrigin.Begin); - } - - using HashAlgorithm alg = SHA1.Create(); - - // create a string output for the hash - var stringBuilder = new StringBuilder(); - var hashedByteArray = alg.ComputeHash(fileStream); - foreach (var b in hashedByteArray) - { - stringBuilder.Append(b.ToString("x2")); - } - return stringBuilder.ToString(); + fileStream.Seek(0, SeekOrigin.Begin); } - /// - /// Attempts to open the file at filePath up to maxRetries times, - /// with a thread sleep time of sleepPerRetryInMilliseconds between retries. - /// - public static FileStream OpenReadWithRetry(this FileInfo file, int maxRetries = 5, int sleepPerRetryInMilliseconds = 50) - { - var retries = maxRetries; + using HashAlgorithm alg = SHA1.Create(); - while (retries > 0) + // create a string output for the hash + var stringBuilder = new StringBuilder(); + var hashedByteArray = alg.ComputeHash(fileStream); + foreach (var b in hashedByteArray) + { + stringBuilder.Append(b.ToString("x2")); + } + + return stringBuilder.ToString(); + } + + /// + /// Attempts to open the file at filePath up to maxRetries times, + /// with a thread sleep time of sleepPerRetryInMilliseconds between retries. + /// + public static FileStream OpenReadWithRetry(this FileInfo file, int maxRetries = 5, int sleepPerRetryInMilliseconds = 50) + { + var retries = maxRetries; + + while (retries > 0) + { + try { - try + return File.OpenRead(file.FullName); + } + catch (IOException) + { + retries--; + + if (retries == 0) { - return File.OpenRead(file.FullName); + throw; } - catch(IOException) - { - retries--; - if (retries == 0) - { - throw; - } - - Thread.Sleep(sleepPerRetryInMilliseconds); - } - } - - throw new ArgumentException("Retries must be greater than zero"); - } - - public static void CopyFile(this IFileSystem fs, string path, string newPath) - { - using (Stream stream = fs.OpenFile(path)) - { - fs.AddFile(newPath, stream); + Thread.Sleep(sleepPerRetryInMilliseconds); } } - public static string GetExtension(this IFileSystem fs, string path) - { - return Path.GetExtension(fs.GetFullPath(path)); - } + throw new ArgumentException("Retries must be greater than zero"); + } - public static string GetFileName(this IFileSystem fs, string path) + public static void CopyFile(this IFileSystem fs, string path, string newPath) + { + using (Stream stream = fs.OpenFile(path)) { - return Path.GetFileName(fs.GetFullPath(path)); - } - - // TODO: Currently this is the only way to do this - public static void CreateFolder(this IFileSystem fs, string folderPath) - { - var path = fs.GetRelativePath(folderPath); - var tempFile = Path.Combine(path, Guid.NewGuid().ToString("N") + ".tmp"); - using (var s = new MemoryStream()) - { - fs.AddFile(tempFile, s); - } - fs.DeleteFile(tempFile); - } - - /// - /// Creates a new from the file system. - /// - /// The file system. - /// When this method returns, contains an created from the file system. - /// - /// true if the was successfully created; otherwise, false. - /// - public static bool TryCreateFileProvider(this IFileSystem fileSystem, [MaybeNullWhen(false)] out IFileProvider fileProvider) - { - fileProvider = fileSystem switch - { - IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), - _ => null - }; - - return fileProvider != null; + fs.AddFile(newPath, stream); } } + + public static string GetExtension(this IFileSystem fs, string path) => Path.GetExtension(fs.GetFullPath(path)); + + public static string GetFileName(this IFileSystem fs, string path) => Path.GetFileName(fs.GetFullPath(path)); + + // TODO: Currently this is the only way to do this + public static void CreateFolder(this IFileSystem fs, string folderPath) + { + var path = fs.GetRelativePath(folderPath); + var tempFile = Path.Combine(path, Guid.NewGuid().ToString("N") + ".tmp"); + using (var s = new MemoryStream()) + { + fs.AddFile(tempFile, s); + } + + fs.DeleteFile(tempFile); + } + + /// + /// Creates a new from the file system. + /// + /// The file system. + /// + /// When this method returns, contains an created from the file + /// system. + /// + /// + /// true if the was successfully created; otherwise, false. + /// + public static bool TryCreateFileProvider( + this IFileSystem fileSystem, + [MaybeNullWhen(false)] out IFileProvider fileProvider) + { + fileProvider = fileSystem switch + { + IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), + _ => null, + }; + + return fileProvider != null; + } } diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs index 5a4c92d509..2a5fa685df 100644 --- a/src/Umbraco.Core/IO/FileSystems.cs +++ b/src/Umbraco.Core/IO/FileSystems.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -29,13 +26,13 @@ namespace Umbraco.Cms.Core.IO private ShadowWrapper? _mvcViewsFileSystem; // well-known file systems lazy initialization - private object _wkfsLock = new object(); + private object _wkfsLock = new(); private bool _wkfsInitialized; private object? _wkfsObject; // unused // shadow support - private readonly List _shadowWrappers = new List(); - private readonly object _shadowLocker = new object(); + private readonly List _shadowWrappers = new(); + private readonly object _shadowLocker = new(); private static string? _shadowCurrentId; // static - unique!! #region Constructor @@ -193,7 +190,7 @@ namespace Umbraco.Cms.Core.IO // to the VirtualPath we get with CodeFileDisplay from the frontend. try { - var rootPath = fileSystem.GetFullPath("/css/"); + fileSystem.GetFullPath("/css/"); } catch (UnauthorizedAccessException exception) { @@ -201,7 +198,8 @@ namespace Umbraco.Cms.Core.IO "Can't register the stylesheet filesystem, " + "this is most likely caused by using a PhysicalFileSystem with an incorrect " + "rootPath/rootUrl. RootPath must be \\wwwroot\\css" - + " and rootUrl must be /css", exception); + + " and rootUrl must be /css", + exception); } _stylesheetsFileSystem = CreateShadowWrapperInternal(fileSystem, "css"); @@ -213,7 +211,7 @@ namespace Umbraco.Cms.Core.IO // but it does not really matter what we return - here, null private object? CreateWellKnownFileSystems() { - var logger = _loggerFactory.CreateLogger(); + ILogger logger = _loggerFactory.CreateLogger(); //TODO this is fucked, why do PhysicalFileSystem has a root url? Mvc views cannot be accessed by url! var macroPartialFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials), _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.MacroPartials)); @@ -228,7 +226,10 @@ namespace Umbraco.Cms.Core.IO if (_stylesheetsFileSystem == null) { - var stylesheetsFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, + var stylesheetsFileSystem = new PhysicalFileSystem( + _ioHelper, + _hostingEnvironment, + logger, _hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath)); diff --git a/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs b/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs index a2937f3f8e..3ca1fadbff 100644 --- a/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs +++ b/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs @@ -1,8 +1,6 @@ -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public interface IDefaultViewContentProvider { - public interface IDefaultViewContentProvider - { - string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, - string? modelNamespace = null, string? modelNamespaceAlias = null); - } + string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null); } diff --git a/src/Umbraco.Core/IO/IFileProviderFactory.cs b/src/Umbraco.Core/IO/IFileProviderFactory.cs index 981d5558fc..0e6cb0f0a8 100644 --- a/src/Umbraco.Core/IO/IFileProviderFactory.cs +++ b/src/Umbraco.Core/IO/IFileProviderFactory.cs @@ -1,18 +1,17 @@ using Microsoft.Extensions.FileProviders; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +/// +/// Factory for creating instances. +/// +public interface IFileProviderFactory { /// - /// Factory for creating instances. + /// Creates a new instance. /// - public interface IFileProviderFactory - { - /// - /// Creates a new instance. - /// - /// - /// The newly created instance (or null if not supported). - /// - IFileProvider? Create(); - } + /// + /// The newly created instance (or null if not supported). + /// + IFileProvider? Create(); } diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 54503b167b..da9dd0b9bb 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -1,178 +1,177 @@ -using System; -using System.Collections.Generic; -using System.IO; +namespace Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.IO +/// +/// Provides methods allowing the manipulation of files. +/// +public interface IFileSystem { /// - /// Provides methods allowing the manipulation of files. + /// Gets a value indicating whether the filesystem can add/copy + /// a file which is on a physical filesystem. /// - public interface IFileSystem - { - /// - /// Gets all directories matching the given path. - /// - /// The path to the directories. - /// - /// The representing the matched directories. - /// - IEnumerable GetDirectories(string path); + /// + /// In other words, whether the filesystem can copy/move a file + /// that is on local disk, in a fast and efficient way. + /// + bool CanAddPhysical { get; } - /// - /// Deletes the specified directory. - /// - /// The name of the directory to remove. - void DeleteDirectory(string path); + /// + /// Gets all directories matching the given path. + /// + /// The path to the directories. + /// + /// The representing the matched directories. + /// + IEnumerable GetDirectories(string path); - /// - /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. - /// - /// Azure blob storage has no real concept of directories so deletion is always recursive. - /// The name of the directory to remove. - /// Whether to remove directories, subdirectories, and files in path. - void DeleteDirectory(string path, bool recursive); + /// + /// Deletes the specified directory. + /// + /// The name of the directory to remove. + void DeleteDirectory(string path); - /// - /// Determines whether the specified directory exists. - /// - /// The directory to check. - /// - /// True if the directory exists and the user has permission to view it; otherwise false. - /// - bool DirectoryExists(string path); + /// + /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. + /// + /// Azure blob storage has no real concept of directories so deletion is always recursive. + /// The name of the directory to remove. + /// Whether to remove directories, subdirectories, and files in path. + void DeleteDirectory(string path, bool recursive); - /// - /// Adds a file to the file system. - /// - /// The path to the given file. - /// The containing the file contents. - void AddFile(string path, Stream stream); + /// + /// Determines whether the specified directory exists. + /// + /// The directory to check. + /// + /// True if the directory exists and the user has permission to view it; otherwise false. + /// + bool DirectoryExists(string path); - /// - /// Adds a file to the file system. - /// - /// The path to the given file. - /// The containing the file contents. - /// Whether to override the file if it already exists. - void AddFile(string path, Stream stream, bool overrideIfExists); + /// + /// Adds a file to the file system. + /// + /// The path to the given file. + /// The containing the file contents. + void AddFile(string path, Stream stream); - /// - /// Gets all files matching the given path. - /// - /// The path to the files. - /// - /// The representing the matched files. - /// - IEnumerable GetFiles(string path); + /// + /// Adds a file to the file system. + /// + /// The path to the given file. + /// The containing the file contents. + /// Whether to override the file if it already exists. + void AddFile(string path, Stream stream, bool overrideIfExists); - /// - /// Gets all files matching the given path and filter. - /// - /// The path to the files. - /// A filter that allows the querying of file extension. *.jpg - /// - /// The representing the matched files. - /// - IEnumerable GetFiles(string path, string filter); + /// + /// Gets all files matching the given path. + /// + /// The path to the files. + /// + /// The representing the matched files. + /// + IEnumerable GetFiles(string path); - /// - /// Gets a representing the file at the given path. - /// - /// The path to the file. - /// - /// . - /// - Stream OpenFile(string path); + /// + /// Gets all files matching the given path and filter. + /// + /// The path to the files. + /// A filter that allows the querying of file extension. + /// *.jpg + /// + /// + /// The representing the matched files. + /// + IEnumerable GetFiles(string path, string filter); - /// - /// Deletes the specified file. - /// - /// The name of the file to remove. - void DeleteFile(string path); + /// + /// Gets a representing the file at the given path. + /// + /// The path to the file. + /// + /// . + /// + Stream OpenFile(string path); - /// - /// Determines whether the specified file exists. - /// - /// The file to check. - /// - /// True if the file exists and the user has permission to view it; otherwise false. - /// - bool FileExists(string path); + /// + /// Deletes the specified file. + /// + /// The name of the file to remove. + void DeleteFile(string path); - /// - /// Returns the application relative path to the file. - /// - /// The full path or URL. - /// - /// The representing the relative path. - /// - string GetRelativePath(string fullPathOrUrl); + /// + /// Determines whether the specified file exists. + /// + /// The file to check. + /// + /// True if the file exists and the user has permission to view it; otherwise false. + /// + bool FileExists(string path); - /// - /// Gets the full qualified path to the file. - /// - /// The file to return the full path for. - /// - /// The representing the full path. - /// - string GetFullPath(string path); + /// + /// Returns the application relative path to the file. + /// + /// The full path or URL. + /// + /// The representing the relative path. + /// + string GetRelativePath(string fullPathOrUrl); - /// - /// Returns the application relative URL to the file. - /// - /// The path to return the URL for. - /// - /// representing the relative URL. - /// - string GetUrl(string? path); + /// + /// Gets the full qualified path to the file. + /// + /// The file to return the full path for. + /// + /// The representing the full path. + /// + string GetFullPath(string path); - /// - /// Gets the last modified date/time of the file, expressed as a UTC value. - /// - /// The path to the file. - /// - /// . - /// - DateTimeOffset GetLastModified(string path); + /// + /// Returns the application relative URL to the file. + /// + /// The path to return the URL for. + /// + /// representing the relative URL. + /// + string GetUrl(string? path); - /// - /// Gets the created date/time of the file, expressed as a UTC value. - /// - /// The path to the file. - /// - /// . - /// - DateTimeOffset GetCreated(string path); + /// + /// Gets the last modified date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// + DateTimeOffset GetLastModified(string path); - /// - /// Gets the size of a file. - /// - /// The path to the file. - /// The size (in bytes) of the file. - long GetSize(string path); + /// + /// Gets the created date/time of the file, expressed as a UTC value. + /// + /// The path to the file. + /// + /// . + /// + DateTimeOffset GetCreated(string path); - /// - /// Gets a value indicating whether the filesystem can add/copy - /// a file which is on a physical filesystem. - /// - /// In other words, whether the filesystem can copy/move a file - /// that is on local disk, in a fast and efficient way. - bool CanAddPhysical { get; } + /// + /// Gets the size of a file. + /// + /// The path to the file. + /// The size (in bytes) of the file. + long GetSize(string path); - /// - /// Adds a file which is on a physical filesystem. - /// - /// The path to the file. - /// The absolute physical path to the source file. - /// A value indicating what to do if the file already exists. - /// A value indicating whether to move (default) or copy. - void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false); + /// + /// Adds a file which is on a physical filesystem. + /// + /// The path to the file. + /// The absolute physical path to the source file. + /// A value indicating what to do if the file already exists. + /// A value indicating whether to move (default) or copy. + void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false); - // TODO: implement these - // - //void CreateDirectory(string path); - // - //// move or rename, directory or file - //void Move(string source, string target); - } + // TODO: implement these + // + // void CreateDirectory(string path); + // + //// move or rename, directory or file + // void Move(string source, string target); } diff --git a/src/Umbraco.Core/IO/IIOHelper.cs b/src/Umbraco.Core/IO/IIOHelper.cs index 5a814ab386..53376dd48b 100644 --- a/src/Umbraco.Core/IO/IIOHelper.cs +++ b/src/Umbraco.Core/IO/IIOHelper.cs @@ -1,73 +1,68 @@ -using System; -using System.Collections.Generic; -using System.IO; +namespace Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.IO +public interface IIOHelper { - public interface IIOHelper - { - string FindFile(string virtualPath); + string FindFile(string virtualPath); - [Obsolete("Use IHostingEnvironment.ToAbsolute instead")] - string ResolveUrl(string virtualPath); + [Obsolete("Use IHostingEnvironment.ToAbsolute instead")] + string ResolveUrl(string virtualPath); - /// - /// Maps a virtual path to a physical path in the content root folder (i.e. www) - /// - /// - /// - [Obsolete("Use IHostingEnvironment.MapPathContentRoot or IHostingEnvironment.MapPathWebRoot instead")] - string MapPath(string path); + /// + /// Maps a virtual path to a physical path in the content root folder (i.e. www) + /// + /// + /// + [Obsolete("Use IHostingEnvironment.MapPathContentRoot or IHostingEnvironment.MapPathWebRoot instead")] + string MapPath(string path); - /// - /// Verifies that the current filepath matches a directory where the user is allowed to edit a file. - /// - /// The filepath to validate. - /// The valid directory. - /// A value indicating whether the filepath is valid. - bool VerifyEditPath(string filePath, string validDir); + /// + /// Verifies that the current filepath matches a directory where the user is allowed to edit a file. + /// + /// The filepath to validate. + /// The valid directory. + /// A value indicating whether the filepath is valid. + bool VerifyEditPath(string filePath, string validDir); - /// - /// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file. - /// - /// The filepath to validate. - /// The valid directories. - /// A value indicating whether the filepath is valid. - bool VerifyEditPath(string filePath, IEnumerable validDirs); + /// + /// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file. + /// + /// The filepath to validate. + /// The valid directories. + /// A value indicating whether the filepath is valid. + bool VerifyEditPath(string filePath, IEnumerable validDirs); - /// - /// Verifies that the current filepath has one of several authorized extensions. - /// - /// The filepath to validate. - /// The valid extensions. - /// A value indicating whether the filepath is valid. - bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions); + /// + /// Verifies that the current filepath has one of several authorized extensions. + /// + /// The filepath to validate. + /// The valid extensions. + /// A value indicating whether the filepath is valid. + bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions); - bool PathStartsWith(string path, string root, params char[] separators); + bool PathStartsWith(string path, string root, params char[] separators); - void EnsurePathExists(string path); + void EnsurePathExists(string path); - /// - /// Get properly formatted relative path from an existing absolute or relative path - /// - /// - /// - string GetRelativePath(string path); + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + string GetRelativePath(string path); - /// - /// Retrieves array of temporary folders from the hosting environment. - /// - /// Array of instances. - DirectoryInfo[] GetTempFolders(); + /// + /// Retrieves array of temporary folders from the hosting environment. + /// + /// Array of instances. + DirectoryInfo[] GetTempFolders(); - /// - /// Cleans contents of a folder by deleting all files older that the provided age. - /// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it can. - /// - /// Folder to clean. - /// Age of files within folder to delete. - /// Result of operation - CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age); - - } + /// + /// Cleans contents of a folder by deleting all files older that the provided age. + /// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it + /// can. + /// + /// Folder to clean. + /// Age of files within folder to delete. + /// Result of operation + CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age); } diff --git a/src/Umbraco.Core/IO/IMediaPathScheme.cs b/src/Umbraco.Core/IO/IMediaPathScheme.cs index da9a06d1b1..70ed6c7a3b 100644 --- a/src/Umbraco.Core/IO/IMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/IMediaPathScheme.cs @@ -1,33 +1,29 @@ -using System; +namespace Umbraco.Cms.Core.IO; -namespace Umbraco.Cms.Core.IO +/// +/// Represents a media file path scheme. +/// +public interface IMediaPathScheme { /// - /// Represents a media file path scheme. + /// Gets a media file path. /// - public interface IMediaPathScheme - { - /// - /// Gets a media file path. - /// - /// The media filesystem. - /// The (content, media) item unique identifier. - /// The property type unique identifier. - /// The file name. - /// - /// The filesystem-relative complete file path. - string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename); + /// The media filesystem. + /// The (content, media) item unique identifier. + /// The property type unique identifier. + /// The file name. + /// The filesystem-relative complete file path. + string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename); - /// - /// Gets the directory that can be deleted when the file is deleted. - /// - /// The media filesystem. - /// The filesystem-relative path of the file. - /// The filesystem-relative path of the directory. - /// - /// The directory, and anything below it, will be deleted. - /// Can return null (or empty) when no directory should be deleted. - /// - string? GetDeleteDirectory(MediaFileManager fileSystem, string filepath); - } + /// + /// Gets the directory that can be deleted when the file is deleted. + /// + /// The media filesystem. + /// The filesystem-relative path of the file. + /// The filesystem-relative path of the directory. + /// + /// The directory, and anything below it, will be deleted. + /// Can return null (or empty) when no directory should be deleted. + /// + string? GetDeleteDirectory(MediaFileManager fileSystem, string filepath); } diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index d0f190868b..cffd2780da 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -1,232 +1,243 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Reflection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public abstract class IOHelper : IIOHelper { - public abstract class IOHelper : IIOHelper + private readonly IHostingEnvironment _hostingEnvironment; + + public IOHelper(IHostingEnvironment hostingEnvironment) => _hostingEnvironment = + hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + + // static compiled regex for faster performance + // private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + // helper to try and match the old path to a new virtual one + public string FindFile(string virtualPath) { - private readonly IHostingEnvironment _hostingEnvironment; + var retval = virtualPath; - public IOHelper(IHostingEnvironment hostingEnvironment) + if (virtualPath.StartsWith("~")) { - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath); } - // static compiled regex for faster performance - //private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - //helper to try and match the old path to a new virtual one - public string FindFile(string virtualPath) + if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath)) { - string retval = virtualPath; - - if (virtualPath.StartsWith("~")) - retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath); - - if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath)) - retval = _hostingEnvironment.ApplicationVirtualPath + "/" + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash); - - return retval; + retval = _hostingEnvironment.ApplicationVirtualPath + "/" + + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash); } - // TODO: This is the same as IHostingEnvironment.ToAbsolute - marked as obsolete in IIOHelper for now - public string ResolveUrl(string virtualPath) - { - if (string.IsNullOrWhiteSpace(virtualPath)) return virtualPath; - return _hostingEnvironment.ToAbsolute(virtualPath); + return retval; + } + // TODO: This is the same as IHostingEnvironment.ToAbsolute - marked as obsolete in IIOHelper for now + public string ResolveUrl(string virtualPath) + { + if (string.IsNullOrWhiteSpace(virtualPath)) + { + return virtualPath; } - public string MapPath(string path) - { - if (path == null) throw new ArgumentNullException(nameof(path)); + return _hostingEnvironment.ToAbsolute(virtualPath); + } - // Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1 - if (IsPathFullyQualified(path)) + public string MapPath(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1 + if (IsPathFullyQualified(path)) + { + return path; + } + + if (_hostingEnvironment.IsHosted) + { + var result = !string.IsNullOrEmpty(path) && + (path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath)) + ? _hostingEnvironment.MapPathWebRoot(path) + : _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart(Constants.CharArrays.ForwardSlash)); + + if (result != null) { - return path; + return result; + } + } + + var dirSepChar = Path.DirectorySeparatorChar; + var root = Assembly.GetExecutingAssembly().GetRootDirectorySafe(); + var newPath = path.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', dirSepChar); + var retval = root + dirSepChar.ToString(CultureInfo.InvariantCulture) + newPath; + + return retval; + } + + /// + /// Verifies that the current filepath matches a directory where the user is allowed to edit a file. + /// + /// The filepath to validate. + /// The valid directory. + /// A value indicating whether the filepath is valid. + public bool VerifyEditPath(string filePath, string validDir) => VerifyEditPath(filePath, new[] { validDir }); + + /// + /// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file. + /// + /// The filepath to validate. + /// The valid directories. + /// A value indicating whether the filepath is valid. + public bool VerifyEditPath(string filePath, IEnumerable validDirs) + { + // this is called from ScriptRepository, PartialViewRepository, etc. + // filePath is the fullPath (rooted, filesystem path, can be trusted) + // validDirs are virtual paths (eg ~/Views) + // + // except that for templates, filePath actually is a virtual path + + // TODO: what's below is dirty, there are too many ways to get the root dir, etc. + // not going to fix everything today + var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath); + if (!PathStartsWith(filePath, mappedRoot)) + { + // TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot + filePath = _hostingEnvironment.MapPathWebRoot(filePath); + } + + // yes we can (see above) + //// don't trust what we get, it may contain relative segments + // filePath = Path.GetFullPath(filePath); + foreach (var dir in validDirs) + { + var validDir = dir; + if (!PathStartsWith(validDir, mappedRoot)) + { + validDir = _hostingEnvironment.MapPathWebRoot(validDir); } - if (_hostingEnvironment.IsHosted) + if (PathStartsWith(filePath, validDir)) { - var result = (!string.IsNullOrEmpty(path) && (path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath))) - ? _hostingEnvironment.MapPathWebRoot(path) - : _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart(Constants.CharArrays.ForwardSlash)); - - if (result != null) return result; + return true; } - - var dirSepChar = Path.DirectorySeparatorChar; - var root = Assembly.GetExecutingAssembly().GetRootDirectorySafe(); - var newPath = path.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', dirSepChar); - var retval = root + dirSepChar.ToString(CultureInfo.InvariantCulture) + newPath; - - return retval; } - /// - /// Returns true if the path has a root, and is considered fully qualified for the OS it is on - /// See https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281 for the .NET Standard 2.1 version of this - /// - /// The path to check - /// True if the path is fully qualified, false otherwise - public abstract bool IsPathFullyQualified(string path); + return false; + } + /// + /// Verifies that the current filepath has one of several authorized extensions. + /// + /// The filepath to validate. + /// The valid extensions. + /// A value indicating whether the filepath is valid. + public bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions) + { + var ext = Path.GetExtension(filePath); + return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period)); + } - /// - /// Verifies that the current filepath matches a directory where the user is allowed to edit a file. - /// - /// The filepath to validate. - /// The valid directory. - /// A value indicating whether the filepath is valid. - public bool VerifyEditPath(string filePath, string validDir) + public abstract bool PathStartsWith(string path, string root, params char[] separators); + + public void EnsurePathExists(string path) + { + var absolutePath = MapPath(path); + if (Directory.Exists(absolutePath) == false) { - return VerifyEditPath(filePath, new[] { validDir }); - } - - /// - /// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file. - /// - /// The filepath to validate. - /// The valid directories. - /// A value indicating whether the filepath is valid. - public bool VerifyEditPath(string filePath, IEnumerable validDirs) - { - // this is called from ScriptRepository, PartialViewRepository, etc. - // filePath is the fullPath (rooted, filesystem path, can be trusted) - // validDirs are virtual paths (eg ~/Views) - // - // except that for templates, filePath actually is a virtual path - - // TODO: what's below is dirty, there are too many ways to get the root dir, etc. - // not going to fix everything today - - var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath); - if (!PathStartsWith(filePath, mappedRoot)) - { - // TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot - filePath = _hostingEnvironment.MapPathWebRoot(filePath); - } - - // yes we can (see above) - //// don't trust what we get, it may contain relative segments - //filePath = Path.GetFullPath(filePath); - - foreach (var dir in validDirs) - { - var validDir = dir; - if (!PathStartsWith(validDir, mappedRoot)) - validDir = _hostingEnvironment.MapPathWebRoot(validDir); - - if (PathStartsWith(filePath, validDir)) - return true; - } - - return false; - } - - /// - /// Verifies that the current filepath has one of several authorized extensions. - /// - /// The filepath to validate. - /// The valid extensions. - /// A value indicating whether the filepath is valid. - public bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions) - { - var ext = Path.GetExtension(filePath); - return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period)); - } - - public abstract bool PathStartsWith(string path, string root, params char[] separators); - - public void EnsurePathExists(string path) - { - var absolutePath = MapPath(path); - if (Directory.Exists(absolutePath) == false) - Directory.CreateDirectory(absolutePath); - } - - /// - /// Get properly formatted relative path from an existing absolute or relative path - /// - /// - /// - public string GetRelativePath(string path) - { - if (path.IsFullPath()) - { - var rootDirectory = MapPath("~"); - var relativePath = PathStartsWith(path, rootDirectory) ? path.Substring(rootDirectory.Length) : path; - path = relativePath; - } - - return PathUtility.EnsurePathIsApplicationRootPrefixed(path); - } - - /// - /// Retrieves array of temporary folders from the hosting environment. - /// - /// Array of instances. - public DirectoryInfo[] GetTempFolders() - { - var tempFolderPaths = new[] - { - _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads) - }; - - foreach (var tempFolderPath in tempFolderPaths) - { - // Ensure it exists - Directory.CreateDirectory(tempFolderPath); - } - - return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray(); - } - - /// - /// Cleans contents of a folder by deleting all files older that the provided age. - /// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it can. - /// - /// Folder to clean. - /// Age of files within folder to delete. - /// Result of operation. - public CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age) - { - folder.Refresh(); // In case it's changed during runtime. - - if (!folder.Exists) - { - return CleanFolderResult.FailedAsDoesNotExist(); - } - - var files = folder.GetFiles("*.*", SearchOption.AllDirectories); - var errors = new List(); - foreach (var file in files) - { - if (DateTime.UtcNow - file.LastWriteTimeUtc > age) - { - try - { - file.IsReadOnly = false; - file.Delete(); - } - catch (Exception ex) - { - errors.Add(new CleanFolderResult.Error(ex, file)); - } - } - } - - return errors.Any() - ? CleanFolderResult.FailedWithErrors(errors) - : CleanFolderResult.Success(); + Directory.CreateDirectory(absolutePath); } } + + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + public string GetRelativePath(string path) + { + if (path.IsFullPath()) + { + var rootDirectory = MapPath("~"); + var relativePath = PathStartsWith(path, rootDirectory) ? path[rootDirectory.Length..] : path; + path = relativePath; + } + + return PathUtility.EnsurePathIsApplicationRootPrefixed(path); + } + + /// + /// Retrieves array of temporary folders from the hosting environment. + /// + /// Array of instances. + public DirectoryInfo[] GetTempFolders() + { + var tempFolderPaths = new[] + { + _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads), + }; + + foreach (var tempFolderPath in tempFolderPaths) + { + // Ensure it exists + Directory.CreateDirectory(tempFolderPath); + } + + return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray(); + } + + /// + /// Cleans contents of a folder by deleting all files older that the provided age. + /// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it + /// can. + /// + /// Folder to clean. + /// Age of files within folder to delete. + /// Result of operation. + public CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age) + { + folder.Refresh(); // In case it's changed during runtime. + + if (!folder.Exists) + { + return CleanFolderResult.FailedAsDoesNotExist(); + } + + FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); + var errors = new List(); + foreach (FileInfo file in files) + { + if (DateTime.UtcNow - file.LastWriteTimeUtc > age) + { + try + { + file.IsReadOnly = false; + file.Delete(); + } + catch (Exception ex) + { + errors.Add(new CleanFolderResult.Error(ex, file)); + } + } + } + + return errors.Any() + ? CleanFolderResult.FailedWithErrors(errors) + : CleanFolderResult.Success(); + } + + /// + /// Returns true if the path has a root, and is considered fully qualified for the OS it is on + /// See + /// https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281 + /// for the .NET Standard 2.1 version of this + /// + /// The path to check + /// True if the path is fully qualified, false otherwise + public abstract bool IsPathFullyQualified(string path); } diff --git a/src/Umbraco.Core/IO/IOHelperExtensions.cs b/src/Umbraco.Core/IO/IOHelperExtensions.cs index 1625c239ff..7ae90e7f8e 100644 --- a/src/Umbraco.Core/IO/IOHelperExtensions.cs +++ b/src/Umbraco.Core/IO/IOHelperExtensions.cs @@ -1,55 +1,54 @@ -using System; -using System.IO; using Umbraco.Cms.Core.IO; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class IOHelperExtensions { - public static class IOHelperExtensions + /// + /// Will resolve a virtual path URL to an absolute path, else if it is not a virtual path (i.e. starts with ~/) then + /// it will just return the path as-is (relative). + /// + /// + /// + /// + public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path) { - /// - /// Will resolve a virtual path URL to an absolute path, else if it is not a virtual path (i.e. starts with ~/) then - /// it will just return the path as-is (relative). - /// - /// - /// - /// - public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path) + if (string.IsNullOrWhiteSpace(path)) { - if (string.IsNullOrWhiteSpace(path)) return path; - return path.StartsWith("~/") ? ioHelper.ResolveUrl(path) : path; + return path; } - /// - /// Tries to create a directory. - /// - /// The IOHelper. - /// the directory path. - /// true if the directory was created, false otherwise. - public static bool TryCreateDirectory(this IIOHelper ioHelper, string dir) - { - try - { - var dirPath = ioHelper.MapPath(dir); - - if (Directory.Exists(dirPath) == false) - Directory.CreateDirectory(dirPath); - - var filePath = dirPath + "/" + CreateRandomFileName(ioHelper) + ".tmp"; - File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); - File.Delete(filePath); - return true; - } - catch - { - return false; - } - } - - public static string CreateRandomFileName(this IIOHelper ioHelper) - { - return "umbraco-test." + Guid.NewGuid().ToString("N").Substring(0, 8); - } - - + return path.StartsWith("~/") ? ioHelper.ResolveUrl(path) : path; } + + /// + /// Tries to create a directory. + /// + /// The IOHelper. + /// the directory path. + /// true if the directory was created, false otherwise. + public static bool TryCreateDirectory(this IIOHelper ioHelper, string dir) + { + try + { + var dirPath = ioHelper.MapPath(dir); + + if (Directory.Exists(dirPath) == false) + { + Directory.CreateDirectory(dirPath); + } + + var filePath = dirPath + "/" + CreateRandomFileName(ioHelper) + ".tmp"; + File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); + File.Delete(filePath); + return true; + } + catch + { + return false; + } + } + + public static string CreateRandomFileName(this IIOHelper ioHelper) => + "umbraco-test." + Guid.NewGuid().ToString("N").Substring(0, 8); } diff --git a/src/Umbraco.Core/IO/IOHelperLinux.cs b/src/Umbraco.Core/IO/IOHelperLinux.cs index 116a7200b3..7d936895a1 100644 --- a/src/Umbraco.Core/IO/IOHelperLinux.cs +++ b/src/Umbraco.Core/IO/IOHelperLinux.cs @@ -1,28 +1,40 @@ -using System; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class IOHelperLinux : IOHelper { - public class IOHelperLinux : IOHelper + public IOHelperLinux(IHostingEnvironment hostingEnvironment) + : base(hostingEnvironment) { - public IOHelperLinux(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + } + + public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); + + public override bool PathStartsWith(string path, string root, params char[] separators) + { + // either it is identical to root, + // or it is root + separator + anything + if (separators == null || separators.Length == 0) { + separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; } - public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); - - public override bool PathStartsWith(string path, string root, params char[] separators) + if (!path.StartsWith(root, StringComparison.Ordinal)) { - // either it is identical to root, - // or it is root + separator + anything - - if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; - if (!path.StartsWith(root, StringComparison.Ordinal)) return false; - if (path.Length == root.Length) return true; - if (path.Length < root.Length) return false; - return separators.Contains(path[root.Length]); + return false; } + + if (path.Length == root.Length) + { + return true; + } + + if (path.Length < root.Length) + { + return false; + } + + return separators.Contains(path[root.Length]); } } diff --git a/src/Umbraco.Core/IO/IOHelperOSX.cs b/src/Umbraco.Core/IO/IOHelperOSX.cs index 53b9cb4dc0..8b8ed20939 100644 --- a/src/Umbraco.Core/IO/IOHelperOSX.cs +++ b/src/Umbraco.Core/IO/IOHelperOSX.cs @@ -1,28 +1,40 @@ -using System; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class IOHelperOSX : IOHelper { - public class IOHelperOSX : IOHelper + public IOHelperOSX(IHostingEnvironment hostingEnvironment) + : base(hostingEnvironment) { - public IOHelperOSX(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + } + + public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); + + public override bool PathStartsWith(string path, string root, params char[] separators) + { + // either it is identical to root, + // or it is root + separator + anything + if (separators == null || separators.Length == 0) { + separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; } - public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path); - - public override bool PathStartsWith(string path, string root, params char[] separators) + if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) { - // either it is identical to root, - // or it is root + separator + anything - - if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; - if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return false; - if (path.Length == root.Length) return true; - if (path.Length < root.Length) return false; - return separators.Contains(path[root.Length]); + return false; } + + if (path.Length == root.Length) + { + return true; + } + + if (path.Length < root.Length) + { + return false; + } + + return separators.Contains(path[root.Length]); } } diff --git a/src/Umbraco.Core/IO/IOHelperWindows.cs b/src/Umbraco.Core/IO/IOHelperWindows.cs index cb60f164dc..9dfec76f36 100644 --- a/src/Umbraco.Core/IO/IOHelperWindows.cs +++ b/src/Umbraco.Core/IO/IOHelperWindows.cs @@ -1,54 +1,67 @@ -using System; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class IOHelperWindows : IOHelper { - public class IOHelperWindows : IOHelper + public IOHelperWindows(IHostingEnvironment hostingEnvironment) + : base(hostingEnvironment) { - public IOHelperWindows(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment) + } + + public override bool IsPathFullyQualified(string path) + { + // TODO: This implementation is taken from the .NET Standard 2.1 implementation. We should switch to using Path.IsPathFullyQualified once we are on .NET Standard 2.1 + if (path.Length < 2) { + // It isn't fixed, it must be relative. There is no way to specify a fixed + // path with one character (or less). + return false; } - public override bool IsPathFullyQualified(string path) + if (path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar) { - // TODO: This implementation is taken from the .NET Standard 2.1 implementation. We should switch to using Path.IsPathFullyQualified once we are on .NET Standard 2.1 - - if (path.Length < 2) - { - // It isn't fixed, it must be relative. There is no way to specify a fixed - // path with one character (or less). - return false; - } - - if (path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar) - { - // There is no valid way to specify a relative path with two initial slashes or - // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ - return path[1] == '?' || path[1] == Path.DirectorySeparatorChar || path[1] == Path.AltDirectorySeparatorChar; - } - - // The only way to specify a fixed path that doesn't begin with two slashes - // is the drive, colon, slash format- i.e. C:\ - return (path.Length >= 3) - && (path[1] == Path.VolumeSeparatorChar) - && (path[2] == Path.DirectorySeparatorChar || path[2] == Path.AltDirectorySeparatorChar) - // To match old behavior we'll check the drive character for validity as the path is technically - // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. - && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')); + // There is no valid way to specify a relative path with two initial slashes or + // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ + return path[1] == '?' || path[1] == Path.DirectorySeparatorChar || + path[1] == Path.AltDirectorySeparatorChar; } - public override bool PathStartsWith(string path, string root, params char[] separators) - { - // either it is identical to root, - // or it is root + separator + anything + // The only way to specify a fixed path that doesn't begin with two slashes + // is the drive, colon, slash format- i.e. C:\ + return path.Length >= 3 + && path[1] == Path.VolumeSeparatorChar + && (path[2] == Path.DirectorySeparatorChar || path[2] == Path.AltDirectorySeparatorChar) - if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; - if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return false; - if (path.Length == root.Length) return true; - if (path.Length < root.Length) return false; - return separators.Contains(path[root.Length]); + // To match old behavior we'll check the drive character for validity as the path is technically + // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. + && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')); + } + + public override bool PathStartsWith(string path, string root, params char[] separators) + { + // either it is identical to root, + // or it is root + separator + anything + if (separators == null || separators.Length == 0) + { + separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; } + + if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (path.Length == root.Length) + { + return true; + } + + if (path.Length < root.Length) + { + return false; + } + + return separators.Contains(path[root.Length]); } } diff --git a/src/Umbraco.Core/IO/IViewHelper.cs b/src/Umbraco.Core/IO/IViewHelper.cs index ae6f8698a4..f84a1ba256 100644 --- a/src/Umbraco.Core/IO/IViewHelper.cs +++ b/src/Umbraco.Core/IO/IViewHelper.cs @@ -1,13 +1,16 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public interface IViewHelper { - public interface IViewHelper - { - bool ViewExists(ITemplate t); - string GetFileContents(ITemplate t); - string CreateView(ITemplate t, bool overWrite = false); - string? UpdateViewFile(ITemplate t, string? currentAlias = null); - string ViewPath(string alias); - } + bool ViewExists(ITemplate t); + + string GetFileContents(ITemplate t); + + string CreateView(ITemplate t, bool overWrite = false); + + string? UpdateViewFile(ITemplate t, string? currentAlias = null); + + string ViewPath(string alias); } diff --git a/src/Umbraco.Core/IO/MediaFileManager.cs b/src/Umbraco.Core/IO/MediaFileManager.cs index d5c421721e..c222c01744 100644 --- a/src/Umbraco.Core/IO/MediaFileManager.cs +++ b/src/Umbraco.Core/IO/MediaFileManager.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,237 +7,246 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public sealed class MediaFileManager { - public sealed class MediaFileManager + private readonly ILogger _logger; + private readonly IMediaPathScheme _mediaPathScheme; + private readonly IServiceProvider _serviceProvider; + private readonly IShortStringHelper _shortStringHelper; + private MediaUrlGeneratorCollection? _mediaUrlGenerators; + + public MediaFileManager( + IFileSystem fileSystem, + IMediaPathScheme mediaPathScheme, + ILogger logger, + IShortStringHelper shortStringHelper, + IServiceProvider serviceProvider) { - private readonly IMediaPathScheme _mediaPathScheme; - private readonly ILogger _logger; - private readonly IShortStringHelper _shortStringHelper; - private readonly IServiceProvider _serviceProvider; - private MediaUrlGeneratorCollection? _mediaUrlGenerators; - - public MediaFileManager( - IFileSystem fileSystem, - IMediaPathScheme mediaPathScheme, - ILogger logger, - IShortStringHelper shortStringHelper, - IServiceProvider serviceProvider) - { - _mediaPathScheme = mediaPathScheme; - _logger = logger; - _shortStringHelper = shortStringHelper; - _serviceProvider = serviceProvider; - FileSystem = fileSystem; - } - - [Obsolete("Use the ctr that doesn't include unused parameters.")] - public MediaFileManager( - IFileSystem fileSystem, - IMediaPathScheme mediaPathScheme, - ILogger logger, - IShortStringHelper shortStringHelper, - IServiceProvider serviceProvider, - IOptions contentSettings) - : this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider) - { } - - /// - /// Gets the media filesystem. - /// - public IFileSystem FileSystem { get; } - - /// - /// Delete media files. - /// - /// Files to delete (filesystem-relative paths). - public void DeleteMediaFiles(IEnumerable files) - { - files = files.Distinct(); - - // kinda try to keep things under control - var options = new ParallelOptions { MaxDegreeOfParallelism = 20 }; - - Parallel.ForEach(files, options, file => - { - try - { - if (file.IsNullOrWhiteSpace()) - { - return; - } - - if (FileSystem.FileExists(file) == false) - { - return; - } - - FileSystem.DeleteFile(file); - - var directory = _mediaPathScheme.GetDeleteDirectory(this, file); - if (!directory.IsNullOrWhiteSpace()) - { - FileSystem.DeleteDirectory(directory!, true); - } - } - catch (Exception e) - { - _logger.LogError(e, "Failed to delete media file '{File}'.", file); - } - }); - } - - #region Media Path - - /// - /// Gets the file path of a media file. - /// - /// The file name. - /// The unique identifier of the content/media owning the file. - /// The unique identifier of the property type owning the file. - /// The filesystem-relative path to the media file. - /// With the old media path scheme, this CREATES a new media path each time it is invoked. - public string GetMediaPath(string? filename, Guid cuid, Guid puid) - { - filename = Path.GetFileName(filename); - if (filename == null) - { - throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); - } - - filename = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant()); - - return _mediaPathScheme.GetFilePath(this, cuid, puid, filename); - } - - #endregion - - #region Associated Media Files - - /// - /// Returns a stream (file) for a content item (or a null stream if there is no file). - /// - /// - /// The file path if a file was found - /// - /// - /// - public Stream GetFile( - IContentBase content, - out string? mediaFilePath, - string propertyTypeAlias = Constants.Conventions.Media.File, - string? culture = null, - string? segment = null) - { - // TODO: If collections were lazy we could just inject them - if (_mediaUrlGenerators == null) - { - _mediaUrlGenerators = _serviceProvider.GetRequiredService(); - } - - if (!content.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators!, out mediaFilePath, culture, segment)) - { - return Stream.Null; - } - - return FileSystem.OpenFile(mediaFilePath!); - } - - /// - /// Stores a media file associated to a property of a content item. - /// - /// The content item owning the media file. - /// The property type owning the media file. - /// The media file name. - /// A stream containing the media bytes. - /// An optional filesystem-relative filepath to the previous media file. - /// The filesystem-relative filepath to the media file. - /// - /// The file is considered "owned" by the content/propertyType. - /// If an is provided then that file (and associated thumbnails if any) is deleted - /// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new file. - /// - public string StoreFile(IContentBase content, IPropertyType? propertyType, string filename, Stream filestream, string? oldpath) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (propertyType == null) - { - throw new ArgumentNullException(nameof(propertyType)); - } - - if (filename == null) - { - throw new ArgumentNullException(nameof(filename)); - } - - if (string.IsNullOrWhiteSpace(filename)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(filename)); - } - - if (filestream == null) - { - throw new ArgumentNullException(nameof(filestream)); - } - - // clear the old file, if any - if (string.IsNullOrWhiteSpace(oldpath) == false) - { - FileSystem.DeleteFile(oldpath!); - } - - // get the filepath, store the data - var filepath = GetMediaPath(filename, content.Key, propertyType.Key); - FileSystem.AddFile(filepath, filestream); - return filepath; - } - - /// - /// Copies a media file as a new media file, associated to a property of a content item. - /// - /// The content item owning the copy of the media file. - /// The property type owning the copy of the media file. - /// The filesystem-relative path to the source media file. - /// The filesystem-relative path to the copy of the media file. - public string? CopyFile(IContentBase content, IPropertyType propertyType, string sourcepath) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (propertyType == null) - { - throw new ArgumentNullException(nameof(propertyType)); - } - - if (sourcepath == null) - { - throw new ArgumentNullException(nameof(sourcepath)); - } - - if (string.IsNullOrWhiteSpace(sourcepath)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(sourcepath)); - } - - // ensure we have a file to copy - if (FileSystem.FileExists(sourcepath) == false) - { - return null; - } - - // get the filepath - var filename = Path.GetFileName(sourcepath); - var filepath = GetMediaPath(filename, content.Key, propertyType.Key); - FileSystem.CopyFile(sourcepath, filepath); - return filepath; - } - - #endregion + _mediaPathScheme = mediaPathScheme; + _logger = logger; + _shortStringHelper = shortStringHelper; + _serviceProvider = serviceProvider; + FileSystem = fileSystem; } + + [Obsolete("Use the ctr that doesn't include unused parameters.")] + public MediaFileManager( + IFileSystem fileSystem, + IMediaPathScheme mediaPathScheme, + ILogger logger, + IShortStringHelper shortStringHelper, + IServiceProvider serviceProvider, + IOptions contentSettings) + : this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider) + { + } + + /// + /// Gets the media filesystem. + /// + public IFileSystem FileSystem { get; } + + /// + /// Delete media files. + /// + /// Files to delete (filesystem-relative paths). + public void DeleteMediaFiles(IEnumerable files) + { + files = files.Distinct(); + + // kinda try to keep things under control + var options = new ParallelOptions { MaxDegreeOfParallelism = 20 }; + + Parallel.ForEach(files, options, file => + { + try + { + if (file.IsNullOrWhiteSpace()) + { + return; + } + + if (FileSystem.FileExists(file) == false) + { + return; + } + + FileSystem.DeleteFile(file); + + var directory = _mediaPathScheme.GetDeleteDirectory(this, file); + if (!directory.IsNullOrWhiteSpace()) + { + FileSystem.DeleteDirectory(directory!, true); + } + } + catch (Exception e) + { + _logger.LogError(e, "Failed to delete media file '{File}'.", file); + } + }); + } + + #region Media Path + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// With the old media path scheme, this CREATES a new media path each time it is invoked. + public string GetMediaPath(string? filename, Guid cuid, Guid puid) + { + filename = Path.GetFileName(filename); + if (filename == null) + { + throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); + } + + filename = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant()); + + return _mediaPathScheme.GetFilePath(this, cuid, puid, filename); + } + + #endregion + + #region Associated Media Files + + /// + /// Returns a stream (file) for a content item (or a null stream if there is no file). + /// + /// + /// The file path if a file was found + /// + /// + /// + /// + /// + public Stream GetFile( + IContentBase content, + out string? mediaFilePath, + string propertyTypeAlias = Constants.Conventions.Media.File, + string? culture = null, + string? segment = null) + { + // TODO: If collections were lazy we could just inject them + if (_mediaUrlGenerators == null) + { + _mediaUrlGenerators = _serviceProvider.GetRequiredService(); + } + + if (!content.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators!, out mediaFilePath, culture, segment)) + { + return Stream.Null; + } + + return FileSystem.OpenFile(mediaFilePath!); + } + + /// + /// Stores a media file associated to a property of a content item. + /// + /// The content item owning the media file. + /// The property type owning the media file. + /// The media file name. + /// A stream containing the media bytes. + /// An optional filesystem-relative filepath to the previous media file. + /// The filesystem-relative filepath to the media file. + /// + /// The file is considered "owned" by the content/propertyType. + /// + /// If an is provided then that file (and associated thumbnails if any) is deleted + /// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new + /// file. + /// + /// + public string StoreFile(IContentBase content, IPropertyType? propertyType, string filename, Stream filestream, string? oldpath) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (propertyType == null) + { + throw new ArgumentNullException(nameof(propertyType)); + } + + if (filename == null) + { + throw new ArgumentNullException(nameof(filename)); + } + + if (string.IsNullOrWhiteSpace(filename)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(filename)); + } + + if (filestream == null) + { + throw new ArgumentNullException(nameof(filestream)); + } + + // clear the old file, if any + if (string.IsNullOrWhiteSpace(oldpath) == false) + { + FileSystem.DeleteFile(oldpath); + } + + // get the filepath, store the data + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + FileSystem.AddFile(filepath, filestream); + return filepath; + } + + /// + /// Copies a media file as a new media file, associated to a property of a content item. + /// + /// The content item owning the copy of the media file. + /// The property type owning the copy of the media file. + /// The filesystem-relative path to the source media file. + /// The filesystem-relative path to the copy of the media file. + public string? CopyFile(IContentBase content, IPropertyType propertyType, string sourcepath) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (propertyType == null) + { + throw new ArgumentNullException(nameof(propertyType)); + } + + if (sourcepath == null) + { + throw new ArgumentNullException(nameof(sourcepath)); + } + + if (string.IsNullOrWhiteSpace(sourcepath)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(sourcepath)); + } + + // ensure we have a file to copy + if (FileSystem.FileExists(sourcepath) == false) + { + return null; + } + + // get the filepath + var filename = Path.GetFileName(sourcepath); + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + FileSystem.CopyFile(sourcepath, filepath); + return filepath; + } + + #endregion } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs index 5adc81276b..b73d29df60 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs @@ -1,28 +1,25 @@ -using System; -using System.IO; +namespace Umbraco.Cms.Core.IO.MediaPathSchemes; -namespace Umbraco.Cms.Core.IO.MediaPathSchemes +/// +/// Implements a combined-guids media path scheme. +/// +/// +/// Path is "{combinedGuid}/{filename}" where combinedGuid is a combination of itemGuid and propertyGuid. +/// +public class CombinedGuidsMediaPathScheme : IMediaPathScheme { - /// - /// Implements a combined-guids media path scheme. - /// - /// - /// Path is "{combinedGuid}/{filename}" where combinedGuid is a combination of itemGuid and propertyGuid. - /// - public class CombinedGuidsMediaPathScheme : IMediaPathScheme + /// + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) { - /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) - { - // assumes that cuid and puid keys can be trusted - and that a single property type - // for a single content cannot store two different files with the same name - - var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); - var directory = HexEncoder.Encode(combinedGuid.ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... - return Path.Combine(directory, filename).Replace('\\', '/'); - } - - /// - public string GetDeleteDirectory(MediaFileManager fileSystem, string filepath) => Path.GetDirectoryName(filepath)!; + // assumes that cuid and puid keys can be trusted - and that a single property type + // for a single content cannot store two different files with the same name + Guid combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); + var directory = + HexEncoder.Encode( + combinedGuid.ToByteArray() /*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... + return Path.Combine(directory, filename).Replace('\\', '/'); } + + /// + public string GetDeleteDirectory(MediaFileManager fileSystem, string filepath) => Path.GetDirectoryName(filepath)!; } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs index 1ee821e3ed..a533a62c92 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs @@ -1,26 +1,17 @@ -using System; -using System.IO; +namespace Umbraco.Cms.Core.IO.MediaPathSchemes; -namespace Umbraco.Cms.Core.IO.MediaPathSchemes +/// +/// Implements a two-guids media path scheme. +/// +/// +/// Path is "{itemGuid}/{propertyGuid}/{filename}". +/// +public class TwoGuidsMediaPathScheme : IMediaPathScheme { - /// - /// Implements a two-guids media path scheme. - /// - /// - /// Path is "{itemGuid}/{propertyGuid}/{filename}". - /// - public class TwoGuidsMediaPathScheme : IMediaPathScheme - { - /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) - { - return Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/'); - } + /// + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) => + Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/'); - /// - public string GetDeleteDirectory(MediaFileManager fileManager, string filepath) - { - return Path.GetDirectoryName(filepath)!; - } - } + /// + public string GetDeleteDirectory(MediaFileManager fileManager, string filepath) => Path.GetDirectoryName(filepath)!; } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs index a3fe36bde9..7b7061506d 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs @@ -1,37 +1,37 @@ -using System; -using System.IO; +namespace Umbraco.Cms.Core.IO.MediaPathSchemes; -namespace Umbraco.Cms.Core.IO.MediaPathSchemes +/// +/// Implements a unique directory media path scheme. +/// +/// +/// This scheme provides deterministic short paths, with potential collisions. +/// +public class UniqueMediaPathScheme : IMediaPathScheme { - /// - /// Implements a unique directory media path scheme. - /// - /// - /// This scheme provides deterministic short paths, with potential collisions. - /// - public class UniqueMediaPathScheme : IMediaPathScheme + private const int DirectoryLength = 8; + + /// + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) { - private const int DirectoryLength = 8; + Guid combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); + var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); - /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) - { - var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); - var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); - - return Path.Combine(directory, filename).Replace('\\', '/'); - } - - /// - /// - /// Returning null so that does *not* - /// delete any directory. This is because the above shortening of the Guid to 8 chars - /// means we're increasing the risk of collision, and we don't want to delete files - /// belonging to other media items. - /// And, at the moment, we cannot delete directory "only if it is empty" because of - /// race conditions. We'd need to implement locks in for - /// this. - /// - public string? GetDeleteDirectory(MediaFileManager fileManager, string filepath) => null; + return Path.Combine(directory, filename).Replace('\\', '/'); } + + /// + /// + /// + /// Returning null so that does *not* + /// delete any directory. This is because the above shortening of the Guid to 8 chars + /// means we're increasing the risk of collision, and we don't want to delete files + /// belonging to other media items. + /// + /// + /// And, at the moment, we cannot delete directory "only if it is empty" because of + /// race conditions. We'd need to implement locks in for + /// this. + /// + /// + public string? GetDeleteDirectory(MediaFileManager fileManager, string filepath) => null; } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 30d1893792..ede481b833 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; @@ -36,11 +31,30 @@ namespace Umbraco.Cms.Core.IO _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - if (rootPath == null) throw new ArgumentNullException(nameof(rootPath)); - if (string.IsNullOrEmpty(rootPath)) throw new ArgumentException("Value can't be empty.", nameof(rootPath)); - if (rootUrl == null) throw new ArgumentNullException(nameof(rootUrl)); - if (string.IsNullOrEmpty(rootUrl)) throw new ArgumentException("Value can't be empty.", nameof(rootUrl)); - if (rootPath.StartsWith("~/")) throw new ArgumentException("Value can't be a virtual path and start with '~/'.", nameof(rootPath)); + if (rootPath == null) + { + throw new ArgumentNullException(nameof(rootPath)); + } + + if (string.IsNullOrEmpty(rootPath)) + { + throw new ArgumentException("Value can't be empty.", nameof(rootPath)); + } + + if (rootUrl == null) + { + throw new ArgumentNullException(nameof(rootUrl)); + } + + if (string.IsNullOrEmpty(rootUrl)) + { + throw new ArgumentException("Value can't be empty.", nameof(rootUrl)); + } + + if (rootPath.StartsWith("~/")) + { + throw new ArgumentException("Value can't be a virtual path and start with '~/'.", nameof(rootPath)); + } // rootPath should be... rooted, as in, it's a root path! if (Path.IsPathRooted(rootPath) == false) @@ -71,7 +85,9 @@ namespace Umbraco.Cms.Core.IO try { if (Directory.Exists(fullPath)) + { return Directory.EnumerateDirectories(fullPath).Select(GetRelativePath); + } } catch (UnauthorizedAccessException ex) { @@ -103,7 +119,9 @@ namespace Umbraco.Cms.Core.IO { var fullPath = GetFullPath(path); if (Directory.Exists(fullPath) == false) + { return; + } try { @@ -154,7 +172,11 @@ namespace Umbraco.Cms.Core.IO } var directory = Path.GetDirectoryName(fullPath); - if (directory == null) throw new InvalidOperationException("Could not get directory."); + if (directory == null) + { + throw new InvalidOperationException("Could not get directory."); + } + Directory.CreateDirectory(directory); // ensure it exists if (stream.CanSeek) @@ -191,7 +213,9 @@ namespace Umbraco.Cms.Core.IO try { if (Directory.Exists(fullPath)) + { return Directory.EnumerateFiles(fullPath, filter).Select(GetRelativePath); + } } catch (UnauthorizedAccessException ex) { @@ -224,7 +248,9 @@ namespace Umbraco.Cms.Core.IO { var fullPath = GetFullPath(path); if (File.Exists(fullPath) == false) + { return; + } try { @@ -265,12 +291,16 @@ namespace Umbraco.Cms.Core.IO // eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg" // or on unix systems "/var/wwwroot/test/Meia/1234/img.jpg" if (_ioHelper.PathStartsWith(path, _rootPathFwd, '/')) + { return path.Substring(_rootPathFwd.Length).TrimStart(Constants.CharArrays.ForwardSlash); + } // if it starts with the root URL, strip it and trim the starting slash to make it relative // eg "/Media/1234/img.jpg" => "1234/img.jpg" if (_ioHelper.PathStartsWith(path, _rootUrl, '/')) + { return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash); + } // unchanged - what else? return path.TrimStart(Constants.CharArrays.ForwardSlash); @@ -296,11 +326,15 @@ namespace Umbraco.Cms.Core.IO // we assume it's not a FS relative path and we try to convert it... but it // really makes little sense? if (path.StartsWith(Path.DirectorySeparatorChar.ToString())) + { path = GetRelativePath(path); + } // if not already rooted, combine with the root path if (_ioHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar) == false) + { path = Path.Combine(_rootPath, path); + } // sanitize - GetFullPath will take care of any relative // segments in path, eg '../../foo.tmp' - it may throw a SecurityException @@ -315,7 +349,10 @@ namespace Umbraco.Cms.Core.IO // this says that 4.7.2 supports long paths - but Windows does not // https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception?view=netframework-4.7.2 if (path.Length > 260) + { throw new PathTooLongException($"Path {path} is too long."); + } + return path; } @@ -384,18 +421,29 @@ namespace Umbraco.Cms.Core.IO if (File.Exists(fullPath)) { if (overrideIfExists == false) + { throw new InvalidOperationException($"A file at path '{path}' already exists"); + } + WithRetry(() => File.Delete(fullPath)); } var directory = Path.GetDirectoryName(fullPath); - if (directory == null) throw new InvalidOperationException("Could not get directory."); + if (directory == null) + { + throw new InvalidOperationException("Could not get directory."); + } + Directory.CreateDirectory(directory); // ensure it exists if (copy) + { WithRetry(() => File.Copy(physicalPath, fullPath)); + } else + { WithRetry(() => File.Move(physicalPath, fullPath)); + } } #region Helper Methods @@ -442,11 +490,17 @@ namespace Umbraco.Cms.Core.IO // if it's not *exactly* IOException then it could be // some inherited exception such as FileNotFoundException, // and then we don't want to retry - if (e.GetType() != typeof(IOException)) throw; + if (e.GetType() != typeof(IOException)) + { + throw; + } // if we have tried enough, throw, else swallow // the exception and retry after a pause - if (i == count) throw; + if (i == count) + { + throw; + } } Thread.Sleep(pausems); diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index bd3f9d770d..95517f8054 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -1,386 +1,473 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +internal class ShadowFileSystem : IFileSystem { - internal class ShadowFileSystem : IFileSystem + private readonly IFileSystem _sfs; + + private Dictionary? _nodes; + + public ShadowFileSystem(IFileSystem fs, IFileSystem sfs) { - private readonly IFileSystem _fs; - private readonly IFileSystem _sfs; + Inner = fs; + _sfs = sfs; + } - public ShadowFileSystem(IFileSystem fs, IFileSystem sfs) + public IFileSystem Inner { get; } + + public bool CanAddPhysical => true; + + private Dictionary Nodes => _nodes ??= new Dictionary(); + + public IEnumerable GetDirectories(string path) + { + var normPath = NormPath(path); + KeyValuePair[] shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + IEnumerable directories = Inner.GetDirectories(path); + return directories + .Except(shadows + .Where(kvp => (kvp.Value.IsDir && kvp.Value.IsDelete) || (kvp.Value.IsFile && kvp.Value.IsExist)) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsDir && kvp.Value.IsExist).Select(kvp => kvp.Key)) + .Distinct(); + } + + public void DeleteDirectory(string path) => DeleteDirectory(path, false); + + public void DeleteDirectory(string path, bool recursive) + { + if (DirectoryExists(path) == false) { - _fs = fs; - _sfs = sfs; + return; } - public IFileSystem Inner => _fs; - - public void Complete() + var normPath = NormPath(path); + if (recursive) { - if (_nodes == null) return; - var exceptions = new List(); - foreach (var kvp in _nodes) + Nodes[normPath] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList(); + foreach (KeyValuePair kvp in remove) { - if (kvp.Value.IsExist) - { - if (kvp.Value.IsFile) - { - try - { - if (_fs.CanAddPhysical) - { - _fs.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move - } - else - { - using (Stream stream = _sfs.OpenFile(kvp.Key)) - { - _fs.AddFile(kvp.Key, stream, true); - } - } - } - catch (Exception e) - { - exceptions.Add(new Exception("Could not save file \"" + kvp.Key + "\".", e)); - } - } - } - else - { - try - { - if (kvp.Value.IsDir) - _fs.DeleteDirectory(kvp.Key, true); - else - _fs.DeleteFile(kvp.Key); - } - catch (Exception e) - { - exceptions.Add(new Exception("Could not delete " + (kvp.Value.IsDir ? "directory": "file") + " \"" + kvp.Key + "\".", e)); - } - } - } - _nodes.Clear(); - - if (exceptions.Count == 0) return; - throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); - } - - private Dictionary? _nodes; - - private Dictionary Nodes => _nodes ?? (_nodes = new Dictionary()); - - private class ShadowNode - { - public ShadowNode(bool isDelete, bool isdir) - { - IsDelete = isDelete; - IsDir = isdir; + Nodes.Remove(kvp.Key); } - public bool IsDelete { get; } - public bool IsDir { get; } - - public bool IsExist => IsDelete == false; - public bool IsFile => IsDir == false; + Delete(path, true); } - - private static string NormPath(string path) + else { - return path.ToLowerInvariant().Replace("\\", "/"); - } - - // values can be "" (root), "foo", "foo/bar"... - private static bool IsChild(string path, string input) - { - if (input.StartsWith(path) == false || input.Length < path.Length + 2) - return false; - if (path.Length > 0 && input[path.Length] != '/') return false; - var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase); - return pos < 0; - } - - private static bool IsDescendant(string path, string input) - { - if (input.StartsWith(path) == false || input.Length < path.Length + 2) - return false; - return path.Length == 0 || input[path.Length] == '/'; - } - - public IEnumerable GetDirectories(string path) - { - var normPath = NormPath(path); - var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); - var directories = _fs.GetDirectories(path); - return directories - .Except(shadows.Where(kvp => (kvp.Value.IsDir && kvp.Value.IsDelete) || (kvp.Value.IsFile && kvp.Value.IsExist)) - .Select(kvp => kvp.Key)) - .Union(shadows.Where(kvp => kvp.Value.IsDir && kvp.Value.IsExist).Select(kvp => kvp.Key)) - .Distinct(); - } - - public void DeleteDirectory(string path) - { - DeleteDirectory(path, false); - } - - public void DeleteDirectory(string path, bool recursive) - { - if (DirectoryExists(path) == false) return; - var normPath = NormPath(path); - if (recursive) + // actual content + if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content + || Inner.GetDirectories(path).Any() || Inner.GetFiles(path).Any()) { - Nodes[normPath] = new ShadowNode(true, true); - var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList(); - foreach (var kvp in remove) Nodes.Remove(kvp.Key); - Delete(path, true); + throw new InvalidOperationException("Directory is not empty."); + } + + Nodes[path] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList(); + foreach (KeyValuePair kvp in remove) + { + Nodes.Remove(kvp.Key); + } + + Delete(path, false); + } + } + + public bool DirectoryExists(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + { + return sf.IsDir && sf.IsExist; + } + + return Inner.DirectoryExists(path); + } + + public void AddFile(string path, Stream stream) => AddFile(path, stream, true); + + public void AddFile(string path, Stream stream, bool overrideIfExists) + { + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out ShadowNode? sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + { + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + } + + var parts = normPath.Split(Constants.CharArrays.ForwardSlash); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + if (Nodes.TryGetValue(dirPath, out ShadowNode? sd)) + { + if (sd.IsFile) + { + throw new InvalidOperationException("Invalid path."); + } + + if (sd.IsDelete) + { + Nodes[dirPath] = new ShadowNode(false, true); + } } else { - if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content - || _fs.GetDirectories(path).Any() || _fs.GetFiles(path).Any()) // actual content - throw new InvalidOperationException("Directory is not empty."); - Nodes[path] = new ShadowNode(true, true); - var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList(); - foreach (var kvp in remove) Nodes.Remove(kvp.Key); - Delete(path, false); - } - } - - private void Delete(string path, bool recurse) - { - foreach (var file in _fs.GetFiles(path)) - { - Nodes[NormPath(file)] = new ShadowNode(true, false); - } - foreach (var dir in _fs.GetDirectories(path)) - { - Nodes[NormPath(dir)] = new ShadowNode(true, true); - if (recurse) Delete(dir, true); - } - } - - public bool DirectoryExists(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsDir && sf.IsExist; - return _fs.DirectoryExists(path); - } - - public void AddFile(string path, Stream stream) - { - AddFile(path, stream, true); - } - - public void AddFile(string path, Stream stream, bool overrideIfExists) - { - ShadowNode? sf; - var normPath = NormPath(path); - if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) - throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - - var parts = normPath.Split(Constants.CharArrays.ForwardSlash); - for (var i = 0; i < parts.Length - 1; i++) - { - var dirPath = string.Join("/", parts.Take(i + 1)); - ShadowNode? sd; - if (Nodes.TryGetValue(dirPath, out sd)) + if (Inner.DirectoryExists(dirPath)) { - if (sd.IsFile) throw new InvalidOperationException("Invalid path."); - if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + continue; } - else + + if (Inner.FileExists(dirPath)) + { + throw new InvalidOperationException("Invalid path."); + } + + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, stream, overrideIfExists); + Nodes[normPath] = new ShadowNode(false, false); + } + + public IEnumerable GetFiles(string path) => GetFiles(path, null); + + public IEnumerable GetFiles(string path, string? filter) + { + var normPath = NormPath(path); + KeyValuePair[] shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + IEnumerable files = filter != null ? Inner.GetFiles(path, filter) : Inner.GetFiles(path); + WildcardExpression? wildcard = filter == null ? null : new WildcardExpression(filter); + return files + .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) + .Select(kvp => kvp.Key)) + .Union(shadows + .Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist && (wildcard == null || wildcard.IsMatch(kvp.Key))) + .Select(kvp => kvp.Key)) + .Distinct(); + } + + public Stream OpenFile(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + { + return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path); + } + + return Inner.OpenFile(path); + } + + public void DeleteFile(string path) + { + if (FileExists(path) == false) + { + return; + } + + Nodes[NormPath(path)] = new ShadowNode(true, false); + } + + public bool FileExists(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + { + return sf.IsFile && sf.IsExist; + } + + return Inner.FileExists(path); + } + + public string GetRelativePath(string fullPathOrUrl) => Inner.GetRelativePath(fullPathOrUrl); + + public string GetFullPath(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + { + return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path); + } + + return Inner.GetFullPath(path); + } + + public string GetUrl(string? path) => Inner.GetUrl(path); + + public DateTimeOffset GetLastModified(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false) + { + return Inner.GetLastModified(path); + } + + if (sf.IsDelete) + { + throw new InvalidOperationException("Invalid path."); + } + + return _sfs.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false) + { + return Inner.GetCreated(path); + } + + if (sf.IsDelete) + { + throw new InvalidOperationException("Invalid path."); + } + + return _sfs.GetCreated(path); + } + + public long GetSize(string path) + { + if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false) + { + return Inner.GetSize(path); + } + + if (sf.IsDelete || sf.IsDir) + { + throw new InvalidOperationException("Invalid path."); + } + + return _sfs.GetSize(path); + } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out ShadowNode? sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + { + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + } + + var parts = normPath.Split(Constants.CharArrays.ForwardSlash); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + if (Nodes.TryGetValue(dirPath, out ShadowNode? sd)) + { + if (sd.IsFile) + { + throw new InvalidOperationException("Invalid path."); + } + + if (sd.IsDelete) { - if (_fs.DirectoryExists(dirPath)) continue; - if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); Nodes[dirPath] = new ShadowNode(false, true); } } - - _sfs.AddFile(path, stream, overrideIfExists); - Nodes[normPath] = new ShadowNode(false, false); - } - - public IEnumerable GetFiles(string path) - { - return GetFiles(path, null); - } - - public IEnumerable GetFiles(string path, string? filter) - { - var normPath = NormPath(path); - var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); - var files = filter != null ? _fs.GetFiles(path, filter) : _fs.GetFiles(path); - var wildcard = filter == null ? null : new WildcardExpression(filter); - return files - .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) - .Select(kvp => kvp.Key)) - .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist && (wildcard == null || wildcard.IsMatch(kvp.Key))).Select(kvp => kvp.Key)) - .Distinct(); - } - - public Stream OpenFile(string path) - { - if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf)) + else { - return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path); - } - - return _fs.OpenFile(path); - } - - public void DeleteFile(string path) - { - if (FileExists(path) == false) return; - Nodes[NormPath(path)] = new ShadowNode(true, false); - } - - public bool FileExists(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsFile && sf.IsExist; - return _fs.FileExists(path); - } - - public string GetRelativePath(string fullPathOrUrl) - { - return _fs.GetRelativePath(fullPathOrUrl); - } - - public string GetFullPath(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf)) - return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path); - return _fs.GetFullPath(path); - } - - public string GetUrl(string? path) - { - return _fs.GetUrl(path); - } - - public DateTimeOffset GetLastModified(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetLastModified(path); - if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); - return _sfs.GetLastModified(path); - } - - public DateTimeOffset GetCreated(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetCreated(path); - if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); - return _sfs.GetCreated(path); - } - - public long GetSize(string path) - { - ShadowNode? sf; - if (Nodes.TryGetValue(NormPath(path), out sf) == false) - return _fs.GetSize(path); - - if (sf.IsDelete || sf.IsDir) throw new InvalidOperationException("Invalid path."); - return _sfs.GetSize(path); - } - - public bool CanAddPhysical { get { return true; } } - - public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) - { - ShadowNode? sf; - var normPath = NormPath(path); - if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) - throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - - var parts = normPath.Split(Constants.CharArrays.ForwardSlash); - for (var i = 0; i < parts.Length - 1; i++) - { - var dirPath = string.Join("/", parts.Take(i + 1)); - ShadowNode? sd; - if (Nodes.TryGetValue(dirPath, out sd)) + if (Inner.DirectoryExists(dirPath)) { - if (sd.IsFile) throw new InvalidOperationException("Invalid path."); - if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + continue; } - else + + if (Inner.FileExists(dirPath)) { - if (_fs.DirectoryExists(dirPath)) continue; - if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); - Nodes[dirPath] = new ShadowNode(false, true); + throw new InvalidOperationException("Invalid path."); + } + + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, physicalPath, overrideIfExists, copy); + Nodes[normPath] = new ShadowNode(false, false); + } + + public void Complete() + { + if (_nodes == null) + { + return; + } + + var exceptions = new List(); + foreach (KeyValuePair kvp in _nodes) + { + if (kvp.Value.IsExist) + { + if (kvp.Value.IsFile) + { + try + { + if (Inner.CanAddPhysical) + { + Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move + } + else + { + using (Stream stream = _sfs.OpenFile(kvp.Key)) + { + Inner.AddFile(kvp.Key, stream, true); + } + } + } + catch (Exception e) + { + exceptions.Add(new Exception("Could not save file \"" + kvp.Key + "\".", e)); + } + } + } + else + { + try + { + if (kvp.Value.IsDir) + { + Inner.DeleteDirectory(kvp.Key, true); + } + else + { + Inner.DeleteFile(kvp.Key); + } + } + catch (Exception e) + { + exceptions.Add(new Exception( + "Could not delete " + (kvp.Value.IsDir ? "directory" : "file") + " \"" + kvp.Key + "\".", e)); } } - - _sfs.AddFile(path, physicalPath, overrideIfExists, copy); - Nodes[normPath] = new ShadowNode(false, false); } - // copied from System.Web.Util.Wildcard internal - internal class WildcardExpression + _nodes.Clear(); + + if (exceptions.Count == 0) { - private readonly string _pattern; - private readonly bool _caseInsensitive; - private Regex? _regex; + return; + } - private static Regex metaRegex = new Regex("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]"); - private static Regex questRegex = new Regex("\\?"); - private static Regex starRegex = new Regex("\\*"); - private static Regex commaRegex = new Regex(","); - private static Regex slashRegex = new Regex("(?=/)"); - private static Regex backslashRegex = new Regex("(?=[\\\\:])"); + throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + } - public WildcardExpression(string pattern, bool caseInsensitive = true) + private static string NormPath(string path) => path.ToLowerInvariant().Replace("\\", "/"); + + // values can be "" (root), "foo", "foo/bar"... + private static bool IsChild(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + { + return false; + } + + if (path.Length > 0 && input[path.Length] != '/') + { + return false; + } + + var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase); + return pos < 0; + } + + private static bool IsDescendant(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + { + return false; + } + + return path.Length == 0 || input[path.Length] == '/'; + } + + private void Delete(string path, bool recurse) + { + foreach (var file in Inner.GetFiles(path)) + { + Nodes[NormPath(file)] = new ShadowNode(true, false); + } + + foreach (var dir in Inner.GetDirectories(path)) + { + Nodes[NormPath(dir)] = new ShadowNode(true, true); + if (recurse) { - _pattern = pattern; - _caseInsensitive = caseInsensitive; - } - - private void EnsureRegex(string pattern) - { - if (_regex != null) return; - - var options = RegexOptions.None; - - // match right-to-left (for speed) if the pattern starts with a * - - if (pattern.Length > 0 && pattern[0] == '*') - options = RegexOptions.RightToLeft | RegexOptions.Singleline; - else - options = RegexOptions.Singleline; - - // case insensitivity - - if (_caseInsensitive) - options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; - - // Remove regex metacharacters - - pattern = metaRegex.Replace(pattern, "\\$0"); - - // Replace wildcard metacharacters with regex codes - - pattern = questRegex.Replace(pattern, "."); - pattern = starRegex.Replace(pattern, ".*"); - pattern = commaRegex.Replace(pattern, "\\z|\\A"); - - // anchor the pattern at beginning and end, and return the regex - - _regex = new Regex("\\A" + pattern + "\\z", options); - } - - public bool IsMatch(string input) - { - EnsureRegex(_pattern); - return _regex?.IsMatch(input) ?? false; + Delete(dir, true); } } } + + // copied from System.Web.Util.Wildcard internal + internal class WildcardExpression + { + private static readonly Regex MetaRegex = new("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]"); + private static readonly Regex QuestRegex = new("\\?"); + private static readonly Regex StarRegex = new("\\*"); + private static readonly Regex CommaRegex = new(","); + private static readonly Regex SlashRegex = new("(?=/)"); + private static readonly Regex BackslashRegex = new("(?=[\\\\:])"); + private readonly bool _caseInsensitive; + private readonly string _pattern; + private Regex? _regex; + + public WildcardExpression(string pattern, bool caseInsensitive = true) + { + _pattern = pattern; + _caseInsensitive = caseInsensitive; + } + + public bool IsMatch(string input) + { + EnsureRegex(_pattern); + return _regex?.IsMatch(input) ?? false; + } + + private void EnsureRegex(string pattern) + { + if (_regex != null) + { + return; + } + + RegexOptions options = RegexOptions.None; + + // match right-to-left (for speed) if the pattern starts with a * + if (pattern.Length > 0 && pattern[0] == '*') + { + options = RegexOptions.RightToLeft | RegexOptions.Singleline; + } + else + { + options = RegexOptions.Singleline; + } + + // case insensitivity + if (_caseInsensitive) + { + options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; + } + + // Remove regex metacharacters + pattern = MetaRegex.Replace(pattern, "\\$0"); + + // Replace wildcard metacharacters with regex codes + pattern = QuestRegex.Replace(pattern, "."); + pattern = StarRegex.Replace(pattern, ".*"); + pattern = CommaRegex.Replace(pattern, "\\z|\\A"); + + // anchor the pattern at beginning and end, and return the regex + _regex = new Regex("\\A" + pattern + "\\z", options); + } + } + + private class ShadowNode + { + public ShadowNode(bool isDelete, bool isdir) + { + IsDelete = isDelete; + IsDir = isdir; + } + + public bool IsDelete { get; } + + public bool IsDir { get; } + + public bool IsExist => IsDelete == false; + + public bool IsFile => IsDir == false; + } } diff --git a/src/Umbraco.Core/IO/ShadowFileSystems.cs b/src/Umbraco.Core/IO/ShadowFileSystems.cs index 413cc73d8a..3d69875dc4 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystems.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystems.cs @@ -1,34 +1,26 @@ -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +// shadow filesystems is definitively ... too convoluted +internal class ShadowFileSystems : ICompletable { - // shadow filesystems is definitively ... too convoluted + private readonly FileSystems _fileSystems; + private bool _completed; - internal class ShadowFileSystems : ICompletable + // invoked by the filesystems when shadowing + public ShadowFileSystems(FileSystems fileSystems, string id) { - private readonly FileSystems _fileSystems; - private bool _completed; + _fileSystems = fileSystems; + Id = id; - // invoked by the filesystems when shadowing - public ShadowFileSystems(FileSystems fileSystems, string id) - { - _fileSystems = fileSystems; - Id = id; - - _fileSystems.BeginShadow(id); - } - - // for tests - public string Id { get; } - - // invoked by the scope when exiting, if completed - public void Complete() - { - _completed = true; - } - - // invoked by the scope when exiting - public void Dispose() - { - _fileSystems.EndShadow(Id, _completed); - } + _fileSystems.BeginShadow(id); } + + // for tests + public string Id { get; } + + // invoked by the scope when exiting, if completed + public void Complete() => _completed = true; + + // invoked by the scope when exiting + public void Dispose() => _fileSystems.EndShadow(Id, _completed); } diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 67808e1fdb..2f2e0a991c 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -1,233 +1,186 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +internal class ShadowWrapper : IFileSystem, IFileProviderFactory { - internal class ShadowWrapper : IFileSystem, IFileProviderFactory + private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIOHelper _ioHelper; + + private readonly Func? _isScoped; + private readonly ILoggerFactory _loggerFactory; + private readonly string _shadowPath; + private string? _shadowDir; + private ShadowFileSystem? _shadowFileSystem; + + public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func? isScoped = null) { - private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"; + InnerFileSystem = innerFileSystem; + _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _loggerFactory = loggerFactory; + _shadowPath = shadowPath; + _isScoped = isScoped; + } - private readonly Func? _isScoped; - private readonly IFileSystem _innerFileSystem; - private readonly string _shadowPath; - private ShadowFileSystem? _shadowFileSystem; - private string? _shadowDir; - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILoggerFactory _loggerFactory; + public IFileSystem InnerFileSystem { get; } - public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func? isScoped = null) + public bool CanAddPhysical => FileSystem.CanAddPhysical; + + private IFileSystem FileSystem + { + get { - _innerFileSystem = innerFileSystem; - _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _loggerFactory = loggerFactory; - _shadowPath = shadowPath; - _isScoped = isScoped; - } - - public static string CreateShadowId(IHostingEnvironment hostingEnvironment) - { - const int retries = 50; // avoid infinite loop - const int idLength = 8; // 6 chars - - // shorten a Guid to idLength chars, and see whether it collides - // with an existing directory or not - if it does, try again, and - // we should end up with a unique identifier eventually - but just - // detect infinite loops (just in case) - - for (var i = 0; i < retries; i++) + if (_isScoped is not null && _shadowFileSystem is not null) { - var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength); + var isScoped = _isScoped!(); - var virt = ShadowFsPath + "/" + id; - var shadowDir = hostingEnvironment.MapPathContentRoot(virt); - if (Directory.Exists(shadowDir)) - continue; + // if the filesystem is created *after* shadowing starts, it won't be shadowing + // better not ignore that situation and raised a meaningful (?) exception + if (isScoped.HasValue && isScoped.Value && _shadowFileSystem == null) + { + throw new Exception("The filesystems are shadowing, but this filesystem is not."); + } - Directory.CreateDirectory(shadowDir); - return id; + return isScoped.HasValue && isScoped.Value + ? _shadowFileSystem + : InnerFileSystem; } - throw new Exception($"Could not get a shadow identifier (tried {retries} times)"); + return InnerFileSystem; + } + } + + /// + public IFileProvider? Create() => + InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null; + + public IEnumerable GetDirectories(string path) => FileSystem.GetDirectories(path); + + public void DeleteDirectory(string path) => FileSystem.DeleteDirectory(path); + + public void DeleteDirectory(string path, bool recursive) => FileSystem.DeleteDirectory(path, recursive); + + public bool DirectoryExists(string path) => FileSystem.DirectoryExists(path); + + public void AddFile(string path, Stream stream) => FileSystem.AddFile(path, stream); + + public void AddFile(string path, Stream stream, bool overrideExisting) => + FileSystem.AddFile(path, stream, overrideExisting); + + public IEnumerable GetFiles(string path) => FileSystem.GetFiles(path); + + public IEnumerable GetFiles(string path, string filter) => FileSystem.GetFiles(path, filter); + + public Stream OpenFile(string path) => FileSystem.OpenFile(path); + + public void DeleteFile(string path) => FileSystem.DeleteFile(path); + + public bool FileExists(string path) => FileSystem.FileExists(path); + + public string GetRelativePath(string fullPathOrUrl) => FileSystem.GetRelativePath(fullPathOrUrl); + + public string GetFullPath(string path) => FileSystem.GetFullPath(path); + + public string GetUrl(string? path) => FileSystem.GetUrl(path); + + public DateTimeOffset GetLastModified(string path) => FileSystem.GetLastModified(path); + + public DateTimeOffset GetCreated(string path) => FileSystem.GetCreated(path); + + public long GetSize(string path) => FileSystem.GetSize(path); + + public static string CreateShadowId(IHostingEnvironment hostingEnvironment) + { + const int retries = 50; // avoid infinite loop + const int idLength = 8; // 6 chars + + // shorten a Guid to idLength chars, and see whether it collides + // with an existing directory or not - if it does, try again, and + // we should end up with a unique identifier eventually - but just + // detect infinite loops (just in case) + for (var i = 0; i < retries; i++) + { + var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength); + + var virt = ShadowFsPath + "/" + id; + var shadowDir = hostingEnvironment.MapPathContentRoot(virt); + if (Directory.Exists(shadowDir)) + { + continue; + } + + Directory.CreateDirectory(shadowDir); + return id; } - internal void Shadow(string id) - { - // note: no thread-safety here, because ShadowFs is thread-safe due to the check - // on ShadowFileSystemsScope.None - and if None is false then we should be running - // in a single thread anyways + throw new Exception($"Could not get a shadow identifier (tried {retries} times)"); + } - var virt = Path.Combine(ShadowFsPath , id , _shadowPath); - _shadowDir = _hostingEnvironment.MapPathContentRoot(virt); - Directory.CreateDirectory(_shadowDir); - var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger(), _shadowDir, _hostingEnvironment.ToAbsolute(virt)); - _shadowFileSystem = new ShadowFileSystem(_innerFileSystem, tempfs); + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) => + FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); + + internal void Shadow(string id) + { + // note: no thread-safety here, because ShadowFs is thread-safe due to the check + // on ShadowFileSystemsScope.None - and if None is false then we should be running + // in a single thread anyways + var virt = Path.Combine(ShadowFsPath, id, _shadowPath); + _shadowDir = _hostingEnvironment.MapPathContentRoot(virt); + Directory.CreateDirectory(_shadowDir); + var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger(), _shadowDir, _hostingEnvironment.ToAbsolute(virt)); + _shadowFileSystem = new ShadowFileSystem(InnerFileSystem, tempfs); + } + + internal void UnShadow(bool complete) + { + ShadowFileSystem? shadowFileSystem = _shadowFileSystem; + var dir = _shadowDir; + _shadowFileSystem = null; + _shadowDir = null; + + try + { + // this may throw an AggregateException if some of the changes could not be applied + if (complete) + { + shadowFileSystem?.Complete(); + } } - - internal void UnShadow(bool complete) + finally { - var shadowFileSystem = _shadowFileSystem; - var dir = _shadowDir; - _shadowFileSystem = null; - _shadowDir = null; - + // in any case, cleanup try { - // this may throw an AggregateException if some of the changes could not be applied - if (complete) shadowFileSystem?.Complete(); - } - finally - { - // in any case, cleanup - try - { - Directory.Delete(dir!, true); + Directory.Delete(dir!, true); - // shadowPath make be path/to/dir, remove each - dir = dir!.Replace('/', Path.DirectorySeparatorChar); - var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length; - var pos = dir.LastIndexOf(Path.DirectorySeparatorChar); - while (pos > min) + // shadowPath make be path/to/dir, remove each + dir = dir!.Replace('/', Path.DirectorySeparatorChar); + var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length; + var pos = dir.LastIndexOf(Path.DirectorySeparatorChar); + while (pos > min) + { + dir = dir.Substring(0, pos); + if (Directory.EnumerateFileSystemEntries(dir).Any() == false) { - dir = dir.Substring(0, pos); - if (Directory.EnumerateFileSystemEntries(dir).Any() == false) - Directory.Delete(dir, true); - else - break; - pos = dir.LastIndexOf(Path.DirectorySeparatorChar); + Directory.Delete(dir, true); } - } - catch - { - // ugly, isn't it? but if we cannot cleanup, bah, just leave it there + else + { + break; + } + + pos = dir.LastIndexOf(Path.DirectorySeparatorChar); } } - } - - public IFileSystem InnerFileSystem => _innerFileSystem; - - private IFileSystem FileSystem - { - get + catch { - if (_isScoped is not null && _shadowFileSystem is not null) - { - var isScoped = _isScoped!(); - - // if the filesystem is created *after* shadowing starts, it won't be shadowing - // better not ignore that situation and raised a meaningful (?) exception - if ( isScoped.HasValue && isScoped.Value && _shadowFileSystem == null) - throw new Exception("The filesystems are shadowing, but this filesystem is not."); - - return isScoped.HasValue && isScoped.Value - ? _shadowFileSystem - : _innerFileSystem; - } - - return _innerFileSystem; + // ugly, isn't it? but if we cannot cleanup, bah, just leave it there } } - - public IEnumerable GetDirectories(string path) - { - return FileSystem.GetDirectories(path); - } - - public void DeleteDirectory(string path) - { - FileSystem.DeleteDirectory(path); - } - - public void DeleteDirectory(string path, bool recursive) - { - FileSystem.DeleteDirectory(path, recursive); - } - - public bool DirectoryExists(string path) - { - return FileSystem.DirectoryExists(path); - } - - public void AddFile(string path, Stream stream) - { - FileSystem.AddFile(path, stream); - } - - public void AddFile(string path, Stream stream, bool overrideExisting) - { - FileSystem.AddFile(path, stream, overrideExisting); - } - - public IEnumerable GetFiles(string path) - { - return FileSystem.GetFiles(path); - } - - public IEnumerable GetFiles(string path, string filter) - { - return FileSystem.GetFiles(path, filter); - } - - public Stream OpenFile(string path) - { - return FileSystem.OpenFile(path); - } - - public void DeleteFile(string path) - { - FileSystem.DeleteFile(path); - } - - public bool FileExists(string path) - { - return FileSystem.FileExists(path); - } - - public string GetRelativePath(string fullPathOrUrl) - { - return FileSystem.GetRelativePath(fullPathOrUrl); - } - - public string GetFullPath(string path) - { - return FileSystem.GetFullPath(path); - } - - public string GetUrl(string? path) - { - return FileSystem.GetUrl(path); - } - - public DateTimeOffset GetLastModified(string path) - { - return FileSystem.GetLastModified(path); - } - - public DateTimeOffset GetCreated(string path) - { - return FileSystem.GetCreated(path); - } - - public long GetSize(string path) - { - return FileSystem.GetSize(path); - } - - public bool CanAddPhysical => FileSystem.CanAddPhysical; - - public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) - { - FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); - } - - /// - public IFileProvider? Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null; } } diff --git a/src/Umbraco.Core/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs index 9bf87c3407..e2502e4669 100644 --- a/src/Umbraco.Core/IO/ViewHelper.cs +++ b/src/Umbraco.Core/IO/ViewHelper.cs @@ -1,133 +1,130 @@ -using System; -using System.IO; -using System.Linq; using System.Text; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.IO +namespace Umbraco.Cms.Core.IO; + +public class ViewHelper : IViewHelper { - public class ViewHelper : IViewHelper + private readonly IDefaultViewContentProvider _defaultViewContentProvider; + private readonly IFileSystem _viewFileSystem; + + [Obsolete("Use ctor with all params")] + public ViewHelper(IFileSystem viewFileSystem) { - private readonly IFileSystem _viewFileSystem; - private readonly IDefaultViewContentProvider _defaultViewContentProvider; + _viewFileSystem = viewFileSystem ?? throw new ArgumentNullException(nameof(viewFileSystem)); + _defaultViewContentProvider = StaticServiceProvider.Instance.GetRequiredService(); + } - [Obsolete("Use ctor with all params")] - public ViewHelper(IFileSystem viewFileSystem) + public ViewHelper(FileSystems fileSystems, IDefaultViewContentProvider defaultViewContentProvider) + { + _viewFileSystem = fileSystems.MvcViewsFileSystem ?? throw new ArgumentNullException(nameof(fileSystems)); + _defaultViewContentProvider = defaultViewContentProvider ?? + throw new ArgumentNullException(nameof(defaultViewContentProvider)); + } + + [Obsolete("Inject IDefaultViewContentProvider instead")] + public static string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null) + { + IDefaultViewContentProvider viewContentProvider = + StaticServiceProvider.Instance.GetRequiredService(); + return viewContentProvider.GetDefaultFileContent(layoutPageAlias, modelClassName, modelNamespace, modelNamespaceAlias); + } + + public bool ViewExists(ITemplate t) => t.Alias is not null && _viewFileSystem.FileExists(ViewPath(t.Alias)); + + public string GetFileContents(ITemplate t) + { + var viewContent = string.Empty; + var path = ViewPath(t.Alias ?? string.Empty); + + if (_viewFileSystem.FileExists(path)) { - _viewFileSystem = viewFileSystem ?? throw new ArgumentNullException(nameof(viewFileSystem)); - _defaultViewContentProvider = StaticServiceProvider.Instance.GetRequiredService(); - } - - public ViewHelper(FileSystems fileSystems, IDefaultViewContentProvider defaultViewContentProvider) - { - _viewFileSystem = fileSystems.MvcViewsFileSystem ?? throw new ArgumentNullException(nameof(fileSystems)); - _defaultViewContentProvider = defaultViewContentProvider ?? throw new ArgumentNullException(nameof(defaultViewContentProvider)); - } - - public bool ViewExists(ITemplate t) => t.Alias is not null && _viewFileSystem.FileExists(ViewPath(t.Alias)); - - - public string GetFileContents(ITemplate t) - { - var viewContent = ""; - var path = ViewPath(t.Alias ?? string.Empty); - - if (_viewFileSystem.FileExists(path)) + using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) { - using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) - { - viewContent = tr.ReadToEnd(); - tr.Close(); - } + viewContent = tr.ReadToEnd(); + tr.Close(); } - - return viewContent; } - public string CreateView(ITemplate t, bool overWrite = false) - { - string viewContent; - var path = ViewPath(t.Alias); + return viewContent; + } - if (_viewFileSystem.FileExists(path) == false || overWrite) + public string CreateView(ITemplate t, bool overWrite = false) + { + string viewContent; + var path = ViewPath(t.Alias); + + if (_viewFileSystem.FileExists(path) == false || overWrite) + { + viewContent = SaveTemplateToFile(t); + } + else + { + using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) { - viewContent = SaveTemplateToFile(t); + viewContent = tr.ReadToEnd(); + tr.Close(); } - else + } + + return viewContent; + } + + public string? UpdateViewFile(ITemplate t, string? currentAlias = null) + { + var path = ViewPath(t.Alias); + + if (string.IsNullOrEmpty(currentAlias) == false && currentAlias != t.Alias) + { + // then kill the old file.. + var oldFile = ViewPath(currentAlias); + if (_viewFileSystem.FileExists(oldFile)) { - using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) - { - viewContent = tr.ReadToEnd(); - tr.Close(); - } + _viewFileSystem.DeleteFile(oldFile); } - - return viewContent; } - [Obsolete("Inject IDefaultViewContentProvider instead")] - public static string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, - string? modelNamespace = null, string? modelNamespaceAlias = null) + var data = Encoding.UTF8.GetBytes(t.Content ?? string.Empty); + var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + + using (var ms = new MemoryStream(withBom)) { - var viewContentProvider = StaticServiceProvider.Instance.GetRequiredService(); - return viewContentProvider.GetDefaultFileContent(layoutPageAlias, modelClassName, modelNamespace, - modelNamespaceAlias); + _viewFileSystem.AddFile(path, ms, true); } - private string SaveTemplateToFile(ITemplate template) + return t.Content; + } + + public string ViewPath(string alias) => _viewFileSystem.GetRelativePath(alias.Replace(" ", string.Empty) + ".cshtml"); + + private string SaveTemplateToFile(ITemplate template) + { + var design = template.Content.IsNullOrWhiteSpace() ? EnsureInheritedLayout(template) : template.Content!; + var path = ViewPath(template.Alias); + + var data = Encoding.UTF8.GetBytes(design); + var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + + using (var ms = new MemoryStream(withBom)) { - var design = template.Content.IsNullOrWhiteSpace() ? EnsureInheritedLayout(template) : template.Content!; - var path = ViewPath(template.Alias); - - var data = Encoding.UTF8.GetBytes(design); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); - - using (var ms = new MemoryStream(withBom)) - { - _viewFileSystem.AddFile(path, ms, true); - } - - return design; + _viewFileSystem.AddFile(path, ms, true); } - public string? UpdateViewFile(ITemplate t, string? currentAlias = null) + return design; + } + + private string EnsureInheritedLayout(ITemplate template) + { + var design = template.Content; + + if (string.IsNullOrEmpty(design)) { - var path = ViewPath(t.Alias); - - if (string.IsNullOrEmpty(currentAlias) == false && currentAlias != t.Alias) - { - //then kill the old file.. - var oldFile = ViewPath(currentAlias); - if (_viewFileSystem.FileExists(oldFile)) - _viewFileSystem.DeleteFile(oldFile); - } - - var data = Encoding.UTF8.GetBytes(t.Content ?? string.Empty); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); - - using (var ms = new MemoryStream(withBom)) - { - _viewFileSystem.AddFile(path, ms, true); - } - return t.Content; + design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias); } - public string ViewPath(string alias) - { - return _viewFileSystem.GetRelativePath(alias.Replace(" ", "") + ".cshtml"); - } - - private string EnsureInheritedLayout(ITemplate template) - { - var design = template.Content; - - if (string.IsNullOrEmpty(design)) - design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias); - - return design; - } + return design; } } diff --git a/src/Umbraco.Core/IRegisteredObject.cs b/src/Umbraco.Core/IRegisteredObject.cs index 54ac6e1a57..103e10dab1 100644 --- a/src/Umbraco.Core/IRegisteredObject.cs +++ b/src/Umbraco.Core/IRegisteredObject.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public interface IRegisteredObject { - public interface IRegisteredObject - { - void Stop(bool immediate); - } + void Stop(bool immediate); } diff --git a/src/Umbraco.Core/Install/FilePermissionTest.cs b/src/Umbraco.Core/Install/FilePermissionTest.cs index f84d9a0a7b..21c6d4f0c7 100644 --- a/src/Umbraco.Core/Install/FilePermissionTest.cs +++ b/src/Umbraco.Core/Install/FilePermissionTest.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Install +namespace Umbraco.Cms.Core.Install; + +public enum FilePermissionTest { - public enum FilePermissionTest - { - FolderCreation, - FileWritingForPackages, - FileWriting, - MediaFolderCreation - } + FolderCreation, + FileWritingForPackages, + FileWriting, + MediaFolderCreation, } diff --git a/src/Umbraco.Core/Install/IFilePermissionHelper.cs b/src/Umbraco.Core/Install/IFilePermissionHelper.cs index cfda3a396d..88b6e23cbf 100644 --- a/src/Umbraco.Core/Install/IFilePermissionHelper.cs +++ b/src/Umbraco.Core/Install/IFilePermissionHelper.cs @@ -1,20 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Install; -namespace Umbraco.Cms.Core.Install +/// +/// Helper to test File and folder permissions +/// +public interface IFilePermissionHelper { /// - /// Helper to test File and folder permissions + /// Run all tests for permissions of the required files and folders. /// - public interface IFilePermissionHelper - { - /// - /// Run all tests for permissions of the required files and folders. - /// - /// True if all permissions are correct. False otherwise. - bool RunFilePermissionTestSuite(out Dictionary> report); - - } + /// True if all permissions are correct. False otherwise. + bool RunFilePermissionTestSuite(out Dictionary> report); } diff --git a/src/Umbraco.Core/Install/InstallException.cs b/src/Umbraco.Core/Install/InstallException.cs index 2ec741d200..69e28db92c 100644 --- a/src/Umbraco.Core/Install/InstallException.cs +++ b/src/Umbraco.Core/Install/InstallException.cs @@ -1,106 +1,122 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install +namespace Umbraco.Cms.Core.Install; + +/// +/// Used for steps to be able to return a JSON structure back to the UI. +/// +/// +[Serializable] +public class InstallException : Exception { /// - /// Used for steps to be able to return a JSON structure back to the UI. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class InstallException : Exception + public InstallException() { - /// - /// Gets the view. - /// - /// - /// The view. - /// - public string? View { get; private set; } + } - /// - /// Gets the view model. - /// - /// - /// The view model. - /// - /// - /// This object is not included when serializing. - /// - public object? ViewModel { get; private set; } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public InstallException(string message) + : this(message, "error", null) + { + } - /// - /// Initializes a new instance of the class. - /// - public InstallException() - { } + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The view model. + public InstallException(string message, object viewModel) + : this(message, "error", viewModel) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public InstallException(string message) - : this(message, "error", null) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The view. + /// The view model. + public InstallException(string message, string view, object? viewModel) + : base(message) + { + View = view; + ViewModel = viewModel; + } - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The view model. - public InstallException(string message, object viewModel) - : this(message, "error", viewModel) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public InstallException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The view. - /// The view model. - public InstallException(string message, string view, object? viewModel) - : base(message) + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected InstallException(SerializationInfo info, StreamingContext context) + : base(info, context) => + View = info.GetString(nameof(View)); + + /// + /// Gets the view. + /// + /// + /// The view. + /// + public string? View { get; private set; } + + /// + /// Gets the view model. + /// + /// + /// The view model. + /// + /// + /// This object is not included when serializing. + /// + public object? ViewModel { get; private set; } + + /// + /// When overridden in a derived class, sets the with + /// information about the exception. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// info + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) { - View = view; - ViewModel = viewModel; + throw new ArgumentNullException(nameof(info)); } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public InstallException(string message, Exception innerException) - : base(message, innerException) - { } + info.AddValue(nameof(View), View); - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected InstallException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - View = info.GetString(nameof(View)); - } - - /// - /// When overridden in a derived class, sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// info - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - info.AddValue(nameof(View), View); - - base.GetObjectData(info, context); - } + base.GetObjectData(info, context); } } diff --git a/src/Umbraco.Core/Install/InstallStatusTracker.cs b/src/Umbraco.Core/Install/InstallStatusTracker.cs index 1ee7f685d4..5403ded3ae 100644 --- a/src/Umbraco.Core/Install/InstallStatusTracker.cs +++ b/src/Umbraco.Core/Install/InstallStatusTracker.cs @@ -1,74 +1,104 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Install +namespace Umbraco.Cms.Core.Install; + +/// +/// An internal in-memory status tracker for the current installation +/// +public class InstallStatusTracker { - /// - /// An internal in-memory status tracker for the current installation - /// - public class InstallStatusTracker + private static ConcurrentHashSet _steps = new(); + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IJsonSerializer _jsonSerializer; + + public InstallStatusTracker(IHostingEnvironment hostingEnvironment, IJsonSerializer jsonSerializer) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IJsonSerializer _jsonSerializer; + _hostingEnvironment = hostingEnvironment; + _jsonSerializer = jsonSerializer; + } - public InstallStatusTracker(IHostingEnvironment hostingEnvironment, IJsonSerializer jsonSerializer) + public static IEnumerable GetStatus() => + new List(_steps).OrderBy(x => x.ServerOrder); + + public void Reset() + { + _steps = new ConcurrentHashSet(); + ClearFiles(); + } + + private string GetFile(Guid installId) + { + var file = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + + "Install/" + + "install_" + + installId.ToString("N") + + ".txt"); + return file; + } + + public void ClearFiles() + { + var dir = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + + "Install/"); + if (Directory.Exists(dir)) { - _hostingEnvironment = hostingEnvironment; - _jsonSerializer = jsonSerializer; - } - - private static ConcurrentHashSet _steps = new ConcurrentHashSet(); - - private string GetFile(Guid installId) - { - var file = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "Install/" - + "install_" - + installId.ToString("N") - + ".txt"); - return file; - } - - public void Reset() - { - _steps = new ConcurrentHashSet(); - ClearFiles(); - } - - public void ClearFiles() - { - var dir = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "Install/"); - if (Directory.Exists(dir)) + var files = Directory.GetFiles(dir); + foreach (var f in files) { - var files = Directory.GetFiles(dir); - foreach (var f in files) + File.Delete(f); + } + } + else + { + Directory.CreateDirectory(dir); + } + } + + public IEnumerable InitializeFromFile(Guid installId) + { + // check if we have our persisted file and read it + var file = GetFile(installId); + if (File.Exists(file)) + { + IEnumerable? deserialized = + _jsonSerializer.Deserialize>( + File.ReadAllText(file)); + if (deserialized is not null) + { + foreach (InstallTrackingItem item in deserialized) { - File.Delete(f); + _steps.Add(item); } } - else - { - Directory.CreateDirectory(dir); - } + } + else + { + throw new InvalidOperationException("Cannot initialize from file, the installation file with id " + + installId + " does not exist"); } - public IEnumerable InitializeFromFile(Guid installId) + return new List(_steps); + } + + public IEnumerable Initialize(Guid installId, IEnumerable steps) + { + // if there are no steps in memory + if (_steps.Count == 0) { - //check if we have our persisted file and read it + // check if we have our persisted file and read it var file = GetFile(installId); if (File.Exists(file)) { - var deserialized = _jsonSerializer.Deserialize>( - File.ReadAllText(file)); + IEnumerable? deserialized = + _jsonSerializer.Deserialize>( + File.ReadAllText(file)); if (deserialized is not null) { - foreach (var item in deserialized) + foreach (InstallTrackingItem item in deserialized) { _steps.Add(item); } @@ -76,81 +106,51 @@ namespace Umbraco.Cms.Core.Install } else { - throw new InvalidOperationException("Cannot initialize from file, the installation file with id " + installId + " does not exist"); + ClearFiles(); + + // otherwise just create the steps in memory (brand new install) + foreach (InstallSetupStep step in steps.OrderBy(x => x.ServerOrder)) + { + _steps.Add(new InstallTrackingItem(step.Name, step.ServerOrder)); + } + + // save the file + var serialized = _jsonSerializer.Serialize(new List(_steps)); + Directory.CreateDirectory(Path.GetDirectoryName(file)!); + File.WriteAllText(file, serialized); } - return new List(_steps); } - - public IEnumerable Initialize(Guid installId, IEnumerable steps) + else { - //if there are no steps in memory - if (_steps.Count == 0) - { - //check if we have our persisted file and read it - var file = GetFile(installId); - if (File.Exists(file)) - { - var deserialized = _jsonSerializer.Deserialize>( - File.ReadAllText(file)); - if (deserialized is not null) - { - foreach (var item in deserialized) - { - _steps.Add(item); - } - } - } - else - { - ClearFiles(); - - //otherwise just create the steps in memory (brand new install) - foreach (var step in steps.OrderBy(x => x.ServerOrder)) - { - _steps.Add(new InstallTrackingItem(step.Name, step.ServerOrder)); - } - //save the file - var serialized = _jsonSerializer.Serialize(new List(_steps)); - Directory.CreateDirectory(Path.GetDirectoryName(file)!); - File.WriteAllText(file, serialized); - } - } - else - { - //ensure that the file exists with the current install id - var file = GetFile(installId); - if (File.Exists(file) == false) - { - ClearFiles(); - - //save the correct file - var serialized = _jsonSerializer.Serialize(new List(_steps)); - Directory.CreateDirectory(Path.GetDirectoryName(file)!); - File.WriteAllText(file, serialized); - } - } - - return new List(_steps); - } - - public void SetComplete(Guid installId, string name, IDictionary? additionalData = null) - { - var trackingItem = _steps.Single(x => x.Name == name); - if (additionalData != null) - { - trackingItem.AdditionalData = additionalData; - } - trackingItem.IsComplete = true; - - //save the file + // ensure that the file exists with the current install id var file = GetFile(installId); - var serialized = _jsonSerializer.Serialize(new List(_steps)); - File.WriteAllText(file, serialized); + if (File.Exists(file) == false) + { + ClearFiles(); + + // save the correct file + var serialized = _jsonSerializer.Serialize(new List(_steps)); + Directory.CreateDirectory(Path.GetDirectoryName(file)!); + File.WriteAllText(file, serialized); + } } - public static IEnumerable GetStatus() + return new List(_steps); + } + + public void SetComplete(Guid installId, string name, IDictionary? additionalData = null) + { + InstallTrackingItem trackingItem = _steps.Single(x => x.Name == name); + if (additionalData != null) { - return new List(_steps).OrderBy(x => x.ServerOrder); + trackingItem.AdditionalData = additionalData; } + + trackingItem.IsComplete = true; + + // save the file + var file = GetFile(installId); + var serialized = _jsonSerializer.Serialize(new List(_steps)); + File.WriteAllText(file, serialized); } } diff --git a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs index 14d77ecb77..40f54bab33 100644 --- a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs @@ -1,56 +1,55 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Install.InstallSteps +namespace Umbraco.Cms.Core.Install.InstallSteps; + +/// +/// Represents a step in the installation that ensure all the required permissions on files and folders are correct. +/// +[InstallSetupStep( + InstallationType.NewInstall | InstallationType.Upgrade, + "Permissions", + 0, + "", + PerformsAppRestart = true)] +public class FilePermissionsStep : InstallSetupStep { + private readonly IFilePermissionHelper _filePermissionHelper; + private readonly ILocalizedTextService _localizedTextService; + /// - /// Represents a step in the installation that ensure all the required permissions on files and folders are correct. + /// Initializes a new instance of the class. /// - [InstallSetupStep( - InstallationType.NewInstall | InstallationType.Upgrade, - "Permissions", - 0, - "", - PerformsAppRestart = true)] - public class FilePermissionsStep : InstallSetupStep + public FilePermissionsStep( + IFilePermissionHelper filePermissionHelper, + ILocalizedTextService localizedTextService) { - private readonly IFilePermissionHelper _filePermissionHelper; - private readonly ILocalizedTextService _localizedTextService; - - /// - /// Initializes a new instance of the class. - /// - public FilePermissionsStep( - IFilePermissionHelper filePermissionHelper, - ILocalizedTextService localizedTextService) - { - _filePermissionHelper = filePermissionHelper; - _localizedTextService = localizedTextService; - } - - /// - public override Task ExecuteAsync(object model) - { - // validate file permissions - var permissionsOk = _filePermissionHelper.RunFilePermissionTestSuite(out Dictionary> report); - - var translatedErrors = report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value); - if (permissionsOk == false) - { - throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors }); - } - - return Task.FromResult(null); - } - - /// - public override bool RequiresExecution(object model) => true; + _filePermissionHelper = filePermissionHelper; + _localizedTextService = localizedTextService; } + + /// + public override Task ExecuteAsync(object model) + { + // validate file permissions + var permissionsOk = + _filePermissionHelper.RunFilePermissionTestSuite( + out Dictionary> report); + + var translatedErrors = + report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value); + if (permissionsOk == false) + { + throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors }); + } + + return Task.FromResult(null); + } + + /// + public override bool RequiresExecution(object model) => true; } diff --git a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs index 15286d249f..cb008bf77c 100644 --- a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -9,47 +7,49 @@ using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.Install.InstallSteps +namespace Umbraco.Cms.Core.Install.InstallSteps; + +[InstallSetupStep( + InstallationType.NewInstall | InstallationType.Upgrade, + "TelemetryIdConfiguration", + 0, + "", + PerformsAppRestart = false)] +public class TelemetryIdentifierStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "TelemetryIdConfiguration", 0, "", - PerformsAppRestart = false)] - public class TelemetryIdentifierStep : InstallSetupStep + private readonly IOptions _globalSettings; + private readonly ISiteIdentifierService _siteIdentifierService; + + public TelemetryIdentifierStep( + IOptions globalSettings, + ISiteIdentifierService siteIdentifierService) { - private readonly IOptions _globalSettings; - private readonly ISiteIdentifierService _siteIdentifierService; + _globalSettings = globalSettings; + _siteIdentifierService = siteIdentifierService; + } - public TelemetryIdentifierStep( - IOptions globalSettings, - ISiteIdentifierService siteIdentifierService) - { - _globalSettings = globalSettings; - _siteIdentifierService = siteIdentifierService; - } - - [Obsolete("Use constructor that takes GlobalSettings and ISiteIdentifierService")] - public TelemetryIdentifierStep( - ILogger logger, - IOptions globalSettings, - IConfigManipulator configManipulator) + [Obsolete("Use constructor that takes GlobalSettings and ISiteIdentifierService")] + public TelemetryIdentifierStep( + ILogger logger, + IOptions globalSettings, + IConfigManipulator configManipulator) : this(globalSettings, StaticServiceProvider.Instance.GetRequiredService()) - { - } + { + } - public override Task ExecuteAsync(object model) - { - _siteIdentifierService.TryCreateSiteIdentifier(out _); - return Task.FromResult(null); - } + public override Task ExecuteAsync(object model) + { + _siteIdentifierService.TryCreateSiteIdentifier(out _); + return Task.FromResult(null); + } - public override bool RequiresExecution(object model) - { - // Verify that Json value is not empty string - // Try & get a value stored in appSettings.json - var backofficeIdentifierRaw = _globalSettings.Value.Id; + public override bool RequiresExecution(object model) + { + // Verify that Json value is not empty string + // Try & get a value stored in appSettings.json + var backofficeIdentifierRaw = _globalSettings.Value.Id; - // No need to add Id again if already found - return string.IsNullOrEmpty(backofficeIdentifierRaw); - } + // No need to add Id again if already found + return string.IsNullOrEmpty(backofficeIdentifierRaw); } } diff --git a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs index 6530983de2..763b69226e 100644 --- a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Semver; @@ -31,16 +29,22 @@ namespace Umbraco.Cms.Core.Install.InstallSteps { string FormatGuidState(string? value) { - if (string.IsNullOrWhiteSpace(value)) value = "unknown"; - else if (Guid.TryParse(value, out var currentStateGuid)) + if (string.IsNullOrWhiteSpace(value)) + { + value = "unknown"; + } + else if (Guid.TryParse(value, out Guid currentStateGuid)) + { value = currentStateGuid.ToString("N").Substring(0, 8); + } + return value; } var currentState = FormatGuidState(_runtimeState.CurrentMigrationState); var newState = FormatGuidState(_runtimeState.FinalMigrationState); var newVersion = _umbracoVersion.SemanticVersion?.ToSemanticStringWithoutBuild(); - var oldVersion = new SemVersion(_umbracoVersion.SemanticVersion?.Major ?? 0, 0, 0).ToString(); //TODO can we find the old version somehow? e.g. from current state + var oldVersion = new SemVersion(_umbracoVersion.SemanticVersion?.Major ?? 0).ToString(); //TODO can we find the old version somehow? e.g. from current state var reportUrl = $"https://our.umbraco.com/contribute/releases/compare?from={oldVersion}&to={newVersion}¬es=1"; diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index da2f61fce5..eb892d9cee 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -1,33 +1,31 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +[DataContract(Name = "database", Namespace = "")] +public class DatabaseModel { - [DataContract(Name = "database", Namespace = "")] - public class DatabaseModel - { - [DataMember(Name = "databaseProviderMetadataId")] - public Guid DatabaseProviderMetadataId { get; set; } + [DataMember(Name = "databaseProviderMetadataId")] + public Guid DatabaseProviderMetadataId { get; set; } - [DataMember(Name = "providerName")] - public string? ProviderName { get; set; } + [DataMember(Name = "providerName")] + public string? ProviderName { get; set; } - [DataMember(Name = "server")] - public string Server { get; set; } = null!; + [DataMember(Name = "server")] + public string Server { get; set; } = null!; - [DataMember(Name = "databaseName")] - public string DatabaseName { get; set; } = null!; + [DataMember(Name = "databaseName")] + public string DatabaseName { get; set; } = null!; - [DataMember(Name = "login")] - public string Login { get; set; } = null!; + [DataMember(Name = "login")] + public string Login { get; set; } = null!; - [DataMember(Name = "password")] - public string Password { get; set; } = null!; + [DataMember(Name = "password")] + public string Password { get; set; } = null!; - [DataMember(Name = "integratedAuth")] - public bool IntegratedAuth { get; set; } + [DataMember(Name = "integratedAuth")] + public bool IntegratedAuth { get; set; } - [DataMember(Name = "connectionString")] - public string? ConnectionString { get; set; } - } + [DataMember(Name = "connectionString")] + public string? ConnectionString { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallInstructions.cs b/src/Umbraco.Core/Install/Models/InstallInstructions.cs index 9dc42553e0..c86307d9b0 100644 --- a/src/Umbraco.Core/Install/Models/InstallInstructions.cs +++ b/src/Umbraco.Core/Install/Models/InstallInstructions.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models -{ - [DataContract(Name = "installInstructions", Namespace = "")] - public class InstallInstructions - { - [DataMember(Name = "instructions")] - public IDictionary? Instructions { get; set; } +namespace Umbraco.Cms.Core.Install.Models; - [DataMember(Name = "installId")] - public Guid InstallId { get; set; } - } +[DataContract(Name = "installInstructions", Namespace = "")] +public class InstallInstructions +{ + [DataMember(Name = "instructions")] + public IDictionary? Instructions { get; set; } + + [DataMember(Name = "installId")] + public Guid InstallId { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs b/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs index 43b3fc73fe..650c746998 100644 --- a/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs +++ b/src/Umbraco.Core/Install/Models/InstallProgressResultModel.cs @@ -1,42 +1,41 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +/// +/// Returned to the UI for each installation step that is completed +/// +[DataContract(Name = "result", Namespace = "")] +public class InstallProgressResultModel { + public InstallProgressResultModel(bool processComplete, string stepCompleted, string nextStep, string? view = null, object? viewModel = null) + { + ProcessComplete = processComplete; + StepCompleted = stepCompleted; + NextStep = nextStep; + ViewModel = viewModel; + View = view; + } /// - /// Returned to the UI for each installation step that is completed + /// The UI view to show when this step executes, by default no views are shown for the completion of a step unless + /// explicitly specified. /// - [DataContract(Name = "result", Namespace = "")] - public class InstallProgressResultModel - { - public InstallProgressResultModel(bool processComplete, string stepCompleted, string nextStep, string? view = null, object? viewModel = null) - { - ProcessComplete = processComplete; - StepCompleted = stepCompleted; - NextStep = nextStep; - ViewModel = viewModel; - View = view; - } + [DataMember(Name = "view")] + public string? View { get; private set; } - /// - /// The UI view to show when this step executes, by default no views are shown for the completion of a step unless explicitly specified. - /// - [DataMember(Name = "view")] - public string? View { get; private set; } + [DataMember(Name = "complete")] + public bool ProcessComplete { get; set; } - [DataMember(Name = "complete")] - public bool ProcessComplete { get; set; } + [DataMember(Name = "stepCompleted")] + public string StepCompleted { get; set; } - [DataMember(Name = "stepCompleted")] - public string StepCompleted { get; set; } + [DataMember(Name = "nextStep")] + public string NextStep { get; set; } - [DataMember(Name = "nextStep")] - public string NextStep { get; set; } - - /// - /// The view model to return to the UI if this step is returning a view (optional) - /// - [DataMember(Name = "model")] - public object? ViewModel { get; private set; } - } + /// + /// The view model to return to the UI if this step is returning a view (optional) + /// + [DataMember(Name = "model")] + public object? ViewModel { get; private set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallSetup.cs b/src/Umbraco.Core/Install/Models/InstallSetup.cs index 358bd92234..2a1e3ce9f7 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetup.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetup.cs @@ -1,26 +1,22 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +/// +/// Model containing all the install steps for setting up the UI +/// +[DataContract(Name = "installSetup", Namespace = "")] +public class InstallSetup { - /// - /// Model containing all the install steps for setting up the UI - /// - [DataContract(Name = "installSetup", Namespace = "")] - public class InstallSetup + public InstallSetup() { - public InstallSetup() - { - Steps = new List(); - InstallId = Guid.NewGuid(); - } - - [DataMember(Name = "installId")] - public Guid InstallId { get; private set; } - - [DataMember(Name = "steps")] - public IEnumerable Steps { get; set; } - + Steps = new List(); + InstallId = Guid.NewGuid(); } + + [DataMember(Name = "installId")] + public Guid InstallId { get; private set; } + + [DataMember(Name = "steps")] + public IEnumerable Steps { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallSetupResult.cs b/src/Umbraco.Core/Install/Models/InstallSetupResult.cs index 15a4c12b47..3849a09d75 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupResult.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupResult.cs @@ -1,47 +1,42 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Core.Install.Models +/// +/// The object returned from each installation step +/// +public class InstallSetupResult { - /// - /// The object returned from each installation step - /// - public class InstallSetupResult + public InstallSetupResult() { - public InstallSetupResult() - { - } - - public InstallSetupResult(IDictionary savedStepData, string view, object? viewModel = null) - { - ViewModel = viewModel; - SavedStepData = savedStepData; - View = view; - } - - public InstallSetupResult(IDictionary savedStepData) - { - SavedStepData = savedStepData; - } - - public InstallSetupResult(string view, object? viewModel = null) - { - ViewModel = viewModel; - View = view; - } - - /// - /// Data that is persisted to the installation file which can be used from other installation steps - /// - public IDictionary? SavedStepData { get; private set; } - - /// - /// The UI view to show when this step executes, by default no views are shown for the completion of a step unless explicitly specified. - /// - public string? View { get; private set; } - - /// - /// The view model to return to the UI if this step is returning a view (optional) - /// - public object? ViewModel { get; private set; } } + + public InstallSetupResult(IDictionary savedStepData, string view, object? viewModel = null) + { + ViewModel = viewModel; + SavedStepData = savedStepData; + View = view; + } + + public InstallSetupResult(IDictionary savedStepData) => SavedStepData = savedStepData; + + public InstallSetupResult(string view, object? viewModel = null) + { + ViewModel = viewModel; + View = view; + } + + /// + /// Data that is persisted to the installation file which can be used from other installation steps + /// + public IDictionary? SavedStepData { get; } + + /// + /// The UI view to show when this step executes, by default no views are shown for the completion of a step unless + /// explicitly specified. + /// + public string? View { get; } + + /// + /// The view model to return to the UI if this step is returning a view (optional) + /// + public object? ViewModel { get; } } diff --git a/src/Umbraco.Core/Install/Models/InstallSetupStep.cs b/src/Umbraco.Core/Install/Models/InstallSetupStep.cs index 766458f99f..a9d24447c6 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupStep.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupStep.cs @@ -1,87 +1,84 @@ -using System; using System.Runtime.Serialization; -using System.Threading.Tasks; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +/// +/// Model to give to the front-end to collect the information for each step +/// +[DataContract(Name = "step", Namespace = "")] +public abstract class InstallSetupStep : InstallSetupStep { /// - /// Model to give to the front-end to collect the information for each step + /// Defines the step model type on the server side so we can bind it /// - [DataContract(Name = "step", Namespace = "")] - public abstract class InstallSetupStep : InstallSetupStep + [IgnoreDataMember] + public override Type StepType => typeof(T); + + /// + /// The step execution method + /// + /// + /// + public abstract Task ExecuteAsync(T model); + + /// + /// Determines if this step needs to execute based on the current state of the application and/or install process + /// + /// + public abstract bool RequiresExecution(T model); +} + +[DataContract(Name = "step", Namespace = "")] +public abstract class InstallSetupStep +{ + protected InstallSetupStep() { - /// - /// Defines the step model type on the server side so we can bind it - /// - [IgnoreDataMember] - public override Type StepType => typeof(T); - - /// - /// The step execution method - /// - /// - /// - public abstract Task ExecuteAsync(T model); - - /// - /// Determines if this step needs to execute based on the current state of the application and/or install process - /// - /// - public abstract bool RequiresExecution(T model); - } - - [DataContract(Name = "step", Namespace = "")] - public abstract class InstallSetupStep - { - protected InstallSetupStep() + InstallSetupStepAttribute? att = GetType().GetCustomAttribute(false); + if (att == null) { - var att = GetType().GetCustomAttribute(false); - if (att == null) - { - throw new InvalidOperationException("Each step must be attributed"); - } - Name = att.Name; - View = att.View; - ServerOrder = att.ServerOrder; - Description = att.Description; - InstallTypeTarget = att.InstallTypeTarget; - PerformsAppRestart = att.PerformsAppRestart; + throw new InvalidOperationException("Each step must be attributed"); } - [DataMember(Name = "name")] - public string Name { get; private set; } - - [DataMember(Name = "view")] - public virtual string View { get; private set; } - - /// - /// The view model used to render the view, by default is null but can be populated - /// - [DataMember(Name = "model")] - public virtual object? ViewModel { get; private set; } - - [DataMember(Name = "description")] - public string Description { get; private set; } - - [IgnoreDataMember] - public InstallationType InstallTypeTarget { get; private set; } - - [IgnoreDataMember] - public bool PerformsAppRestart { get; private set; } - - /// - /// Defines what order this step needs to execute on the server side since the - /// steps might be shown out of order on the front-end - /// - [DataMember(Name = "serverOrder")] - public int ServerOrder { get; private set; } - - /// - /// Defines the step model type on the server side so we can bind it - /// - [IgnoreDataMember] - public abstract Type StepType { get; } - + Name = att.Name; + View = att.View; + ServerOrder = att.ServerOrder; + Description = att.Description; + InstallTypeTarget = att.InstallTypeTarget; + PerformsAppRestart = att.PerformsAppRestart; } + + [DataMember(Name = "name")] + public string Name { get; private set; } + + [DataMember(Name = "view")] + public virtual string View { get; private set; } + + /// + /// The view model used to render the view, by default is null but can be populated + /// + [DataMember(Name = "model")] + public virtual object? ViewModel { get; private set; } + + [DataMember(Name = "description")] + public string Description { get; private set; } + + [IgnoreDataMember] + public InstallationType InstallTypeTarget { get; } + + [IgnoreDataMember] + public bool PerformsAppRestart { get; } + + /// + /// Defines what order this step needs to execute on the server side since the + /// steps might be shown out of order on the front-end + /// + [DataMember(Name = "serverOrder")] + public int ServerOrder { get; private set; } + + /// + /// Defines the step model type on the server side so we can bind it + /// + [IgnoreDataMember] + public abstract Type StepType { get; } } diff --git a/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs b/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs index 7feaced052..c6d0657d33 100644 --- a/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs +++ b/src/Umbraco.Core/Install/Models/InstallSetupStepAttribute.cs @@ -1,44 +1,47 @@ -using System; +namespace Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Core.Install.Models +public sealed class InstallSetupStepAttribute : Attribute { - public sealed class InstallSetupStepAttribute : Attribute + public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, string view, int serverOrder, string description) { - public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, string view, int serverOrder, string description) - { - InstallTypeTarget = installTypeTarget; - Name = name; - View = view; - ServerOrder = serverOrder; - Description = description; + InstallTypeTarget = installTypeTarget; + Name = name; + View = view; + ServerOrder = serverOrder; + Description = description; - //default - PerformsAppRestart = false; - } - - public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, int serverOrder, string description) - { - InstallTypeTarget = installTypeTarget; - Name = name; - View = string.Empty; - ServerOrder = serverOrder; - Description = description; - - //default - PerformsAppRestart = false; - } - - public InstallationType InstallTypeTarget { get; private set; } - public string Name { get; private set; } - public string View { get; private set; } - public int ServerOrder { get; private set; } - public string Description { get; private set; } - - /// - /// A flag to notify the installer that this step performs an app pool restart, this can be handy to know since if the current - /// step is performing a restart, we cannot 'look ahead' to see if the next step can execute since we won't know until the app pool - /// is restarted. - /// - public bool PerformsAppRestart { get; set; } + // default + PerformsAppRestart = false; } + + public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, int serverOrder, string description) + { + InstallTypeTarget = installTypeTarget; + Name = name; + View = string.Empty; + ServerOrder = serverOrder; + Description = description; + + // default + PerformsAppRestart = false; + } + + public InstallationType InstallTypeTarget { get; } + + public string Name { get; } + + public string View { get; } + + public int ServerOrder { get; } + + public string Description { get; } + + /// + /// A flag to notify the installer that this step performs an app pool restart, this can be handy to know since if the + /// current + /// step is performing a restart, we cannot 'look ahead' to see if the next step can execute since we won't know until + /// the app pool + /// is restarted. + /// + public bool PerformsAppRestart { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs b/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs index 3a34264d77..74170857b5 100644 --- a/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs +++ b/src/Umbraco.Core/Install/Models/InstallTrackingItem.cs @@ -1,37 +1,43 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Core.Install.Models +public class InstallTrackingItem { - public class InstallTrackingItem + public InstallTrackingItem(string name, int serverOrder) { - public InstallTrackingItem(string name, int serverOrder) - { - Name = name; - ServerOrder = serverOrder; - AdditionalData = new Dictionary(); - } - - public string Name { get; set; } - public int ServerOrder { get; set; } - public bool IsComplete { get; set; } - public IDictionary AdditionalData { get; set; } - - protected bool Equals(InstallTrackingItem other) - { - return string.Equals(Name, other.Name); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((InstallTrackingItem) obj); - } - - public override int GetHashCode() - { - return Name.GetHashCode(); - } + Name = name; + ServerOrder = serverOrder; + AdditionalData = new Dictionary(); } + + public string Name { get; set; } + + public int ServerOrder { get; set; } + + public bool IsComplete { get; set; } + + public IDictionary AdditionalData { get; set; } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((InstallTrackingItem)obj); + } + + protected bool Equals(InstallTrackingItem other) => string.Equals(Name, other.Name); + + public override int GetHashCode() => Name.GetHashCode(); } diff --git a/src/Umbraco.Core/Install/Models/InstallationType.cs b/src/Umbraco.Core/Install/Models/InstallationType.cs index 99ecf8ce1f..b2b6a428fa 100644 --- a/src/Umbraco.Core/Install/Models/InstallationType.cs +++ b/src/Umbraco.Core/Install/Models/InstallationType.cs @@ -1,11 +1,8 @@ -using System; +namespace Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Core.Install.Models +[Flags] +public enum InstallationType { - [Flags] - public enum InstallationType - { - NewInstall = 1 << 0, // 1 - Upgrade = 1 << 1, // 2 - } + NewInstall = 1 << 0, // 1 + Upgrade = 1 << 1, // 2 } diff --git a/src/Umbraco.Core/Install/Models/Package.cs b/src/Umbraco.Core/Install/Models/Package.cs index 3b9a204f10..9ac30ab9a7 100644 --- a/src/Umbraco.Core/Install/Models/Package.cs +++ b/src/Umbraco.Core/Install/Models/Package.cs @@ -1,16 +1,16 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +[DataContract(Name = "package")] +public class Package { - [DataContract(Name = "package")] - public class Package - { - [DataMember(Name = "name")] - public string? Name { get; set; } - [DataMember(Name = "thumbnail")] - public string? Thumbnail { get; set; } - [DataMember(Name = "id")] - public Guid Id { get; set; } - } + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "thumbnail")] + public string? Thumbnail { get; set; } + + [DataMember(Name = "id")] + public Guid Id { get; set; } } diff --git a/src/Umbraco.Core/Install/Models/UserModel.cs b/src/Umbraco.Core/Install/Models/UserModel.cs index d294a24c1d..61f76c795d 100644 --- a/src/Umbraco.Core/Install/Models/UserModel.cs +++ b/src/Umbraco.Core/Install/Models/UserModel.cs @@ -1,24 +1,23 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Install.Models +namespace Umbraco.Cms.Core.Install.Models; + +[DataContract(Name = "user", Namespace = "")] +public class UserModel { - [DataContract(Name = "user", Namespace = "")] - public class UserModel - { - [DataMember(Name = "name")] - public string Name { get; set; } = null!; + [DataMember(Name = "name")] + public string Name { get; set; } = null!; - [DataMember(Name = "email")] - public string Email { get; set; } = null!; + [DataMember(Name = "email")] + public string Email { get; set; } = null!; - [DataMember(Name = "password")] - public string Password { get; set; } = null!; + [DataMember(Name = "password")] + public string Password { get; set; } = null!; - [DataMember(Name = "subscribeToNewsLetter")] - public bool SubscribeToNewsLetter { get; set; } + [DataMember(Name = "subscribeToNewsLetter")] + public bool SubscribeToNewsLetter { get; set; } - [DataMember(Name = "telemetryLevel")] - public TelemetryLevel TelemetryLevel { get; set; } - } + [DataMember(Name = "telemetryLevel")] + public TelemetryLevel TelemetryLevel { get; set; } } diff --git a/src/Umbraco.Core/InstallLog.cs b/src/Umbraco.Core/InstallLog.cs index 3d8ab26af9..d0bec2097f 100644 --- a/src/Umbraco.Core/InstallLog.cs +++ b/src/Umbraco.Core/InstallLog.cs @@ -1,34 +1,52 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +public class InstallLog { - public class InstallLog + public InstallLog( + Guid installId, + bool isUpgrade, + bool installCompleted, + DateTime timestamp, + int versionMajor, + int versionMinor, + int versionPatch, + string versionComment, + string error, + string? userAgent, + string dbProvider) { - public Guid InstallId { get; } - public bool IsUpgrade { get; set; } - public bool InstallCompleted { get; set; } - public DateTime Timestamp { get; set; } - public int VersionMajor { get; } - public int VersionMinor { get; } - public int VersionPatch { get; } - public string VersionComment { get; } - public string Error { get; } - public string? UserAgent { get; } - public string DbProvider { get; set; } - - public InstallLog(Guid installId, bool isUpgrade, bool installCompleted, DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string? userAgent, string dbProvider) - { - InstallId = installId; - IsUpgrade = isUpgrade; - InstallCompleted = installCompleted; - Timestamp = timestamp; - VersionMajor = versionMajor; - VersionMinor = versionMinor; - VersionPatch = versionPatch; - VersionComment = versionComment; - Error = error; - UserAgent = userAgent; - DbProvider = dbProvider; - } + InstallId = installId; + IsUpgrade = isUpgrade; + InstallCompleted = installCompleted; + Timestamp = timestamp; + VersionMajor = versionMajor; + VersionMinor = versionMinor; + VersionPatch = versionPatch; + VersionComment = versionComment; + Error = error; + UserAgent = userAgent; + DbProvider = dbProvider; } + + public Guid InstallId { get; } + + public bool IsUpgrade { get; set; } + + public bool InstallCompleted { get; set; } + + public DateTime Timestamp { get; set; } + + public int VersionMajor { get; } + + public int VersionMinor { get; } + + public int VersionPatch { get; } + + public string VersionComment { get; } + + public string Error { get; } + + public string? UserAgent { get; } + + public string DbProvider { get; set; } } diff --git a/src/Umbraco.Core/LambdaExpressionCacheKey.cs b/src/Umbraco.Core/LambdaExpressionCacheKey.cs index 123654bbe2..31ebcf688f 100644 --- a/src/Umbraco.Core/LambdaExpressionCacheKey.cs +++ b/src/Umbraco.Core/LambdaExpressionCacheKey.cs @@ -1,83 +1,77 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a simple in a form which is suitable for using as a dictionary key +/// by exposing the return type, argument types and expression string form in a single concatenated string. +/// +public struct LambdaExpressionCacheKey { /// - /// Represents a simple in a form which is suitable for using as a dictionary key - /// by exposing the return type, argument types and expression string form in a single concatenated string. + /// The argument type names of the /// - public struct LambdaExpressionCacheKey + public readonly HashSet ArgTypes; + + public LambdaExpressionCacheKey(string returnType, string expression, params string[] argTypes) { - public LambdaExpressionCacheKey(string returnType, string expression, params string[] argTypes) - { - ReturnType = returnType; - ExpressionAsString = expression; - ArgTypes = new HashSet(argTypes); - _toString = null; - } - - public LambdaExpressionCacheKey(LambdaExpression obj) - { - ReturnType = obj.ReturnType.FullName; - ExpressionAsString = obj.ToString(); - ArgTypes = new HashSet(obj.Parameters.Select(x => x.Type.FullName)); - _toString = null; - } - - /// - /// The argument type names of the - /// - public readonly HashSet ArgTypes; - - /// - /// The return type of the - /// - public readonly string? ReturnType; - - /// - /// The original string representation of the - /// - public readonly string ExpressionAsString; - - private string? _toString; - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return _toString ?? (_toString = String.Concat(String.Join("|", ArgTypes), ",", ReturnType, ",", ExpressionAsString)); - } - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object? obj) - { - if (ReferenceEquals(obj, null)) return false; - var casted = (LambdaExpressionCacheKey)obj; - return casted.ToString() == ToString(); - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - return ToString().GetHashCode(); - } + ReturnType = returnType; + ExpressionAsString = expression; + ArgTypes = new HashSet(argTypes); + _toString = null; } + + public LambdaExpressionCacheKey(LambdaExpression obj) + { + ReturnType = obj.ReturnType.FullName; + ExpressionAsString = obj.ToString(); + ArgTypes = new HashSet(obj.Parameters.Select(x => x.Type.FullName)); + _toString = null; + } + + /// + /// The return type of the + /// + public readonly string? ReturnType; + + /// + /// The original string representation of the + /// + public readonly string ExpressionAsString; + + private string? _toString; + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => _toString ??= string.Concat(string.Join("|", ArgTypes), ",", ReturnType, ",", ExpressionAsString); + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(obj, null)) + { + return false; + } + + var casted = (LambdaExpressionCacheKey)obj; + return casted.ToString() == ToString(); + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() => ToString().GetHashCode(); } diff --git a/src/Umbraco.Core/Logging/DisposableTimer.cs b/src/Umbraco.Core/Logging/DisposableTimer.cs index a22ac75127..b153e096c4 100644 --- a/src/Umbraco.Core/Logging/DisposableTimer.cs +++ b/src/Umbraco.Core/Logging/DisposableTimer.cs @@ -1,172 +1,183 @@ -using System; using System.Diagnostics; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Starts the timer and invokes a callback upon disposal. Provides a simple way of timing an operation by wrapping it +/// in a using (C#) statement. +/// +public class DisposableTimer : DisposableObjectSlim { - /// - /// Starts the timer and invokes a callback upon disposal. Provides a simple way of timing an operation by wrapping it in a using (C#) statement. - /// - public class DisposableTimer : DisposableObjectSlim + private readonly string _endMessage; + private readonly object[]? _endMessageArgs; + private readonly object[]? _failMessageArgs; + private readonly LogLevel _level; + private readonly ILogger _logger; + private readonly Type _loggerType; + private readonly IDisposable? _profilerStep; + private readonly int _thresholdMilliseconds; + private readonly string _timingId; + private bool _failed; + private Exception? _failException; + private string? _failMessage; + + // internal - created by profiling logger + internal DisposableTimer( + ILogger logger, + LogLevel level, + IProfiler profiler, + Type loggerType, + string startMessage, + string endMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null, + int thresholdMilliseconds = 0) { - private readonly ILogger _logger; - private readonly LogLevel _level; - private readonly Type _loggerType; - private readonly int _thresholdMilliseconds; - private readonly IDisposable? _profilerStep; - private readonly string _endMessage; - private string? _failMessage; - private readonly object[]? _endMessageArgs; - private readonly object[]? _failMessageArgs; - private Exception? _failException; - private bool _failed; - private readonly string _timingId; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _level = level; + _loggerType = loggerType ?? throw new ArgumentNullException(nameof(loggerType)); + _endMessage = endMessage; + _failMessage = failMessage; + _endMessageArgs = endMessageArgs; + _failMessageArgs = failMessageArgs; + _thresholdMilliseconds = thresholdMilliseconds < 0 ? 0 : thresholdMilliseconds; + _timingId = Guid.NewGuid().ToString("N").Substring(0, 7); // keep it short-ish - // internal - created by profiling logger - internal DisposableTimer( - ILogger logger, - LogLevel level, - IProfiler profiler, - Type loggerType, - string startMessage, - string endMessage, - string? failMessage = null, - object[]? startMessageArgs = null, - object[]? endMessageArgs = null, - object[]? failMessageArgs = null, - int thresholdMilliseconds = 0) + if (thresholdMilliseconds == 0) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _level = level; - _loggerType = loggerType ?? throw new ArgumentNullException(nameof(loggerType)); - _endMessage = endMessage; - _failMessage = failMessage; - _endMessageArgs = endMessageArgs; - _failMessageArgs = failMessageArgs; - _thresholdMilliseconds = thresholdMilliseconds < 0 ? 0 : thresholdMilliseconds; - _timingId = Guid.NewGuid().ToString("N").Substring(0, 7); // keep it short-ish + switch (_level) + { + case LogLevel.Debug: + if (startMessageArgs == null) + { + logger.LogDebug("{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + } + else + { + var args = new object[startMessageArgs.Length + 1]; + startMessageArgs.CopyTo(args, 0); + args[startMessageArgs.Length] = _timingId; + logger.LogDebug(startMessage + " [Timing {TimingId}]", args); + } - if (thresholdMilliseconds == 0) + break; + case LogLevel.Information: + if (startMessageArgs == null) + { + logger.LogInformation("{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + } + else + { + var args = new object[startMessageArgs.Length + 1]; + startMessageArgs.CopyTo(args, 0); + args[startMessageArgs.Length] = _timingId; + logger.LogInformation(startMessage + " [Timing {TimingId}]", args); + } + + break; + default: + throw new ArgumentOutOfRangeException(nameof(level)); + } + } + + // else aren't logging the start message, this is output to the profiler but not the log, + // we just want the log to contain the result if it's more than the minimum ms threshold. + _profilerStep = profiler?.Step(loggerType, startMessage); + } + + public Stopwatch Stopwatch { get; } = Stopwatch.StartNew(); + + /// + /// Reports a failure. + /// + /// The fail message. + /// The exception. + /// Completion of the timer will be reported as an error, with the specified message and exception. + public void Fail(string? failMessage = null, Exception? exception = null) + { + _failed = true; + _failMessage = failMessage ?? _failMessage ?? "Failed."; + _failException = exception; + } + + /// + /// Disposes resources. + /// + /// Overrides abstract class which handles required locking. + protected override void DisposeResources() + { + Stopwatch.Stop(); + + _profilerStep?.Dispose(); + + if ((Stopwatch.ElapsedMilliseconds >= _thresholdMilliseconds || _failed) + && _loggerType != null && _logger != null + && (string.IsNullOrWhiteSpace(_endMessage) == false || _failed)) + { + if (_failed) + { + if (_failMessageArgs is null) + { + _logger.LogError(_failException, "{FailMessage} ({Duration}ms) [Timing {TimingId}]", _failMessage, Stopwatch.ElapsedMilliseconds, _timingId); + } + else + { + var args = new object[_failMessageArgs.Length + 2]; + _failMessageArgs.CopyTo(args, 0); + args[_failMessageArgs.Length - 1] = Stopwatch.ElapsedMilliseconds; + args[_failMessageArgs.Length] = _timingId; + _logger.LogError(_failException, _failMessage + " ({Duration}ms) [Timing {TimingId}]", args); + } + } + else { switch (_level) { case LogLevel.Debug: - if (startMessageArgs == null) + if (_endMessageArgs == null) { - logger.LogDebug("{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + _logger.LogDebug( + "{EndMessage} ({Duration}ms) [Timing {TimingId}]", + _endMessage, + Stopwatch.ElapsedMilliseconds, + _timingId); } else { - var args = new object[startMessageArgs.Length + 1]; - startMessageArgs.CopyTo(args, 0); - args[startMessageArgs.Length] = _timingId; - logger.LogDebug(startMessage + " [Timing {TimingId}]", args); + var args = new object[_endMessageArgs.Length + 2]; + _endMessageArgs.CopyTo(args, 0); + args[^1] = Stopwatch.ElapsedMilliseconds; + args[args.Length] = _timingId; + _logger.LogDebug(_endMessage + " ({Duration}ms) [Timing {TimingId}]", args); } + break; case LogLevel.Information: - if (startMessageArgs == null) + if (_endMessageArgs == null) { - logger.LogInformation("{StartMessage} [Timing {TimingId}]", startMessage, _timingId); + _logger.LogInformation( + "{EndMessage} ({Duration}ms) [Timing {TimingId}]", + _endMessage, + Stopwatch.ElapsedMilliseconds, + _timingId); } else { - var args = new object[startMessageArgs.Length + 1]; - startMessageArgs.CopyTo(args, 0); - args[startMessageArgs.Length] = _timingId; - logger.LogInformation(startMessage + " [Timing {TimingId}]", args); + var args = new object[_endMessageArgs.Length + 2]; + _endMessageArgs.CopyTo(args, 0); + args[_endMessageArgs.Length - 1] = Stopwatch.ElapsedMilliseconds; + args[_endMessageArgs.Length] = _timingId; + _logger.LogInformation(_endMessage + " ({Duration}ms) [Timing {TimingId}]", args); } + break; - default: - throw new ArgumentOutOfRangeException(nameof(level)); - } - } - // else aren't logging the start message, this is output to the profiler but not the log, - // we just want the log to contain the result if it's more than the minimum ms threshold. - - _profilerStep = profiler?.Step(loggerType, startMessage); - } - - /// - /// Reports a failure. - /// - /// The fail message. - /// The exception. - /// Completion of the timer will be reported as an error, with the specified message and exception. - public void Fail(string? failMessage = null, Exception? exception = null) - { - _failed = true; - _failMessage = failMessage ?? _failMessage ?? "Failed."; - _failException = exception; - } - - public Stopwatch Stopwatch { get; } = Stopwatch.StartNew(); - - /// - ///Disposes resources. - /// - /// Overrides abstract class which handles required locking. - protected override void DisposeResources() - { - Stopwatch.Stop(); - - _profilerStep?.Dispose(); - - if ((Stopwatch.ElapsedMilliseconds >= _thresholdMilliseconds || _failed) - && _loggerType != null && _logger != null - && (string.IsNullOrWhiteSpace(_endMessage) == false || _failed)) - { - if (_failed) - { - if (_failMessageArgs is null) - { - _logger.LogError(_failException, "{FailMessage} ({Duration}ms) [Timing {TimingId}]", _failMessage, Stopwatch.ElapsedMilliseconds, _timingId); - } - else - { - var args = new object[_failMessageArgs.Length + 2]; - _failMessageArgs.CopyTo(args, 0); - args[_failMessageArgs.Length - 1] = Stopwatch.ElapsedMilliseconds; - args[_failMessageArgs.Length] = _timingId; - _logger.LogError(_failException, _failMessage + " ({Duration}ms) [Timing {TimingId}]", args); - } - } - else - { - switch (_level) - { - case LogLevel.Debug: - if (_endMessageArgs == null) - { - _logger.LogDebug("{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId); - } - else - { - var args = new object[_endMessageArgs.Length + 2]; - _endMessageArgs.CopyTo(args, 0); - args[args.Length - 1] = Stopwatch.ElapsedMilliseconds; - args[args.Length] = _timingId; - _logger.LogDebug(_endMessage + " ({Duration}ms) [Timing {TimingId}]", args); - } - break; - case LogLevel.Information: - if (_endMessageArgs == null) - { - _logger.LogInformation("{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId); - } - else - { - var args = new object[_endMessageArgs.Length + 2]; - _endMessageArgs.CopyTo(args, 0); - args[_endMessageArgs.Length - 1] = Stopwatch.ElapsedMilliseconds; - args[_endMessageArgs.Length] = _timingId; - _logger.LogInformation(_endMessage + " ({Duration}ms) [Timing {TimingId}]", args); - } - break; - // filtered in the ctor - //default: - // throw new Exception(); - } + // filtered in the ctor + // default: + // throw new Exception(); } } } diff --git a/src/Umbraco.Core/Logging/ILoggingConfiguration.cs b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs index 34e4d702c6..662ee7891c 100644 --- a/src/Umbraco.Core/Logging/ILoggingConfiguration.cs +++ b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs @@ -1,11 +1,9 @@ -namespace Umbraco.Cms.Core.Logging -{ +namespace Umbraco.Cms.Core.Logging; - public interface ILoggingConfiguration - { - /// - /// Gets the physical path where logs are stored - /// - string LogDirectory { get; } - } +public interface ILoggingConfiguration +{ + /// + /// Gets the physical path where logs are stored + /// + string LogDirectory { get; } } diff --git a/src/Umbraco.Core/Logging/IMessageTemplates.cs b/src/Umbraco.Core/Logging/IMessageTemplates.cs index 99d88ce926..252f91aaa5 100644 --- a/src/Umbraco.Core/Logging/IMessageTemplates.cs +++ b/src/Umbraco.Core/Logging/IMessageTemplates.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Provides tools to support message templates. +/// +public interface IMessageTemplates { - /// - /// Provides tools to support message templates. - /// - public interface IMessageTemplates - { - string Render(string messageTemplate, params object[] args); - } + string Render(string messageTemplate, params object[] args); } diff --git a/src/Umbraco.Core/Logging/IProfiler.cs b/src/Umbraco.Core/Logging/IProfiler.cs index 4b2bf6fc48..ab580d6aae 100644 --- a/src/Umbraco.Core/Logging/IProfiler.cs +++ b/src/Umbraco.Core/Logging/IProfiler.cs @@ -1,32 +1,30 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +/// +/// Defines the profiling service. +/// +public interface IProfiler { + /// + /// Gets an that will time the code between its creation and disposal. + /// + /// The name of the step. + /// A step. + /// The returned is meant to be used within a using (...) {{ ... }} block. + IDisposable? Step(string name); /// - /// Defines the profiling service. + /// Starts the profiler. /// - public interface IProfiler - { - /// - /// Gets an that will time the code between its creation and disposal. - /// - /// The name of the step. - /// A step. - /// The returned is meant to be used within a using (...) {{ ... }} block. - IDisposable? Step(string name); + void Start(); - /// - /// Starts the profiler. - /// - void Start(); - - /// - /// Stops the profiler. - /// - /// A value indicating whether to discard results. - /// Set discardResult to true to abandon all profiling - useful when eg someone is not - /// authenticated or you want to clear the results, based upon some other mechanism. - void Stop(bool discardResults = false); - } + /// + /// Stops the profiler. + /// + /// A value indicating whether to discard results. + /// + /// Set discardResult to true to abandon all profiling - useful when eg someone is not + /// authenticated or you want to clear the results, based upon some other mechanism. + /// + void Stop(bool discardResults = false); } diff --git a/src/Umbraco.Core/Logging/IProfilerHtml.cs b/src/Umbraco.Core/Logging/IProfilerHtml.cs index 30812fc156..806ee54e7a 100644 --- a/src/Umbraco.Core/Logging/IProfilerHtml.cs +++ b/src/Umbraco.Core/Logging/IProfilerHtml.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Used to render a profiler in a web page +/// +public interface IProfilerHtml { /// - /// Used to render a profiler in a web page + /// Renders the profiling results. /// - public interface IProfilerHtml - { - /// - /// Renders the profiling results. - /// - /// The profiling results. - /// Generally used for HTML rendering. - string Render(); - } + /// The profiling results. + /// Generally used for HTML rendering. + string Render(); } diff --git a/src/Umbraco.Core/Logging/IProfilingLogger.cs b/src/Umbraco.Core/Logging/IProfilingLogger.cs index 5873619988..92c4d55f0c 100644 --- a/src/Umbraco.Core/Logging/IProfilingLogger.cs +++ b/src/Umbraco.Core/Logging/IProfilingLogger.cs @@ -1,40 +1,65 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +/// +/// Defines the profiling logging service. +/// +public interface IProfilingLogger { /// - /// Defines the profiling logging service. + /// Profiles an action and log as information messages. /// - public interface IProfilingLogger - { - /// - /// Profiles an action and log as information messages. - /// - DisposableTimer TraceDuration(string startMessage, object[]? startMessageArgs = null); + DisposableTimer TraceDuration(string startMessage, object[]? startMessageArgs = null); - /// - /// Profiles an action and log as information messages. - /// - DisposableTimer TraceDuration(string startMessage, string completeMessage, string? failMessage = null, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null); + /// + /// Profiles an action and log as information messages. + /// + DisposableTimer TraceDuration( + string startMessage, + string completeMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null); - /// - /// Profiles an action and log as information messages. - /// - DisposableTimer TraceDuration(Type loggerType, string startMessage, string completeMessage, string? failMessage = null, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null); + /// + /// Profiles an action and log as information messages. + /// + DisposableTimer TraceDuration( + Type loggerType, + string startMessage, + string completeMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null); - /// - /// Profiles an action and log as debug messages. - /// - DisposableTimer? DebugDuration(string startMessage, object[]? startMessageArgs = null); + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer? DebugDuration(string startMessage, object[]? startMessageArgs = null); - /// - /// Profiles an action and log as debug messages. - /// - DisposableTimer? DebugDuration(string startMessage, string completeMessage, string? failMessage = null, int thresholdMilliseconds = 0, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null); + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer? DebugDuration( + string startMessage, + string completeMessage, + string? failMessage = null, + int thresholdMilliseconds = 0, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null); - /// - /// Profiles an action and log as debug messages. - /// - DisposableTimer? DebugDuration(Type loggerType, string startMessage, string completeMessage, string? failMessage = null, int thresholdMilliseconds = 0, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null); - } + /// + /// Profiles an action and log as debug messages. + /// + DisposableTimer? DebugDuration( + Type loggerType, + string startMessage, + string completeMessage, + string? failMessage = null, + int thresholdMilliseconds = 0, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null); } diff --git a/src/Umbraco.Core/Logging/LogHttpRequestExtension.cs b/src/Umbraco.Core/Logging/LogHttpRequestExtension.cs index c9e1b09e08..2981dd5987 100644 --- a/src/Umbraco.Core/Logging/LogHttpRequestExtension.cs +++ b/src/Umbraco.Core/Logging/LogHttpRequestExtension.cs @@ -1,24 +1,22 @@ -using System; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class LogHttpRequest { - public static class LogHttpRequest + private static readonly string RequestIdItemName = typeof(LogHttpRequest).Name + "+RequestId"; + + /// + /// Retrieve the id assigned to the currently-executing HTTP request, if any. + /// + /// The request id. + /// + /// true if there is a request in progress; false otherwise. + public static bool TryGetCurrentHttpRequestId(out Guid? requestId, IRequestCache requestCache) { - static readonly string RequestIdItemName = typeof(LogHttpRequest).Name + "+RequestId"; + var requestIdItem = requestCache.Get(RequestIdItemName, () => Guid.NewGuid()); + requestId = (Guid?)requestIdItem; - /// - /// Retrieve the id assigned to the currently-executing HTTP request, if any. - /// - /// The request id. - /// - /// true if there is a request in progress; false otherwise. - public static bool TryGetCurrentHttpRequestId(out Guid? requestId, IRequestCache requestCache) - { - var requestIdItem = requestCache.Get(RequestIdItemName, () => Guid.NewGuid()); - requestId = (Guid?)requestIdItem; - - return true; - } + return true; } } diff --git a/src/Umbraco.Core/Logging/LogLevel.cs b/src/Umbraco.Core/Logging/LogLevel.cs index 9e12002324..b7271ecf04 100644 --- a/src/Umbraco.Core/Logging/LogLevel.cs +++ b/src/Umbraco.Core/Logging/LogLevel.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Specifies the level of a log event. +/// +public enum LogLevel { - /// - /// Specifies the level of a log event. - /// - public enum LogLevel - { - Verbose, - Debug, - Information, - Warning, - Error, - Fatal - } + Verbose, + Debug, + Information, + Warning, + Error, + Fatal, } diff --git a/src/Umbraco.Core/Logging/LogProfiler.cs b/src/Umbraco.Core/Logging/LogProfiler.cs index 1f4b4bbe90..0504a2a1ae 100644 --- a/src/Umbraco.Core/Logging/LogProfiler.cs +++ b/src/Umbraco.Core/Logging/LogProfiler.cs @@ -1,57 +1,52 @@ -using System; using System.Diagnostics; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Implements by writing profiling results to an . +/// +public class LogProfiler : IProfiler { - /// - /// Implements by writing profiling results to an . - /// - public class LogProfiler : IProfiler + private readonly ILogger _logger; + + public LogProfiler(ILogger logger) => _logger = logger; + + /// + public IDisposable Step(string name) { - private readonly ILogger _logger; + _logger.LogDebug("Begin: {ProfileName}", name); + return new LightDisposableTimer(duration => + _logger.LogInformation("End {ProfileName} ({ProfileDuration}ms)", name, duration)); + } - public LogProfiler(ILogger logger) + /// + public void Start() + { + // the log will always be started + } + + /// + public void Stop(bool discardResults = false) + { + // the log never stops + } + + // a lightweight disposable timer + private class LightDisposableTimer : DisposableObjectSlim + { + private readonly Action _callback; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + + protected internal LightDisposableTimer(Action callback) { - _logger = logger; + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); } - /// - public IDisposable Step(string name) + protected override void DisposeResources() { - _logger.LogDebug("Begin: {ProfileName}", name); - return new LightDisposableTimer(duration => _logger.LogInformation("End {ProfileName} ({ProfileDuration}ms)", name, duration)); - } - - /// - public void Start() - { - // the log will always be started - } - - /// - public void Stop(bool discardResults = false) - { - // the log never stops - } - - // a lightweight disposable timer - private class LightDisposableTimer : DisposableObjectSlim - { - private readonly Action _callback; - private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); - - protected internal LightDisposableTimer(Action callback) - { - if (callback == null) throw new ArgumentNullException(nameof(callback)); - _callback = callback; - } - - protected override void DisposeResources() - { - _stopwatch.Stop(); - _callback(_stopwatch.ElapsedMilliseconds); - } + _stopwatch.Stop(); + _callback(_stopwatch.ElapsedMilliseconds); } } } diff --git a/src/Umbraco.Core/Logging/LoggingConfiguration.cs b/src/Umbraco.Core/Logging/LoggingConfiguration.cs index f191af3023..d2a24d24a9 100644 --- a/src/Umbraco.Core/Logging/LoggingConfiguration.cs +++ b/src/Umbraco.Core/Logging/LoggingConfiguration.cs @@ -1,14 +1,9 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +public class LoggingConfiguration : ILoggingConfiguration { - public class LoggingConfiguration : ILoggingConfiguration - { - public LoggingConfiguration(string logDirectory) - { - LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory)); - } + public LoggingConfiguration(string logDirectory) => + LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory)); - public string LogDirectory { get; } - } + public string LogDirectory { get; } } diff --git a/src/Umbraco.Core/Logging/LoggingTaskExtension.cs b/src/Umbraco.Core/Logging/LoggingTaskExtension.cs index 5a6f995dfa..950e9bb8f4 100644 --- a/src/Umbraco.Core/Logging/LoggingTaskExtension.cs +++ b/src/Umbraco.Core/Logging/LoggingTaskExtension.cs @@ -1,52 +1,50 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +internal static class LoggingTaskExtension { - internal static class LoggingTaskExtension + /// + /// This task shouldn't be waited on (as it's not guaranteed to run), and you shouldn't wait on the parent task either + /// (because it might throw an + /// exception that doesn't get handled). If you want to be waiting on something, use LogErrorsWaitable instead. + /// None of these methods are suitable for tasks that return a value. If you're wanting a result, you should probably + /// be handling + /// errors yourself. + /// + public static Task LogErrors(this Task task, Action logMethod) => + task.ContinueWith( + t => LogErrorsInner(t, logMethod), + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted, + + // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + TaskScheduler.Default); + + /// + /// This task can be waited on (as it's guaranteed to run), and you should wait on this rather than the parent task. + /// Because it's + /// guaranteed to run, it may be slower than using LogErrors, and you should consider using that method if you don't + /// want to wait. + /// None of these methods are suitable for tasks that return a value. If you're wanting a result, you should probably + /// be handling + /// errors yourself. + /// + public static Task LogErrorsWaitable(this Task task, Action logMethod) => + task.ContinueWith( + t => LogErrorsInner(t, logMethod), + + // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + TaskScheduler.Default); + + private static void LogErrorsInner(Task task, Action logAction) { - /// - /// This task shouldn't be waited on (as it's not guaranteed to run), and you shouldn't wait on the parent task either (because it might throw an - /// exception that doesn't get handled). If you want to be waiting on something, use LogErrorsWaitable instead. - /// - /// None of these methods are suitable for tasks that return a value. If you're wanting a result, you should probably be handling - /// errors yourself. - /// - public static Task LogErrors(this Task task, Action logMethod) + if (task.Exception != null) { - return task.ContinueWith( - t => LogErrorsInner(t, logMethod), - CancellationToken.None, - TaskContinuationOptions.OnlyOnFaulted, - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - } - - /// - /// This task can be waited on (as it's guaranteed to run), and you should wait on this rather than the parent task. Because it's - /// guaranteed to run, it may be slower than using LogErrors, and you should consider using that method if you don't want to wait. - /// - /// None of these methods are suitable for tasks that return a value. If you're wanting a result, you should probably be handling - /// errors yourself. - /// - public static Task LogErrorsWaitable(this Task task, Action logMethod) - { - return task.ContinueWith( - t => LogErrorsInner(t, logMethod), - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - } - - private static void LogErrorsInner(Task task, Action logAction) - { - if (task.Exception != null) + logAction( + "Aggregate Exception with " + task.Exception.InnerExceptions.Count + " inner exceptions: ", + task.Exception); + foreach (Exception innerException in task.Exception.InnerExceptions) { - logAction("Aggregate Exception with " + task.Exception.InnerExceptions.Count + " inner exceptions: ", task.Exception); - foreach (var innerException in task.Exception.InnerExceptions) - { - logAction("Inner exception from aggregate exception: ", innerException); - } + logAction("Inner exception from aggregate exception: ", innerException); } } } diff --git a/src/Umbraco.Core/Logging/NoopProfiler.cs b/src/Umbraco.Core/Logging/NoopProfiler.cs index 89a0307515..821728c7a6 100644 --- a/src/Umbraco.Core/Logging/NoopProfiler.cs +++ b/src/Umbraco.Core/Logging/NoopProfiler.cs @@ -1,26 +1,23 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +public class NoopProfiler : IProfiler { - public class NoopProfiler : IProfiler + private readonly VoidDisposable _disposable = new(); + + public IDisposable Step(string name) => _disposable; + + public void Start() { - private readonly VoidDisposable _disposable = new VoidDisposable(); + } - public IDisposable Step(string name) + public void Stop(bool discardResults = false) + { + } + + private class VoidDisposable : DisposableObjectSlim + { + protected override void DisposeResources() { - return _disposable; - } - - public void Start() - { } - - public void Stop(bool discardResults = false) - { } - - private class VoidDisposable : DisposableObjectSlim - { - protected override void DisposeResources() - { } } } } diff --git a/src/Umbraco.Core/Logging/ProfilerExtensions.cs b/src/Umbraco.Core/Logging/ProfilerExtensions.cs index 67739c2f38..e69506702a 100644 --- a/src/Umbraco.Core/Logging/ProfilerExtensions.cs +++ b/src/Umbraco.Core/Logging/ProfilerExtensions.cs @@ -1,39 +1,52 @@ -using System; +namespace Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +internal static class ProfilerExtensions { - internal static class ProfilerExtensions + /// + /// Gets an that will time the code between its creation and disposal, + /// prefixing the name of the step with a reporting type name. + /// + /// The reporting type. + /// The profiler. + /// The name of the step. + /// A step. + /// The returned is meant to be used within a using (...) {{ ... }} block. + internal static IDisposable? Step(this IProfiler profiler, string name) { - /// - /// Gets an that will time the code between its creation and disposal, - /// prefixing the name of the step with a reporting type name. - /// - /// The reporting type. - /// The profiler. - /// The name of the step. - /// A step. - /// The returned is meant to be used within a using (...) {{ ... }} block. - internal static IDisposable? Step(this IProfiler profiler, string name) + if (profiler == null) { - if (profiler == null) throw new ArgumentNullException(nameof(profiler)); - return profiler.Step(typeof (T), name); + throw new ArgumentNullException(nameof(profiler)); } - /// - /// Gets an that will time the code between its creation and disposal, - /// prefixing the name of the step with a reporting type name. - /// - /// The profiler. - /// The reporting type. - /// The name of the step. - /// A step. - /// The returned is meant to be used within a using (...) {{ ... }} block. - internal static IDisposable? Step(this IProfiler profiler, Type reporting, string name) + return profiler.Step(typeof(T), name); + } + + /// + /// Gets an that will time the code between its creation and disposal, + /// prefixing the name of the step with a reporting type name. + /// + /// The profiler. + /// The reporting type. + /// The name of the step. + /// A step. + /// The returned is meant to be used within a using (...) {{ ... }} block. + internal static IDisposable? Step(this IProfiler profiler, Type reporting, string name) + { + if (profiler == null) { - if (profiler == null) throw new ArgumentNullException(nameof(profiler)); - if (reporting == null) throw new ArgumentNullException(nameof(reporting)); - if (name == null) throw new ArgumentNullException(nameof(name)); - return profiler.Step($"[{reporting.Name}] {name}"); + throw new ArgumentNullException(nameof(profiler)); } + + if (reporting == null) + { + throw new ArgumentNullException(nameof(reporting)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return profiler.Step($"[{reporting.Name}] {name}"); } } diff --git a/src/Umbraco.Core/Logging/ProfilingLogger.cs b/src/Umbraco.Core/Logging/ProfilingLogger.cs index d3388bda01..997f139539 100644 --- a/src/Umbraco.Core/Logging/ProfilingLogger.cs +++ b/src/Umbraco.Core/Logging/ProfilingLogger.cs @@ -1,99 +1,145 @@ -using System; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +/// +/// Provides logging and profiling services. +/// +public sealed class ProfilingLogger : IProfilingLogger { /// - /// Provides logging and profiling services. + /// Initializes a new instance of the class. /// - public sealed class ProfilingLogger : IProfilingLogger + public ProfilingLogger(ILogger logger, IProfiler profiler) { - /// - /// Gets the underlying implementation. - /// - public ILogger Logger { get; } - - /// - /// Gets the underlying implementation. - /// - public IProfiler Profiler { get; } - - /// - /// Initializes a new instance of the class. - /// - public ProfilingLogger(ILogger logger, IProfiler profiler) - { - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); - } - - /// - /// Initializes a new instance of the class. - /// - public ProfilingLogger(ILogger logger, IProfiler profiler) - { - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); - } - - public DisposableTimer TraceDuration(string startMessage, object[]? startMessageArgs = null) - => TraceDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs); - - public DisposableTimer TraceDuration(string startMessage, string completeMessage, string? failMessage = null, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null) - => new DisposableTimer(Logger, LogLevel.Information, Profiler, typeof(T), startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs); - - public DisposableTimer TraceDuration(Type loggerType, string startMessage, string completeMessage, string? failMessage = null, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null) - => new DisposableTimer(Logger, LogLevel.Information, Profiler, loggerType, startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs); - - public DisposableTimer? DebugDuration(string startMessage, object[]? startMessageArgs = null) - => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) - ? DebugDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs) - : null; - - public DisposableTimer? DebugDuration(string startMessage, string completeMessage, string? failMessage = null, int thresholdMilliseconds = 0, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null) - => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) - ? new DisposableTimer(Logger, LogLevel.Debug, Profiler, typeof(T), startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs, thresholdMilliseconds) - : null; - - public DisposableTimer? DebugDuration(Type loggerType, string startMessage, string completeMessage, string? failMessage = null, int thresholdMilliseconds = 0, object[]? startMessageArgs = null, object[]? endMessageArgs = null, object[]? failMessageArgs = null) - => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) - ? new DisposableTimer(Logger, LogLevel.Debug, Profiler, loggerType, startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs, thresholdMilliseconds) - : null; - - #region ILogger - - public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level) - => Logger.IsEnabled(level); - - public void LogCritical(Exception exception, string messageTemplate, params object[] propertyValues) - => Logger.LogCritical(exception, messageTemplate, propertyValues); - - public void LogCritical(string messageTemplate, params object[] propertyValues) - => Logger.LogCritical(messageTemplate, propertyValues); - - public void LogError(Exception exception, string messageTemplate, params object[] propertyValues) - => Logger.LogError(exception, messageTemplate, propertyValues); - - public void LogError(string messageTemplate, params object[] propertyValues) - => Logger.LogError(messageTemplate, propertyValues); - - public void LogWarning(string messageTemplate, params object[] propertyValues) - => Logger.LogWarning(messageTemplate, propertyValues); - - public void LogWarning(Exception exception, string messageTemplate, params object[] propertyValues) - => Logger.LogWarning(exception, messageTemplate, propertyValues); - - public void LogInformation(string messageTemplate, params object[] propertyValues) - => Logger.LogInformation(messageTemplate, propertyValues); - - public void LogDebug(string messageTemplate, params object[] propertyValues) - => Logger.LogDebug(messageTemplate, propertyValues); - - public void LogTrace(string messageTemplate, params object[] propertyValues) - => Logger.LogTrace(messageTemplate, propertyValues); - - - - #endregion + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); } + + /// + /// Initializes a new instance of the class. + /// + public ProfilingLogger(ILogger logger, IProfiler profiler) + { + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); + } + + /// + /// Gets the underlying implementation. + /// + public ILogger Logger { get; } + + /// + /// Gets the underlying implementation. + /// + public IProfiler Profiler { get; } + + public DisposableTimer TraceDuration(string startMessage, object[]? startMessageArgs = null) + => TraceDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs); + + public DisposableTimer TraceDuration( + string startMessage, + string completeMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null) + => new(Logger, LogLevel.Information, Profiler, typeof(T), startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs); + + public DisposableTimer TraceDuration( + Type loggerType, + string startMessage, + string completeMessage, + string? failMessage = null, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null) + => new(Logger, LogLevel.Information, Profiler, loggerType, startMessage, completeMessage, failMessage, startMessageArgs, endMessageArgs, failMessageArgs); + + public DisposableTimer? DebugDuration(string startMessage, object[]? startMessageArgs = null) + => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) + ? DebugDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs) + : null; + + public DisposableTimer? DebugDuration( + string startMessage, + string completeMessage, + string? failMessage = null, + int thresholdMilliseconds = 0, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null) + => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) + ? new DisposableTimer( + Logger, + LogLevel.Debug, + Profiler, + typeof(T), + startMessage, + completeMessage, + failMessage, + startMessageArgs, + endMessageArgs, + failMessageArgs, + thresholdMilliseconds) + : null; + + public DisposableTimer? DebugDuration( + Type loggerType, + string startMessage, + string completeMessage, + string? failMessage = null, + int thresholdMilliseconds = 0, + object[]? startMessageArgs = null, + object[]? endMessageArgs = null, + object[]? failMessageArgs = null) + => Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug) + ? new DisposableTimer( + Logger, + LogLevel.Debug, + Profiler, + loggerType, + startMessage, + completeMessage, + failMessage, + startMessageArgs, + endMessageArgs, + failMessageArgs, + thresholdMilliseconds) + : null; + + #region ILogger + + public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level) + => Logger.IsEnabled(level); + + public void LogCritical(Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.LogCritical(exception, messageTemplate, propertyValues); + + public void LogCritical(string messageTemplate, params object[] propertyValues) + => Logger.LogCritical(messageTemplate, propertyValues); + + public void LogError(Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.LogError(exception, messageTemplate, propertyValues); + + public void LogError(string messageTemplate, params object[] propertyValues) + => Logger.LogError(messageTemplate, propertyValues); + + public void LogWarning(string messageTemplate, params object[] propertyValues) + => Logger.LogWarning(messageTemplate, propertyValues); + + public void LogWarning(Exception exception, string messageTemplate, params object[] propertyValues) + => Logger.LogWarning(exception, messageTemplate, propertyValues); + + public void LogInformation(string messageTemplate, params object[] propertyValues) + => Logger.LogInformation(messageTemplate, propertyValues); + + public void LogDebug(string messageTemplate, params object[] propertyValues) + => Logger.LogDebug(messageTemplate, propertyValues); + + public void LogTrace(string messageTemplate, params object[] propertyValues) + => Logger.LogTrace(messageTemplate, propertyValues); + + #endregion } diff --git a/src/Umbraco.Core/Macros/IMacroRenderer.cs b/src/Umbraco.Core/Macros/IMacroRenderer.cs index bac3d36268..f1e7d8c383 100644 --- a/src/Umbraco.Core/Macros/IMacroRenderer.cs +++ b/src/Umbraco.Core/Macros/IMacroRenderer.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Macros +namespace Umbraco.Cms.Core.Macros; + +/// +/// Renders a macro +/// +public interface IMacroRenderer { - /// - /// Renders a macro - /// - public interface IMacroRenderer - { - Task RenderAsync(string macroAlias, IPublishedContent? content, IDictionary? macroParams); - } + Task RenderAsync(string macroAlias, IPublishedContent? content, IDictionary? macroParams); } diff --git a/src/Umbraco.Core/Macros/MacroContent.cs b/src/Umbraco.Core/Macros/MacroContent.cs index 7998b00fd7..c36c630168 100644 --- a/src/Umbraco.Core/Macros/MacroContent.cs +++ b/src/Umbraco.Core/Macros/MacroContent.cs @@ -1,20 +1,17 @@ -using System; +namespace Umbraco.Cms.Core.Macros; -namespace Umbraco.Cms.Core.Macros +// represents the content of a macro +public class MacroContent { - // represents the content of a macro - public class MacroContent - { - // gets or sets the text content - public string? Text { get; set; } + // gets an empty macro content + public static MacroContent Empty { get; } = new(); - // gets or sets the date the content was generated - public DateTime Date { get; set; } = DateTime.Now; + // gets or sets the text content + public string? Text { get; set; } - // a value indicating whether the content is empty - public bool IsEmpty => Text is null; + // gets or sets the date the content was generated + public DateTime Date { get; set; } = DateTime.Now; - // gets an empty macro content - public static MacroContent Empty { get; } = new MacroContent(); - } + // a value indicating whether the content is empty + public bool IsEmpty => Text is null; } diff --git a/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs b/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs index b3c505682a..49a53f11b0 100644 --- a/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs +++ b/src/Umbraco.Core/Macros/MacroErrorBehaviour.cs @@ -1,29 +1,28 @@ -namespace Umbraco.Cms.Core.Macros +namespace Umbraco.Cms.Core.Macros; + +public enum MacroErrorBehaviour { - public enum MacroErrorBehaviour - { - /// - /// Default umbraco behavior - show an inline error within the - /// macro but allow the page to continue rendering. - /// - Inline, + /// + /// Default umbraco behavior - show an inline error within the + /// macro but allow the page to continue rendering. + /// + Inline, - /// - /// Silently eat the error and do not display the offending macro. - /// - Silent, + /// + /// Silently eat the error and do not display the offending macro. + /// + Silent, - /// - /// Throw an exception which can be caught by the global error handler - /// defined in Application_OnError. If no such error handler is defined - /// then you'll see the Yellow Screen Of Death (YSOD) error page. - /// - Throw, + /// + /// Throw an exception which can be caught by the global error handler + /// defined in Application_OnError. If no such error handler is defined + /// then you'll see the Yellow Screen Of Death (YSOD) error page. + /// + Throw, - /// - /// Silently eat the error and display the custom content reported in - /// the error event args - /// - Content - } + /// + /// Silently eat the error and display the custom content reported in + /// the error event args + /// + Content, } diff --git a/src/Umbraco.Core/Macros/MacroModel.cs b/src/Umbraco.Core/Macros/MacroModel.cs index 5242b14d86..12649bf91c 100644 --- a/src/Umbraco.Core/Macros/MacroModel.cs +++ b/src/Umbraco.Core/Macros/MacroModel.cs @@ -1,57 +1,61 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Macros +namespace Umbraco.Cms.Core.Macros; + +public class MacroModel { - public class MacroModel + public MacroModel() { - /// - /// The Macro Id - /// - public int Id { get; set; } + } - /// - /// The Macro Name - /// - public string? Name { get; set; } - - /// - /// The Macro Alias - /// - public string? Alias { get; set; } - - public string? MacroSource { get; set; } - - public int CacheDuration { get; set; } - - public bool CacheByPage { get; set; } - - public bool CacheByMember { get; set; } - - public bool RenderInEditor { get; set; } - - public string? CacheIdentifier { get; set; } - - public List Properties { get; } = new List(); - - public MacroModel() - { } - - public MacroModel(IMacro macro) + public MacroModel(IMacro? macro) + { + if (macro == null) { - if (macro == null) return; + return; + } - Id = macro.Id; - Name = macro.Name; - Alias = macro.Alias; - MacroSource = macro.MacroSource; - CacheDuration = macro.CacheDuration; - CacheByPage = macro.CacheByPage; - CacheByMember = macro.CacheByMember; - RenderInEditor = macro.UseInEditor; + Id = macro.Id; + Name = macro.Name; + Alias = macro.Alias; + MacroSource = macro.MacroSource; + CacheDuration = macro.CacheDuration; + CacheByPage = macro.CacheByPage; + CacheByMember = macro.CacheByMember; + RenderInEditor = macro.UseInEditor; - foreach (var prop in macro.Properties) - Properties.Add(new MacroPropertyModel(prop.Alias, string.Empty, prop.EditorAlias)); + foreach (IMacroProperty prop in macro.Properties) + { + Properties.Add(new MacroPropertyModel(prop.Alias, string.Empty, prop.EditorAlias)); } } + + /// + /// The Macro Id + /// + public int Id { get; set; } + + /// + /// The Macro Name + /// + public string? Name { get; set; } + + /// + /// The Macro Alias + /// + public string? Alias { get; set; } + + public string? MacroSource { get; set; } + + public int CacheDuration { get; set; } + + public bool CacheByPage { get; set; } + + public bool CacheByMember { get; set; } + + public bool RenderInEditor { get; set; } + + public string? CacheIdentifier { get; set; } + + public List Properties { get; } = new(); } diff --git a/src/Umbraco.Core/Macros/MacroPropertyModel.cs b/src/Umbraco.Core/Macros/MacroPropertyModel.cs index 643d154f21..c1022c3561 100644 --- a/src/Umbraco.Core/Macros/MacroPropertyModel.cs +++ b/src/Umbraco.Core/Macros/MacroPropertyModel.cs @@ -1,29 +1,25 @@ -namespace Umbraco.Cms.Core.Macros +namespace Umbraco.Cms.Core.Macros; + +public class MacroPropertyModel { - public class MacroPropertyModel + public MacroPropertyModel() => Key = string.Empty; + + public MacroPropertyModel(string key, string value) { - public string Key { get; set; } - - public string? Value { get; set; } - - public string? Type { get; set; } - - public MacroPropertyModel() - { - Key = string.Empty; - } - - public MacroPropertyModel(string key, string value) - { - Key = key; - Value = value; - } - - public MacroPropertyModel(string key, string value, string type) - { - Key = key; - Value = value; - Type = type; - } + Key = key; + Value = value; } + + public MacroPropertyModel(string key, string value, string type) + { + Key = key; + Value = value; + Type = type; + } + + public string Key { get; set; } + + public string? Value { get; set; } + + public string? Type { get; set; } } diff --git a/src/Umbraco.Core/Mail/IEmailSender.cs b/src/Umbraco.Core/Mail/IEmailSender.cs index 0c573c542c..2eb8cc8263 100644 --- a/src/Umbraco.Core/Mail/IEmailSender.cs +++ b/src/Umbraco.Core/Mail/IEmailSender.cs @@ -1,17 +1,15 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Mail +namespace Umbraco.Cms.Core.Mail; + +/// +/// Simple abstraction to send an email message +/// +public interface IEmailSender { - /// - /// Simple abstraction to send an email message - /// - public interface IEmailSender - { - Task SendAsync(EmailMessage message, string emailType); + Task SendAsync(EmailMessage message, string emailType); - Task SendAsync(EmailMessage message, string emailType, bool enableNotification); + Task SendAsync(EmailMessage message, string emailType, bool enableNotification); - bool CanSendRequiredEmail(); - } + bool CanSendRequiredEmail(); } diff --git a/src/Umbraco.Core/Mail/ISmsSender.cs b/src/Umbraco.Core/Mail/ISmsSender.cs index 885ad89da2..3c09bdc7e6 100644 --- a/src/Umbraco.Core/Mail/ISmsSender.cs +++ b/src/Umbraco.Core/Mail/ISmsSender.cs @@ -1,14 +1,10 @@ -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Mail; -namespace Umbraco.Cms.Core.Mail +/// +/// Service to send an SMS +/// +public interface ISmsSender { - /// - /// Service to send an SMS - /// - public interface ISmsSender - { - // borrowed from https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/common/samples/WebApplication1/Services/ISmsSender.cs#L8 - - Task SendSmsAsync(string number, string message); - } + // borrowed from https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/common/samples/WebApplication1/Services/ISmsSender.cs#L8 + Task SendSmsAsync(string number, string message); } diff --git a/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs b/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs index 15e36767d9..5b1fa0923a 100644 --- a/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs +++ b/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs @@ -1,19 +1,18 @@ -using System; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Mail +namespace Umbraco.Cms.Core.Mail; + +internal class NotImplementedEmailSender : IEmailSender { - internal class NotImplementedEmailSender : IEmailSender - { - public Task SendAsync(EmailMessage message, string emailType) - => throw new NotImplementedException("To send an Email ensure IEmailSender is implemented with a custom implementation"); + public Task SendAsync(EmailMessage message, string emailType) + => throw new NotImplementedException( + "To send an Email ensure IEmailSender is implemented with a custom implementation"); - public Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => - throw new NotImplementedException( - "To send an Email ensure IEmailSender is implemented with a custom implementation"); + public Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => + throw new NotImplementedException( + "To send an Email ensure IEmailSender is implemented with a custom implementation"); - public bool CanSendRequiredEmail() - => throw new NotImplementedException("To send an Email ensure IEmailSender is implemented with a custom implementation"); - } + public bool CanSendRequiredEmail() + => throw new NotImplementedException( + "To send an Email ensure IEmailSender is implemented with a custom implementation"); } diff --git a/src/Umbraco.Core/Mail/NotImplementedSmsSender.cs b/src/Umbraco.Core/Mail/NotImplementedSmsSender.cs index 0cb5016a1b..b3901d5ab9 100644 --- a/src/Umbraco.Core/Mail/NotImplementedSmsSender.cs +++ b/src/Umbraco.Core/Mail/NotImplementedSmsSender.cs @@ -1,14 +1,11 @@ -using System; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Mail; -namespace Umbraco.Cms.Core.Mail +/// +/// An that throws +/// +internal class NotImplementedSmsSender : ISmsSender { - /// - /// An that throws - /// - internal class NotImplementedSmsSender : ISmsSender - { - public Task SendSmsAsync(string number, string message) - => throw new NotImplementedException("To send an SMS ensure ISmsSender is implemented with a custom implementation"); - } + public Task SendSmsAsync(string number, string message) + => throw new NotImplementedException( + "To send an SMS ensure ISmsSender is implemented with a custom implementation"); } diff --git a/src/Umbraco.Core/Manifest/BundleOptions.cs b/src/Umbraco.Core/Manifest/BundleOptions.cs index 810efb6c45..fe04c205d9 100644 --- a/src/Umbraco.Core/Manifest/BundleOptions.cs +++ b/src/Umbraco.Core/Manifest/BundleOptions.cs @@ -1,26 +1,25 @@ -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +public enum BundleOptions { - public enum BundleOptions - { - /// - /// The default bundling behavior for assets in the package folder. - /// - /// - /// The assets will be bundled with the typical packages bundle. - /// - Default = 0, + /// + /// The default bundling behavior for assets in the package folder. + /// + /// + /// The assets will be bundled with the typical packages bundle. + /// + Default = 0, - /// - /// The assets in the package will not be processed at all and will all be requested as individual assets. - /// - /// - /// This will essentially be a bundle that has composite processing turned off for both debug and production. - /// - None = 1, + /// + /// The assets in the package will not be processed at all and will all be requested as individual assets. + /// + /// + /// This will essentially be a bundle that has composite processing turned off for both debug and production. + /// + None = 1, - /// - /// The packages assets will be processed as it's own separate bundle. (in debug, files will not be processed) - /// - Independent = 2 - } + /// + /// The packages assets will be processed as it's own separate bundle. (in debug, files will not be processed) + /// + Independent = 2, } diff --git a/src/Umbraco.Core/Manifest/CompositePackageManifest.cs b/src/Umbraco.Core/Manifest/CompositePackageManifest.cs index 939d635fc3..5e41681ea6 100644 --- a/src/Umbraco.Core/Manifest/CompositePackageManifest.cs +++ b/src/Umbraco.Core/Manifest/CompositePackageManifest.cs @@ -1,67 +1,63 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// A package manifest made up of all combined manifests +/// +public class CompositePackageManifest { + public CompositePackageManifest( + IReadOnlyList propertyEditors, + IReadOnlyList parameterEditors, + IReadOnlyList gridEditors, + IReadOnlyList contentApps, + IReadOnlyList dashboards, + IReadOnlyList sections, + IReadOnlyDictionary> scripts, + IReadOnlyDictionary> stylesheets) + { + PropertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + ParameterEditors = parameterEditors ?? throw new ArgumentNullException(nameof(parameterEditors)); + GridEditors = gridEditors ?? throw new ArgumentNullException(nameof(gridEditors)); + ContentApps = contentApps ?? throw new ArgumentNullException(nameof(contentApps)); + Dashboards = dashboards ?? throw new ArgumentNullException(nameof(dashboards)); + Sections = sections ?? throw new ArgumentNullException(nameof(sections)); + Scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); + Stylesheets = stylesheets ?? throw new ArgumentNullException(nameof(stylesheets)); + } /// - /// A package manifest made up of all combined manifests + /// Gets or sets the property editors listed in the manifest. /// - public class CompositePackageManifest - { - public CompositePackageManifest( - IReadOnlyList propertyEditors, - IReadOnlyList parameterEditors, - IReadOnlyList gridEditors, - IReadOnlyList contentApps, - IReadOnlyList dashboards, - IReadOnlyList sections, - IReadOnlyDictionary> scripts, - IReadOnlyDictionary> stylesheets) - { - PropertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); - ParameterEditors = parameterEditors ?? throw new ArgumentNullException(nameof(parameterEditors)); - GridEditors = gridEditors ?? throw new ArgumentNullException(nameof(gridEditors)); - ContentApps = contentApps ?? throw new ArgumentNullException(nameof(contentApps)); - Dashboards = dashboards ?? throw new ArgumentNullException(nameof(dashboards)); - Sections = sections ?? throw new ArgumentNullException(nameof(sections)); - Scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); - Stylesheets = stylesheets ?? throw new ArgumentNullException(nameof(stylesheets)); - } + public IReadOnlyList PropertyEditors { get; } - /// - /// Gets or sets the property editors listed in the manifest. - /// - public IReadOnlyList PropertyEditors { get; } + /// + /// Gets or sets the parameter editors listed in the manifest. + /// + public IReadOnlyList ParameterEditors { get; } - /// - /// Gets or sets the parameter editors listed in the manifest. - /// - public IReadOnlyList ParameterEditors { get; } + /// + /// Gets or sets the grid editors listed in the manifest. + /// + public IReadOnlyList GridEditors { get; } - /// - /// Gets or sets the grid editors listed in the manifest. - /// - public IReadOnlyList GridEditors { get; } + /// + /// Gets or sets the content apps listed in the manifest. + /// + public IReadOnlyList ContentApps { get; } - /// - /// Gets or sets the content apps listed in the manifest. - /// - public IReadOnlyList ContentApps { get; } + /// + /// Gets or sets the dashboards listed in the manifest. + /// + public IReadOnlyList Dashboards { get; } - /// - /// Gets or sets the dashboards listed in the manifest. - /// - public IReadOnlyList Dashboards { get; } + /// + /// Gets or sets the sections listed in the manifest. + /// + public IReadOnlyCollection Sections { get; } - /// - /// Gets or sets the sections listed in the manifest. - /// - public IReadOnlyCollection Sections { get; } + public IReadOnlyDictionary> Scripts { get; } - public IReadOnlyDictionary> Scripts { get; } - - public IReadOnlyDictionary> Stylesheets { get; } - } + public IReadOnlyDictionary> Stylesheets { get; } } diff --git a/src/Umbraco.Core/Manifest/IManifestFilter.cs b/src/Umbraco.Core/Manifest/IManifestFilter.cs index 0984f1a889..d2998a0839 100644 --- a/src/Umbraco.Core/Manifest/IManifestFilter.cs +++ b/src/Umbraco.Core/Manifest/IManifestFilter.cs @@ -1,19 +1,16 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.Manifest +/// +/// Provides filtering for package manifests. +/// +public interface IManifestFilter { /// - /// Provides filtering for package manifests. + /// Filters package manifests. /// - public interface IManifestFilter - { - /// - /// Filters package manifests. - /// - /// The package manifests. - /// - /// It is possible to remove, change, or add manifests. - /// - void Filter(List manifests); - } + /// The package manifests. + /// + /// It is possible to remove, change, or add manifests. + /// + void Filter(List manifests); } diff --git a/src/Umbraco.Core/Manifest/IManifestParser.cs b/src/Umbraco.Core/Manifest/IManifestParser.cs index 09d3ccbe1c..f8b29e9f56 100644 --- a/src/Umbraco.Core/Manifest/IManifestParser.cs +++ b/src/Umbraco.Core/Manifest/IManifestParser.cs @@ -1,26 +1,23 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.Manifest +public interface IManifestParser { - public interface IManifestParser - { - string AppPluginsPath { get; set; } + string AppPluginsPath { get; set; } - /// - /// Gets all manifests, merged into a single manifest object. - /// - /// - CompositePackageManifest CombinedManifest { get; } + /// + /// Gets all manifests, merged into a single manifest object. + /// + /// + CompositePackageManifest CombinedManifest { get; } - /// - /// Parses a manifest. - /// - PackageManifest ParseManifest(string text); + /// + /// Parses a manifest. + /// + PackageManifest ParseManifest(string text); - /// - /// Returns all package individual manifests - /// - /// - IEnumerable GetManifests(); - } + /// + /// Returns all package individual manifests + /// + /// + IEnumerable GetManifests(); } diff --git a/src/Umbraco.Core/Manifest/IPackageManifest.cs b/src/Umbraco.Core/Manifest/IPackageManifest.cs index 39e4878233..ba911b183c 100644 --- a/src/Umbraco.Core/Manifest/IPackageManifest.cs +++ b/src/Umbraco.Core/Manifest/IPackageManifest.cs @@ -1,65 +1,66 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +public interface IPackageManifest { - public interface IPackageManifest - { - /// - /// Gets the source path of the manifest. - /// - /// - /// Gets the full absolute file path of the manifest, - /// using system directory separators. - /// - string Source { get; set; } + /// + /// Gets the source path of the manifest. + /// + /// + /// + /// Gets the full absolute file path of the manifest, + /// using system directory separators. + /// + /// + string Source { get; set; } - /// - /// Gets or sets the scripts listed in the manifest. - /// - [DataMember(Name = "javascript")] - string[] Scripts { get; set; } + /// + /// Gets or sets the scripts listed in the manifest. + /// + [DataMember(Name = "javascript")] + string[] Scripts { get; set; } - /// - /// Gets or sets the stylesheets listed in the manifest. - /// - [DataMember(Name = "css")] - string[] Stylesheets { get; set; } + /// + /// Gets or sets the stylesheets listed in the manifest. + /// + [DataMember(Name = "css")] + string[] Stylesheets { get; set; } - /// - /// Gets or sets the property editors listed in the manifest. - /// - [DataMember(Name = "propertyEditors")] - IDataEditor[] PropertyEditors { get; set; } + /// + /// Gets or sets the property editors listed in the manifest. + /// + [DataMember(Name = "propertyEditors")] + IDataEditor[] PropertyEditors { get; set; } - /// - /// Gets or sets the parameter editors listed in the manifest. - /// - [DataMember(Name = "parameterEditors")] - IDataEditor[] ParameterEditors { get; set; } + /// + /// Gets or sets the parameter editors listed in the manifest. + /// + [DataMember(Name = "parameterEditors")] + IDataEditor[] ParameterEditors { get; set; } - /// - /// Gets or sets the grid editors listed in the manifest. - /// - [DataMember(Name = "gridEditors")] - GridEditor[] GridEditors { get; set; } + /// + /// Gets or sets the grid editors listed in the manifest. + /// + [DataMember(Name = "gridEditors")] + GridEditor[] GridEditors { get; set; } - /// - /// Gets or sets the content apps listed in the manifest. - /// - [DataMember(Name = "contentApps")] - ManifestContentAppDefinition[] ContentApps { get; set; } + /// + /// Gets or sets the content apps listed in the manifest. + /// + [DataMember(Name = "contentApps")] + ManifestContentAppDefinition[] ContentApps { get; set; } - /// - /// Gets or sets the dashboards listed in the manifest. - /// - [DataMember(Name = "dashboards")] - ManifestDashboard[] Dashboards { get; set; } + /// + /// Gets or sets the dashboards listed in the manifest. + /// + [DataMember(Name = "dashboards")] + ManifestDashboard[] Dashboards { get; set; } - /// - /// Gets or sets the sections listed in the manifest. - /// - [DataMember(Name = "sections")] - ManifestSection[] Sections { get; set; } - } + /// + /// Gets or sets the sections listed in the manifest. + /// + [DataMember(Name = "sections")] + ManifestSection[] Sections { get; set; } } diff --git a/src/Umbraco.Core/Manifest/ManifestAssets.cs b/src/Umbraco.Core/Manifest/ManifestAssets.cs index 6532e2f63d..2bd84a1bdd 100644 --- a/src/Umbraco.Core/Manifest/ManifestAssets.cs +++ b/src/Umbraco.Core/Manifest/ManifestAssets.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.Manifest +public class ManifestAssets { - public class ManifestAssets + public ManifestAssets(string? packageName, IReadOnlyList assets) { - public ManifestAssets(string? packageName, IReadOnlyList assets) - { - PackageName = packageName ?? throw new ArgumentNullException(nameof(packageName)); - Assets = assets ?? throw new ArgumentNullException(nameof(assets)); - } - - public string PackageName { get; } - public IReadOnlyList Assets { get; } + PackageName = packageName ?? throw new ArgumentNullException(nameof(packageName)); + Assets = assets ?? throw new ArgumentNullException(nameof(assets)); } + + public string PackageName { get; } + + public IReadOnlyList Assets { get; } } diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs index ed44742bc0..5bfc2a740e 100644 --- a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs +++ b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs @@ -1,75 +1,72 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +// contentApps: [ +// { +// name: 'App Name', // required +// alias: 'appAlias', // required +// weight: 0, // optional, default is 0, use values between -99 and +99 +// icon: 'icon.app', // required +// view: 'path/view.htm', // required +// show: [ // optional, default is always show +// '-content/foo', // hide for content type 'foo' +// '+content/*', // show for all other content types +// '+media/*', // show for all media types +// '+role/admin' // show for admin users. Role based permissions will override others. +// ] +// }, +// ... +// ] + +/// +/// Represents a content app definition, parsed from a manifest. +/// +/// Is used to create an actual . +[DataContract(Name = "appdef", Namespace = "")] +public class ManifestContentAppDefinition { - // contentApps: [ - // { - // name: 'App Name', // required - // alias: 'appAlias', // required - // weight: 0, // optional, default is 0, use values between -99 and +99 - // icon: 'icon.app', // required - // view: 'path/view.htm', // required - // show: [ // optional, default is always show - // '-content/foo', // hide for content type 'foo' - // '+content/*', // show for all other content types - // '+media/*', // show for all media types - // '+role/admin' // show for admin users. Role based permissions will override others. - // ] - // }, - // ... - // ] + private readonly string? _view; /// - /// Represents a content app definition, parsed from a manifest. + /// Gets or sets the name of the content app. /// - /// Is used to create an actual . - [DataContract(Name = "appdef", Namespace = "")] - public class ManifestContentAppDefinition - { - private string? _view; + [DataMember(Name = "name")] + public string? Name { get; set; } - /// - /// Gets or sets the name of the content app. - /// - [DataMember(Name = "name")] - public string? Name { get; set; } + /// + /// Gets or sets the unique alias of the content app. + /// + /// + /// Must be a valid javascript identifier, ie no spaces etc. + /// + [DataMember(Name = "alias")] + public string? Alias { get; set; } - /// - /// Gets or sets the unique alias of the content app. - /// - /// - /// Must be a valid javascript identifier, ie no spaces etc. - /// - [DataMember(Name = "alias")] - public string? Alias { get; set; } + /// + /// Gets or sets the weight of the content app. + /// + [DataMember(Name = "weight")] + public int Weight { get; set; } - /// - /// Gets or sets the weight of the content app. - /// - [DataMember(Name = "weight")] - public int Weight { get; set; } + /// + /// Gets or sets the icon of the content app. + /// + /// + /// Must be a valid helveticons class name (see http://hlvticons.ch/). + /// + [DataMember(Name = "icon")] + public string? Icon { get; set; } - /// - /// Gets or sets the icon of the content app. - /// - /// - /// Must be a valid helveticons class name (see http://hlvticons.ch/). - /// - [DataMember(Name = "icon")] - public string? Icon { get; set; } + /// + /// Gets or sets the view for rendering the content app. + /// + [DataMember(Name = "view")] + public string? View { get; set; } - /// - /// Gets or sets the view for rendering the content app. - /// - [DataMember(Name = "view")] - public string? View { get; set; } - - /// - /// Gets or sets the list of 'show' conditions for the content app. - /// - [DataMember(Name = "show")] - public string[] Show { get; set; } = Array.Empty(); - - } + /// + /// Gets or sets the list of 'show' conditions for the content app. + /// + [DataMember(Name = "show")] + public string[] Show { get; set; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs index c4bc87e9a2..122ecc1cb7 100644 --- a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs +++ b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -8,182 +5,202 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +// contentApps: [ +// { +// name: 'App Name', // required +// alias: 'appAlias', // required +// weight: 0, // optional, default is 0, use values between -99 and +99 +// icon: 'icon.app', // required +// view: 'path/view.htm', // required +// show: [ // optional, default is always show +// '-content/foo', // hide for content type 'foo' +// '+content/*', // show for all other content types +// '+media/*', // show for all media types +// '-member/foo' // hide for member type 'foo' +// '+member/*' // show for all member types +// '+role/admin' // show for admin users. Role based permissions will override others. +// ] +// }, +// ... +// ] + +/// +/// Represents a content app factory, for content apps parsed from the manifest. +/// +public class ManifestContentAppFactory : IContentAppFactory { - // contentApps: [ - // { - // name: 'App Name', // required - // alias: 'appAlias', // required - // weight: 0, // optional, default is 0, use values between -99 and +99 - // icon: 'icon.app', // required - // view: 'path/view.htm', // required - // show: [ // optional, default is always show - // '-content/foo', // hide for content type 'foo' - // '+content/*', // show for all other content types - // '+media/*', // show for all media types - // '-member/foo' // hide for member type 'foo' - // '+member/*' // show for all member types - // '+role/admin' // show for admin users. Role based permissions will override others. - // ] - // }, - // ... - // ] + private readonly ManifestContentAppDefinition _definition; + private readonly IIOHelper _ioHelper; - /// - /// Represents a content app factory, for content apps parsed from the manifest. - /// - public class ManifestContentAppFactory : IContentAppFactory + private ContentApp? _app; + private ShowRule[]? _showRules; + + public ManifestContentAppFactory(ManifestContentAppDefinition definition, IIOHelper ioHelper) { - private readonly ManifestContentAppDefinition _definition; - private readonly IIOHelper _ioHelper; + _definition = definition; + _ioHelper = ioHelper; + } - public ManifestContentAppFactory(ManifestContentAppDefinition definition, IIOHelper ioHelper) + /// + public ContentApp? GetContentAppFor(object o, IEnumerable userGroups) + { + string? partA, partB; + + switch (o) { - _definition = definition; - _ioHelper = ioHelper; + case IContent content: + partA = "content"; + partB = content.ContentType.Alias; + break; + + case IMedia media: + partA = "media"; + partB = media.ContentType.Alias; + break; + case IMember member: + partA = "member"; + partB = member.ContentType.Alias; + break; + case IContentType contentType: + partA = "contentType"; + partB = contentType?.Alias; + break; + case IDictionaryItem _: + partA = "dictionary"; + partB = "*"; // Not really a different type for dictionary items + break; + + default: + return null; } - private ContentApp? _app; - private ShowRule[]? _showRules; + ShowRule[] rules = _showRules ??= ShowRule.Parse(_definition.Show).ToArray(); + var userGroupsList = userGroups.ToList(); - /// - public ContentApp? GetContentAppFor(object o,IEnumerable userGroups) + var okRole = false; + var hasRole = false; + var okType = false; + var hasType = false; + + foreach (ShowRule rule in rules) { - string? partA, partB; - - switch (o) + if (rule.PartA?.InvariantEquals("role") ?? false) { - case IContent content: - partA = "content"; - partB = content.ContentType.Alias; - break; - - case IMedia media: - partA = "media"; - partB = media.ContentType.Alias; - break; - case IMember member: - partA = "member"; - partB = member.ContentType.Alias; - break; - case IContentType contentType: - partA = "contentType"; - partB = contentType?.Alias; - break; - case IDictionaryItem _: - partA = "dictionary"; - partB = "*"; //Not really a different type for dictionary items - break; - - default: - return null; - } - - var rules = _showRules ?? (_showRules = ShowRule.Parse(_definition.Show).ToArray()); - var userGroupsList = userGroups.ToList(); - - var okRole = false; - var hasRole = false; - var okType = false; - var hasType = false; - - foreach (var rule in rules) - { - if (rule.PartA?.InvariantEquals("role") ?? false) + // if roles have been ok-ed already, skip the rule + if (okRole) { - // if roles have been ok-ed already, skip the rule - if (okRole) - continue; - - // remember we have role rules - hasRole = true; - - foreach (var group in userGroupsList) - { - // if the entry does not apply, skip - if (!rule.Matches("role", group.Alias)) - continue; - - // if the entry applies, - // if it's an exclude entry, exit, do not display the content app - if (!rule.Show) - return null; - - // else ok to display, remember roles are ok, break from userGroupsList - okRole = rule.Show; - break; - } + continue; } - else // it is a type rule + + // remember we have role rules + hasRole = true; + + foreach (IReadOnlyUserGroup group in userGroupsList) { - // if type has been ok-ed already, skip the rule - if (okType) - continue; - - // remember we have type rules - hasType = true; - - // if the entry does not apply, skip it - if (!rule.Matches(partA, partB)) + // if the entry does not apply, skip + if (!rule.Matches("role", group.Alias)) + { continue; + } // if the entry applies, // if it's an exclude entry, exit, do not display the content app if (!rule.Show) - return null; - - // else ok to display, remember type rules are ok - okType = true; - } - } - - // if roles rules are specified but not ok, - // or if type roles are specified but not ok, - // cannot display the content app - if ((hasRole && !okRole) || (hasType && !okType)) - return null; - - // else - // content app can be displayed - return _app ??= new ContentApp - { - Alias = _definition.Alias, - Name = _definition.Name, - Icon = _definition.Icon, - View = _ioHelper.ResolveRelativeOrVirtualUrl(_definition.View), - Weight = _definition.Weight - }; - } - - private class ShowRule - { - private static readonly Regex ShowRegex = new Regex("^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public bool Show { get; private set; } - public string? PartA { get; private set; } - public string? PartB { get; private set; } - - public bool Matches(string? partA, string? partB) - { - return (PartA == "*" || (PartA?.InvariantEquals(partA) ?? false)) && (PartB == "*" || (PartB?.InvariantEquals(partB) ?? false)); - } - - public static IEnumerable Parse(string[] rules) - { - foreach (var rule in rules) - { - var match = ShowRegex.Match(rule); - if (!match.Success) - throw new FormatException($"Illegal 'show' entry \"{rule}\" in manifest."); - - yield return new ShowRule { - Show = match.Groups[1].Value != "-", - PartA = match.Groups[2].Value, - PartB = match.Groups[3].Value - }; + return null; + } + + // else ok to display, remember roles are ok, break from userGroupsList + okRole = rule.Show; + break; } } + + // it is a type rule + else + { + // if type has been ok-ed already, skip the rule + if (okType) + { + continue; + } + + // remember we have type rules + hasType = true; + + // if the entry does not apply, skip it + if (!rule.Matches(partA, partB)) + { + continue; + } + + // if the entry applies, + // if it's an exclude entry, exit, do not display the content app + if (!rule.Show) + { + return null; + } + + // else ok to display, remember type rules are ok + okType = true; + } } + + // if roles rules are specified but not ok, + // or if type roles are specified but not ok, + // cannot display the content app + if ((hasRole && !okRole) || (hasType && !okType)) + { + return null; + } + + // else + // content app can be displayed + return _app ??= new ContentApp + { + Alias = _definition.Alias, + Name = _definition.Name, + Icon = _definition.Icon, + View = _ioHelper.ResolveRelativeOrVirtualUrl(_definition.View), + Weight = _definition.Weight, + }; + } + + private class ShowRule + { + private static readonly Regex ShowRegex = new( + "^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public bool Show { get; private set; } + + public string? PartA { get; private set; } + + public string? PartB { get; private set; } + + public static IEnumerable Parse(string[] rules) + { + foreach (var rule in rules) + { + Match match = ShowRegex.Match(rule); + if (!match.Success) + { + throw new FormatException($"Illegal 'show' entry \"{rule}\" in manifest."); + } + + yield return new ShowRule + { + Show = match.Groups[1].Value != "-", + PartA = match.Groups[2].Value, + PartB = match.Groups[3].Value, + }; + } + } + + public bool Matches(string? partA, string? partB) => + (PartA == "*" || (PartA?.InvariantEquals(partA) ?? false)) && + (PartB == "*" || (PartB?.InvariantEquals(partB) ?? false)); } } diff --git a/src/Umbraco.Core/Manifest/ManifestDashboard.cs b/src/Umbraco.Core/Manifest/ManifestDashboard.cs index a10c3a2177..75cdf24ebe 100644 --- a/src/Umbraco.Core/Manifest/ManifestDashboard.cs +++ b/src/Umbraco.Core/Manifest/ManifestDashboard.cs @@ -1,25 +1,23 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Dashboards; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +[DataContract] +public class ManifestDashboard : IDashboard { - [DataContract] - public class ManifestDashboard : IDashboard - { - [DataMember(Name = "alias", IsRequired = true)] - public string Alias { get; set; } = null!; + [DataMember(Name = "weight")] + public int Weight { get; set; } = 100; - [DataMember(Name = "weight")] - public int Weight { get; set; } = 100; + [DataMember(Name = "alias", IsRequired = true)] + public string Alias { get; set; } = null!; - [DataMember(Name = "view", IsRequired = true)] - public string View { get; set; } = null!; + [DataMember(Name = "view", IsRequired = true)] + public string View { get; set; } = null!; - [DataMember(Name = "sections")] - public string[] Sections { get; set; } = Array.Empty(); + [DataMember(Name = "sections")] + public string[] Sections { get; set; } = Array.Empty(); - [DataMember(Name = "access")] - public IAccessRule[] AccessRules { get; set; } = Array.Empty(); - } + [DataMember(Name = "access")] + public IAccessRule[] AccessRules { get; set; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs index 9c692f69b3..a1d5cac0c1 100644 --- a/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs @@ -1,26 +1,26 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Manifest -{ - /// - /// Contains the manifest filters. - /// - public class ManifestFilterCollection : BuilderCollectionBase - { - public ManifestFilterCollection(Func> items) : base(items) - { - } +namespace Umbraco.Cms.Core.Manifest; - /// - /// Filters package manifests. - /// - /// The package manifests. - public void Filter(List manifests) +/// +/// Contains the manifest filters. +/// +public class ManifestFilterCollection : BuilderCollectionBase +{ + public ManifestFilterCollection(Func> items) + : base(items) + { + } + + /// + /// Filters package manifests. + /// + /// The package manifests. + public void Filter(List manifests) + { + foreach (IManifestFilter filter in this) { - foreach (var filter in this) - filter.Filter(manifests); + filter.Filter(manifests); } } } diff --git a/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs index 00ac3609dd..5f012d10c9 100644 --- a/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs @@ -1,13 +1,13 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Manifest -{ - public class ManifestFilterCollectionBuilder : OrderedCollectionBuilderBase - { - protected override ManifestFilterCollectionBuilder This => this; +namespace Umbraco.Cms.Core.Manifest; - // do NOT cache this, it's only used once - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; - } +public class ManifestFilterCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override ManifestFilterCollectionBuilder This => this; + + // do NOT cache this, it's only used once + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; } diff --git a/src/Umbraco.Core/Manifest/ManifestSection.cs b/src/Umbraco.Core/Manifest/ManifestSection.cs index 864a0734e2..c7671c91e2 100644 --- a/src/Umbraco.Core/Manifest/ManifestSection.cs +++ b/src/Umbraco.Core/Manifest/ManifestSection.cs @@ -1,15 +1,14 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Sections; -namespace Umbraco.Cms.Core.Manifest -{ - [DataContract(Name = "section", Namespace = "")] - public class ManifestSection : ISection - { - [DataMember(Name = "alias")] - public string Alias { get; set; } = string.Empty; +namespace Umbraco.Cms.Core.Manifest; - [DataMember(Name = "name")] - public string Name { get; set; } = string.Empty; - } +[DataContract(Name = "section", Namespace = "")] +public class ManifestSection : ISection +{ + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; + + [DataMember(Name = "name")] + public string Name { get; set; } = string.Empty; } diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index a71cf1f6f6..7bf07cfde9 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -1,115 +1,115 @@ -using System; -using System.IO; using System.Runtime.Serialization; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Represents the content of a package manifest. +/// +[DataContract] +public class PackageManifest { + private string? _packageName; /// - /// Represents the content of a package manifest. + /// An optional package name. If not specified then the directory name is used. /// - [DataContract] - public class PackageManifest + [DataMember(Name = "name")] + public string? PackageName { - private string? _packageName; - - /// - /// An optional package name. If not specified then the directory name is used. - /// - [DataMember(Name = "name")] - public string? PackageName + get { - get + if (!_packageName.IsNullOrWhiteSpace()) { - if (!_packageName.IsNullOrWhiteSpace()) - { - return _packageName; - } - if (!Source.IsNullOrWhiteSpace()) - { - _packageName = Path.GetFileName(Path.GetDirectoryName(Source)); - } return _packageName; } - set => _packageName = value; + + if (!Source.IsNullOrWhiteSpace()) + { + _packageName = Path.GetFileName(Path.GetDirectoryName(Source)); + } + + return _packageName; } - - [DataMember(Name = "packageView")] - public string? PackageView { get; set; } - - /// - /// Gets the source path of the manifest. - /// - /// - /// Gets the full absolute file path of the manifest, - /// using system directory separators. - /// - [IgnoreDataMember] - public string Source { get; set; } = null!; - - /// - /// Gets or sets the version of the package - /// - [DataMember(Name = "version")] - public string Version { get; set; } = string.Empty; - - /// - /// Gets or sets a value indicating whether telemetry is allowed - /// - [DataMember(Name = "allowPackageTelemetry")] - public bool AllowPackageTelemetry { get; set; } = true; - - [DataMember(Name = "bundleOptions")] - public BundleOptions BundleOptions { get; set; } - - /// - /// Gets or sets the scripts listed in the manifest. - /// - [DataMember(Name = "javascript")] - public string[] Scripts { get; set; } = Array.Empty(); - - /// - /// Gets or sets the stylesheets listed in the manifest. - /// - [DataMember(Name = "css")] - public string[] Stylesheets { get; set; } = Array.Empty(); - - /// - /// Gets or sets the property editors listed in the manifest. - /// - [DataMember(Name = "propertyEditors")] - public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); - - /// - /// Gets or sets the parameter editors listed in the manifest. - /// - [DataMember(Name = "parameterEditors")] - public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); - - /// - /// Gets or sets the grid editors listed in the manifest. - /// - [DataMember(Name = "gridEditors")] - public GridEditor[] GridEditors { get; set; } = Array.Empty(); - - /// - /// Gets or sets the content apps listed in the manifest. - /// - [DataMember(Name = "contentApps")] - public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); - - /// - /// Gets or sets the dashboards listed in the manifest. - /// - [DataMember(Name = "dashboards")] - public ManifestDashboard[] Dashboards { get; set; } = Array.Empty(); - - /// - /// Gets or sets the sections listed in the manifest. - /// - [DataMember(Name = "sections")] - public ManifestSection[] Sections { get; set; } = Array.Empty(); + set => _packageName = value; } + + [DataMember(Name = "packageView")] + public string? PackageView { get; set; } + + /// + /// Gets the source path of the manifest. + /// + /// + /// + /// Gets the full absolute file path of the manifest, + /// using system directory separators. + /// + /// + [IgnoreDataMember] + public string Source { get; set; } = null!; + + /// + /// Gets or sets the version of the package + /// + [DataMember(Name = "version")] + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether telemetry is allowed + /// + [DataMember(Name = "allowPackageTelemetry")] + public bool AllowPackageTelemetry { get; set; } = true; + + [DataMember(Name = "bundleOptions")] + public BundleOptions BundleOptions { get; set; } + + /// + /// Gets or sets the scripts listed in the manifest. + /// + [DataMember(Name = "javascript")] + public string[] Scripts { get; set; } = Array.Empty(); + + /// + /// Gets or sets the stylesheets listed in the manifest. + /// + [DataMember(Name = "css")] + public string[] Stylesheets { get; set; } = Array.Empty(); + + /// + /// Gets or sets the property editors listed in the manifest. + /// + [DataMember(Name = "propertyEditors")] + public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); + + /// + /// Gets or sets the parameter editors listed in the manifest. + /// + [DataMember(Name = "parameterEditors")] + public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); + + /// + /// Gets or sets the grid editors listed in the manifest. + /// + [DataMember(Name = "gridEditors")] + public GridEditor[] GridEditors { get; set; } = Array.Empty(); + + /// + /// Gets or sets the content apps listed in the manifest. + /// + [DataMember(Name = "contentApps")] + public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); + + /// + /// Gets or sets the dashboards listed in the manifest. + /// + [DataMember(Name = "dashboards")] + public ManifestDashboard[] Dashboards { get; set; } = Array.Empty(); + + /// + /// Gets or sets the sections listed in the manifest. + /// + [DataMember(Name = "sections")] + public ManifestSection[] Sections { get; set; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Mapping/IMapDefinition.cs b/src/Umbraco.Core/Mapping/IMapDefinition.cs index 3d4270c93e..db836fa3b8 100644 --- a/src/Umbraco.Core/Mapping/IMapDefinition.cs +++ b/src/Umbraco.Core/Mapping/IMapDefinition.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Mapping +namespace Umbraco.Cms.Core.Mapping; + +/// +/// Defines maps for . +/// +public interface IMapDefinition { /// - /// Defines maps for . + /// Defines maps. /// - public interface IMapDefinition - { - /// - /// Defines maps. - /// - void DefineMaps(IUmbracoMapper mapper); - } + void DefineMaps(IUmbracoMapper mapper); } diff --git a/src/Umbraco.Core/Mapping/IUmbracoMapper.cs b/src/Umbraco.Core/Mapping/IUmbracoMapper.cs index c99359cbdf..5cbee7164c 100644 --- a/src/Umbraco.Core/Mapping/IUmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/IUmbracoMapper.cs @@ -1,156 +1,158 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Mapping; -namespace Umbraco.Cms.Core.Mapping +public interface IUmbracoMapper { - public interface IUmbracoMapper - { - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - void Define(); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + void Define(); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A mapping method. - void Define(Action map); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A mapping method. + void Define(Action map); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - void Define(Func ctor); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + void Define(Func ctor); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - /// A mapping method. - void Define(Func ctor, Action map); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + /// A mapping method. + void Define( + Func ctor, + Action map); - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// The target object. - TTarget? Map(object? source); + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// The target object. + TTarget? Map(object? source); - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - TTarget? Map(object? source, Action f); + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + TTarget? Map(object? source, Action f); - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - TTarget? Map(object? source, MapperContext context); + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + TTarget? Map(object? source, MapperContext context); - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - TTarget? Map(TSource? source); + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + TTarget? Map(TSource? source); - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - TTarget? Map(TSource source, Action f); + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + TTarget? Map(TSource source, Action f); - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - TTarget? Map(TSource? source, MapperContext context); + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + TTarget? Map(TSource? source, MapperContext context); - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// The target object. - TTarget Map(TSource source, TTarget target); + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// The target object. + TTarget Map(TSource source, TTarget target); - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context preparation method. - /// The target object. - TTarget Map(TSource source, TTarget target, Action f); + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context preparation method. + /// The target object. + TTarget Map(TSource source, TTarget target, Action f); - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context. - /// The target object. - TTarget Map(TSource source, TTarget target, MapperContext context); + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context. + /// The target object. + TTarget Map(TSource source, TTarget target, MapperContext context); - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A list containing the target objects. - List MapEnumerable(IEnumerable source); + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A list containing the target objects. + List MapEnumerable(IEnumerable source); - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context preparation method. - /// A list containing the target objects. - List MapEnumerable(IEnumerable source, Action f); + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context preparation method. + /// A list containing the target objects. + List MapEnumerable( + IEnumerable source, + Action f); - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context. - /// A list containing the target objects. - List MapEnumerable(IEnumerable source, MapperContext context); - } + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context. + /// A list containing the target objects. + List MapEnumerable( + IEnumerable source, + MapperContext context); } diff --git a/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs b/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs index 27d4ad73d0..db35a3ffac 100644 --- a/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs +++ b/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Mapping +namespace Umbraco.Cms.Core.Mapping; + +public class MapDefinitionCollection : BuilderCollectionBase { - public class MapDefinitionCollection : BuilderCollectionBase + public MapDefinitionCollection(Func> items) + : base(items) { - public MapDefinitionCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Mapping/MapDefinitionCollectionBuilder.cs b/src/Umbraco.Core/Mapping/MapDefinitionCollectionBuilder.cs index 698dce1648..1ac6de5b33 100644 --- a/src/Umbraco.Core/Mapping/MapDefinitionCollectionBuilder.cs +++ b/src/Umbraco.Core/Mapping/MapDefinitionCollectionBuilder.cs @@ -1,12 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Mapping -{ - public class MapDefinitionCollectionBuilder : SetCollectionBuilderBase - { - protected override MapDefinitionCollectionBuilder This => this; +namespace Umbraco.Cms.Core.Mapping; - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; - } +public class MapDefinitionCollectionBuilder : SetCollectionBuilderBase +{ + protected override MapDefinitionCollectionBuilder This => this; + + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; } diff --git a/src/Umbraco.Core/Mapping/MapperContext.cs b/src/Umbraco.Core/Mapping/MapperContext.cs index 2355e9bd05..ef8663beeb 100644 --- a/src/Umbraco.Core/Mapping/MapperContext.cs +++ b/src/Umbraco.Core/Mapping/MapperContext.cs @@ -1,129 +1,120 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Mapping; -namespace Umbraco.Cms.Core.Mapping +/// +/// Represents a mapper context. +/// +public class MapperContext { + private readonly IUmbracoMapper _mapper; + private IDictionary? _items; + /// - /// Represents a mapper context. + /// Initializes a new instance of the class. /// - public class MapperContext + public MapperContext(IUmbracoMapper mapper) => _mapper = mapper; + + /// + /// Gets a value indicating whether the context has items. + /// + public bool HasItems => _items != null; + + /// + /// Gets the context items. + /// + public IDictionary Items => _items ??= new Dictionary(); + + #region Map + + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(object? source) + => _mapper.Map(source, this); + + // let's say this is a bad (dangerous) idea, and leave it out for now + /* + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(object source, Action f) { - private readonly IUmbracoMapper _mapper; - private IDictionary? _items; - - /// - /// Initializes a new instance of the class. - /// - public MapperContext(IUmbracoMapper mapper) - { - _mapper = mapper; - } - - /// - /// Gets a value indicating whether the context has items. - /// - public bool HasItems => _items != null; - - /// - /// Gets the context items. - /// - public IDictionary Items => _items ?? (_items = new Dictionary()); - - #region Map - - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(object? source) - => _mapper.Map(source, this); - - // let's say this is a bad (dangerous) idea, and leave it out for now - /* - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(object source, Action f) - { - f(this); - return _mapper.Map(source, this); - } - */ - - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(TSource? source) - => _mapper.Map(source, this); - - // let's say this is a bad (dangerous) idea, and leave it out for now - /* - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(TSource source, Action f) - { - f(this); - return _mapper.Map(source, this); - } - */ - - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// The target object. - public TTarget Map(TSource source, TTarget target) - => _mapper.Map(source, target, this); - - // let's say this is a bad (dangerous) idea, and leave it out for now - /* - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(TSource source, TTarget target, Action f) - { - f(this); - return _mapper.Map(source, target, this); - } - */ - - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source) - { - return source.Select(Map).Where(x => x is not null).ToList()!; - } - - #endregion + f(this); + return _mapper.Map(source, this); } + */ + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(TSource? source) + => _mapper.Map(source, this); + + // let's say this is a bad (dangerous) idea, and leave it out for now + /* + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(TSource source, Action f) + { + f(this); + return _mapper.Map(source, this); + } + */ + + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// The target object. + public TTarget Map(TSource source, TTarget target) + => _mapper.Map(source, target, this); + + // let's say this is a bad (dangerous) idea, and leave it out for now + /* + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(TSource source, TTarget target, Action f) + { + f(this); + return _mapper.Map(source, target, this); + } + */ + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source) => + source.Select(Map).Where(x => x is not null).ToList()!; + + #endregion } diff --git a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs index b79e1a8de2..3ea329abec 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs @@ -1,34 +1,31 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO (V10): change base class to OEmbedProviderBase +public class DailyMotion : EmbedProviderBase { - // TODO (V10): change base class to OEmbedProviderBase - public class DailyMotion : EmbedProviderBase + public DailyMotion(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://www.dailymotion.com/services/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"dailymotion.com/video/.*" - }; + public override string ApiEndpoint => "https://www.dailymotion.com/services/oembed"; - public override Dictionary RequestParams => new Dictionary() - { - //ApiUrl/?format=xml - {"format", "xml"} - }; + public override string[] UrlSchemeRegex => new[] { @"dailymotion.com/video/.*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new() + { + // ApiUrl/?format=xml + { "format", "xml" }, + }; - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public DailyMotion(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs b/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs index 6d745d3d49..e51005b84b 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/EmbedProviderBase.cs @@ -1,14 +1,12 @@ -using System; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +[Obsolete("Use OEmbedProviderBase instead")] +public abstract class EmbedProviderBase : OEmbedProviderBase { - [Obsolete("Use OEmbedProviderBase instead")] - public abstract class EmbedProviderBase : OEmbedProviderBase + protected EmbedProviderBase(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - protected EmbedProviderBase(IJsonSerializer jsonSerializer) - : base(jsonSerializer) - { - } } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs index 615d16f51c..655d68b878 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +public class EmbedProvidersCollection : BuilderCollectionBase { - public class EmbedProvidersCollection : BuilderCollectionBase + public EmbedProvidersCollection(Func> items) + : base(items) { - public EmbedProvidersCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollectionBuilder.cs b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollectionBuilder.cs index f79880b61f..121785d7eb 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollectionBuilder.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollectionBuilder.cs @@ -1,9 +1,8 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +public class EmbedProvidersCollectionBuilder : OrderedCollectionBuilderBase { - public class EmbedProvidersCollectionBuilder : OrderedCollectionBuilderBase - { - protected override EmbedProvidersCollectionBuilder This => this; - } + protected override EmbedProvidersCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs b/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs index 2ea5fd8109..5e11780645 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs @@ -1,37 +1,33 @@ -using System.Collections.Generic; using System.Net; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Flickr : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Flickr : EmbedProviderBase + public Flickr(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.flickr.com/services/oembed/"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"flickr.com\/photos\/*", - @"flic.kr\/p\/*" - }; + public override string ApiEndpoint => "http://www.flickr.com/services/oembed/"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"flickr.com\/photos\/*", @"flic.kr\/p\/*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - var imageUrl = GetXmlProperty(xmlDocument, "/oembed/url"); - var imageWidth = GetXmlProperty(xmlDocument, "/oembed/width"); - var imageHeight = GetXmlProperty(xmlDocument, "/oembed/height"); - var imageTitle = GetXmlProperty(xmlDocument, "/oembed/title"); + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - return string.Format("\"{3}\"", imageUrl, imageWidth, imageHeight, WebUtility.HtmlEncode(imageTitle)); - } + var imageUrl = GetXmlProperty(xmlDocument, "/oembed/url"); + var imageWidth = GetXmlProperty(xmlDocument, "/oembed/width"); + var imageHeight = GetXmlProperty(xmlDocument, "/oembed/height"); + var imageTitle = GetXmlProperty(xmlDocument, "/oembed/title"); - public Flickr(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return string.Format("\"{3}\"", imageUrl, imageWidth, imageHeight, WebUtility.HtmlEncode(imageTitle)); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs b/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs index 2e0ea78649..53d13cc063 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs @@ -1,33 +1,28 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class GettyImages : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class GettyImages : EmbedProviderBase + public GettyImages(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://embed.gettyimages.com/oembed"; + } - //http://gty.im/74917285 - //http://www.gettyimages.com/detail/74917285 - public override string[] UrlSchemeRegex => new string[] - { - @"gty\.im/*", - @"gettyimages.com\/detail\/*" - }; + public override string ApiEndpoint => "http://embed.gettyimages.com/oembed"; - public override Dictionary RequestParams => new Dictionary(); + // http://gty.im/74917285 + // http://www.gettyimages.com/detail/74917285 + public override string[] UrlSchemeRegex => new[] { @"gty\.im/*", @"gettyimages.com\/detail\/*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public GettyImages(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs b/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs index 36df7e7362..4adb02f8fb 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs @@ -1,34 +1,29 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for Giphy.com the popular online GIFs and animated sticker provider. +/// +/// TODO(V10) : change base class to OEmbedProviderBase +public class Giphy : EmbedProviderBase { - /// - /// Embed Provider for Giphy.com the popular online GIFs and animated sticker provider. - /// - /// TODO(V10) : change base class to OEmbedProviderBase - public class Giphy : EmbedProviderBase + public Giphy(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://giphy.com/services/oembed?url="; + } - public override string[] UrlSchemeRegex => new string[] - { - @"giphy\.com/*", - @"gph\.is/*" - }; + public override string ApiEndpoint => "https://giphy.com/services/oembed?url="; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"giphy\.com/*", @"gph\.is/*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public Giphy(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs b/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs index 1d6bed791b..2fdadee6ea 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs @@ -1,30 +1,26 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Hulu : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Hulu : EmbedProviderBase + public Hulu(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.hulu.com/api/oembed.json"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"hulu.com/watch/.*" - }; + public override string ApiEndpoint => "http://www.hulu.com/api/oembed.json"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"hulu.com/watch/.*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public Hulu(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs b/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs index 89179d40af..ded01ef0d9 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs @@ -1,34 +1,31 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Issuu : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Issuu : EmbedProviderBase + public Issuu(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://issuu.com/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"issuu.com/.*/docs/.*" - }; + public override string ApiEndpoint => "https://issuu.com/oembed"; - public override Dictionary RequestParams => new Dictionary() - { - //ApiUrl/?format=xml - {"format", "xml"} - }; + public override string[] UrlSchemeRegex => new[] { @"issuu.com/.*/docs/.*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new() + { + // ApiUrl/?format=xml + { "format", "xml" }, + }; - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Issuu(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs b/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs index e9ada74cf6..4e3f5b6731 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs @@ -1,30 +1,26 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Kickstarter : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Kickstarter : EmbedProviderBase + public Kickstarter(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.kickstarter.com/services/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"kickstarter\.com/projects/*" - }; + public override string ApiEndpoint => "http://www.kickstarter.com/services/oembed"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"kickstarter\.com/projects/*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public Kickstarter(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs b/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs index f79e78b8b3..95330a6467 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs @@ -1,57 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Embed Provider for lottiefiles.com the popular opensource JSON-based animation format platform. +/// +public class LottieFiles : OEmbedProviderBase { - /// - /// Embed Provider for lottiefiles.com the popular opensource JSON-based animation format platform. - /// - public class LottieFiles : OEmbedProviderBase + public LottieFiles(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public LottieFiles(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { + } + public override string ApiEndpoint => "https://embed.lottiefiles.com/oembed"; + + public override string[] UrlSchemeRegex => new[] { @"lottiefiles\.com/*" }; + + public override Dictionary RequestParams => new(); + + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = this.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = this.GetJsonResponse(requestUrl); + var html = oembed?.GetHtml(); + + // LottieFiles doesn't seem to support maxwidth and maxheight via oembed + // this is therefore a hack... with regexes.. is that ok? HtmlAgility etc etc + // otherwise it always defaults to 300... + if (html is null) + { + return null; } - public override string ApiEndpoint => "https://embed.lottiefiles.com/oembed"; - - public override string[] UrlSchemeRegex => new string[] + if (maxWidth > 0 && maxHeight > 0) { - @"lottiefiles\.com/*" - }; - public override Dictionary RequestParams => new Dictionary(); - - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"" + maxWidth + "\""); + html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"" + maxHeight + "\""); + } + else { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - var html = oembed?.GetHtml(); - //LottieFiles doesn't seem to support maxwidth and maxheight via oembed - // this is therefore a hack... with regexes.. is that ok? HtmlAgility etc etc - // otherwise it always defaults to 300... - if (html is null) - { - return null; - } - - if (maxWidth > 0 && maxHeight > 0) - { - - html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"" + maxWidth + "\""); - html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"" + maxHeight + "\""); - - } - else - { - //if set to 0, let's default to 100% as an easter egg - html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"100%\""); - html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"100%\""); - } - return html; + // if set to 0, let's default to 100% as an easter egg + html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"100%\""); + html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"100%\""); } + return html; } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs index 031105033b..b09baba0db 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs @@ -1,85 +1,88 @@ -using System; -using System.Collections.Generic; using System.Net; -using System.Net.Http; using System.Text; using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +public abstract class OEmbedProviderBase : IEmbedProvider { - public abstract class OEmbedProviderBase : IEmbedProvider + private static HttpClient? _httpClient; + private readonly IJsonSerializer _jsonSerializer; + + protected OEmbedProviderBase(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + public abstract string ApiEndpoint { get; } + + public abstract string[] UrlSchemeRegex { get; } + + public abstract Dictionary RequestParams { get; } + + public abstract string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); + + public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) { - private readonly IJsonSerializer _jsonSerializer; - - protected OEmbedProviderBase(IJsonSerializer jsonSerializer) + if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) { - _jsonSerializer = jsonSerializer; + throw new ArgumentException("Not a valid URL.", nameof(url)); } - private static HttpClient? _httpClient; + var fullUrl = new StringBuilder(); - public abstract string ApiEndpoint { get; } + fullUrl.Append(ApiEndpoint); + fullUrl.Append("?url=" + WebUtility.UrlEncode(url)); - public abstract string[] UrlSchemeRegex { get; } - - public abstract Dictionary RequestParams { get; } - - public abstract string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); - - public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) + foreach (KeyValuePair param in RequestParams) { - if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) - throw new ArgumentException("Not a valid URL.", nameof(url)); - - var fullUrl = new StringBuilder(); - - fullUrl.Append(ApiEndpoint); - fullUrl.Append("?url=" + WebUtility.UrlEncode(url)); - - foreach (var param in RequestParams) - fullUrl.Append($"&{param.Key}={param.Value}"); - - if (maxWidth > 0) - fullUrl.Append("&maxwidth=" + maxWidth); - - if (maxHeight > 0) - fullUrl.Append("&maxheight=" + maxHeight); - - return fullUrl.ToString(); + fullUrl.Append($"&{param.Key}={param.Value}"); } - public virtual string DownloadResponse(string url) + if (maxWidth > 0) { - if (_httpClient == null) - _httpClient = new HttpClient(); - - using (var request = new HttpRequestMessage(HttpMethod.Get, url)) - { - var response = _httpClient.SendAsync(request).Result; - return response.Content.ReadAsStringAsync().Result; - } + fullUrl.Append("&maxwidth=" + maxWidth); } - public virtual T? GetJsonResponse(string url) where T : class + if (maxHeight > 0) { - var response = DownloadResponse(url); - return _jsonSerializer.Deserialize(response); + fullUrl.Append("&maxheight=" + maxHeight); } - public virtual XmlDocument GetXmlResponse(string url) - { - var response = DownloadResponse(url); - var doc = new XmlDocument(); - doc.LoadXml(response); + return fullUrl.ToString(); + } - return doc; + public virtual string DownloadResponse(string url) + { + if (_httpClient == null) + { + _httpClient = new HttpClient(); } - public virtual string GetXmlProperty(XmlDocument doc, string property) + using (var request = new HttpRequestMessage(HttpMethod.Get, url)) { - var selectSingleNode = doc.SelectSingleNode(property); - return selectSingleNode != null ? selectSingleNode.InnerText : string.Empty; + HttpResponseMessage response = _httpClient.SendAsync(request).Result; + return response.Content.ReadAsStringAsync().Result; } } -}; + + public virtual T? GetJsonResponse(string url) + where T : class + { + var response = DownloadResponse(url); + return _jsonSerializer.Deserialize(response); + } + + public virtual XmlDocument GetXmlResponse(string url) + { + var response = DownloadResponse(url); + var doc = new XmlDocument(); + doc.LoadXml(response); + + return doc; + } + + public virtual string GetXmlProperty(XmlDocument doc, string property) + { + XmlNode? selectSingleNode = doc.SelectSingleNode(property); + return selectSingleNode != null ? selectSingleNode.InnerText : string.Empty; + } +} diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs index f003aa841c..370d2609c7 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs @@ -1,68 +1,68 @@ using System.Net; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +/// +/// Wrapper class for OEmbed response +/// +[DataContract] +public class OEmbedResponse { + [DataMember(Name = "type")] + public string? Type { get; set; } + + [DataMember(Name = "version")] + public string? Version { get; set; } + + [DataMember(Name = "title")] + public string? Title { get; set; } + + [DataMember(Name = "author_name")] + public string? AuthorName { get; set; } + + [DataMember(Name = "author_url")] + public string? AuthorUrl { get; set; } + + [DataMember(Name = "provider_name")] + public string? ProviderName { get; set; } + + [DataMember(Name = "provider_url")] + public string? ProviderUrl { get; set; } + + [DataMember(Name = "thumbnail_url")] + public string? ThumbnailUrl { get; set; } + + [DataMember(Name = "thumbnail_height")] + public double? ThumbnailHeight { get; set; } + + [DataMember(Name = "thumbnail_width")] + public double? ThumbnailWidth { get; set; } + + [DataMember(Name = "html")] + public string? Html { get; set; } + + [DataMember(Name = "url")] + public string? Url { get; set; } + + [DataMember(Name = "height")] + public double? Height { get; set; } + + [DataMember(Name = "width")] + public double? Width { get; set; } + /// - /// Wrapper class for OEmbed response + /// Gets the HTML. /// - [DataContract] - public class OEmbedResponse + /// The response HTML + public string GetHtml() { - [DataMember(Name ="type")] - public string? Type { get; set; } - - [DataMember(Name ="version")] - public string? Version { get; set; } - - [DataMember(Name ="title")] - public string? Title { get; set; } - - [DataMember(Name ="author_name")] - public string? AuthorName { get; set; } - - [DataMember(Name ="author_url")] - public string? AuthorUrl { get; set; } - - [DataMember(Name ="provider_name")] - public string? ProviderName { get; set; } - - [DataMember(Name ="provider_url")] - public string? ProviderUrl { get; set; } - - [DataMember(Name ="thumbnail_url")] - public string? ThumbnailUrl { get; set; } - - [DataMember(Name ="thumbnail_height")] - public double? ThumbnailHeight { get; set; } - - [DataMember(Name ="thumbnail_width")] - public double? ThumbnailWidth { get; set; } - - [DataMember(Name ="html")] - public string? Html { get; set; } - - [DataMember(Name ="url")] - public string? Url { get; set; } - - [DataMember(Name ="height")] - public double? Height { get; set; } - - [DataMember(Name ="width")] - public double? Width { get; set; } - - /// - /// Gets the HTML. - /// - /// The response HTML - public string GetHtml() + if (Type == "photo") { - if (Type == "photo") - { - return "\"""; - } - - return string.IsNullOrEmpty(Html) == false ? Html : string.Empty; + return "\"""; } + + return string.IsNullOrEmpty(Html) == false ? Html : string.Empty; } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs b/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs index 42e500aa5c..f00e631d25 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs @@ -1,30 +1,27 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Slideshare : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Slideshare : EmbedProviderBase + public Slideshare(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.slideshare.net/api/oembed/2"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"slideshare\.net/" - }; + public override string ApiEndpoint => "http://www.slideshare.net/api/oembed/2"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"slideshare\.net/" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Slideshare(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs b/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs index 687da98697..f3d4e2caae 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs @@ -1,30 +1,27 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Soundcloud : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Soundcloud : EmbedProviderBase + public Soundcloud(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://soundcloud.com/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"soundcloud.com\/*" - }; + public override string ApiEndpoint => "https://soundcloud.com/oembed"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"soundcloud.com\/*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Soundcloud(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Ted.cs b/src/Umbraco.Core/Media/EmbedProviders/Ted.cs index 511cbf012d..9c8a607e13 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Ted.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Ted.cs @@ -1,30 +1,27 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Ted : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Ted : EmbedProviderBase + public Ted(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://www.ted.com/talks/oembed.xml"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"ted.com\/talks\/*" - }; + public override string ApiEndpoint => "http://www.ted.com/talks/oembed.xml"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"ted.com\/talks\/*" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Ted(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs b/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs index 934ec4b5c1..555224032a 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs @@ -1,30 +1,26 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Twitter : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Twitter : EmbedProviderBase + public Twitter(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "http://publish.twitter.com/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"twitter.com/.*/status/.*" - }; + public override string ApiEndpoint => "http://publish.twitter.com/oembed"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"twitter.com/.*/status/.*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new(); - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public Twitter(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs b/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs index db324bda12..ed3990ba4d 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs @@ -1,30 +1,27 @@ -using System.Collections.Generic; +using System.Xml; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class Vimeo : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class Vimeo : EmbedProviderBase + public Vimeo(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://vimeo.com/api/oembed.xml"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"vimeo\.com/" - }; + public override string ApiEndpoint => "https://vimeo.com/api/oembed.xml"; - public override Dictionary RequestParams => new Dictionary(); + public override string[] UrlSchemeRegex => new[] { @"vimeo\.com/" }; - public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var xmlDocument = base.GetXmlResponse(requestUrl); + public override Dictionary RequestParams => new(); - return GetXmlProperty(xmlDocument, "/oembed/html"); - } + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = base.GetXmlResponse(requestUrl); - public Vimeo(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return GetXmlProperty(xmlDocument, "/oembed/html"); } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs index 3888462dbc..594c7ead83 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs @@ -1,35 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Media.EmbedProviders +namespace Umbraco.Cms.Core.Media.EmbedProviders; + +// TODO(V10) : change base class to OEmbedProviderBase +public class YouTube : EmbedProviderBase { - // TODO(V10) : change base class to OEmbedProviderBase - public class YouTube : EmbedProviderBase + public YouTube(IJsonSerializer jsonSerializer) + : base(jsonSerializer) { - public override string ApiEndpoint => "https://www.youtube.com/oembed"; + } - public override string[] UrlSchemeRegex => new string[] - { - @"youtu.be/.*", - @"youtube.com/watch.*" - }; + public override string ApiEndpoint => "https://www.youtube.com/oembed"; - public override Dictionary RequestParams => new Dictionary() - { - //ApiUrl/?format=json - {"format", "json"} - }; + public override string[] UrlSchemeRegex => new[] { @"youtu.be/.*", @"youtube.com/watch.*" }; - public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - var oembed = base.GetJsonResponse(requestUrl); + public override Dictionary RequestParams => new() + { + // ApiUrl/?format=json + { "format", "json" }, + }; - return oembed?.GetHtml(); - } + public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); - public YouTube(IJsonSerializer jsonSerializer) : base(jsonSerializer) - { - } + return oembed?.GetHtml(); } } diff --git a/src/Umbraco.Core/Media/Exif/BitConverterEx.cs b/src/Umbraco.Core/Media/Exif/BitConverterEx.cs index 6afc6e4308..f6cc50f801 100644 --- a/src/Umbraco.Core/Media/Exif/BitConverterEx.cs +++ b/src/Umbraco.Core/Media/Exif/BitConverterEx.cs @@ -1,405 +1,346 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// An endian-aware converter for converting between base data types +/// and an array of bytes. +/// +internal class BitConverterEx { + #region Public Enums + /// - /// An endian-aware converter for converting between base data types - /// and an array of bytes. + /// Represents the byte order. /// - internal class BitConverterEx + public enum ByteOrder { - #region Public Enums - /// - /// Represents the byte order. - /// - public enum ByteOrder - { - LittleEndian = 1, - BigEndian = 2, - } - #endregion - - #region Member Variables - private ByteOrder mFrom, mTo; - #endregion - - #region Constructors - public BitConverterEx(ByteOrder from, ByteOrder to) - { - mFrom = from; - mTo = to; - } - #endregion - - #region Properties - /// - /// Indicates the byte order in which data is stored in this platform. - /// - public static ByteOrder SystemByteOrder - { - get - { - return (BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian); - } - } - #endregion - - #region Predefined Values - /// - /// Returns a bit converter that converts between little-endian and system byte-order. - /// - public static BitConverterEx LittleEndian - { - get - { - return new BitConverterEx(ByteOrder.LittleEndian, BitConverterEx.SystemByteOrder); - } - } - - /// - /// Returns a bit converter that converts between big-endian and system byte-order. - /// - public static BitConverterEx BigEndian - { - get - { - return new BitConverterEx(ByteOrder.BigEndian, BitConverterEx.SystemByteOrder); - } - } - - /// - /// Returns a bit converter that does not do any byte-order conversion. - /// - public static BitConverterEx SystemEndian - { - get - { - return new BitConverterEx(BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder); - } - } - #endregion - - #region Static Methods - /// - /// Converts the given array of bytes to a Unicode character. - /// - public static char ToChar(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 2, from, to); - return BitConverter.ToChar(data, 0); - } - - /// - /// Converts the given array of bytes to a 16-bit unsigned integer. - /// - public static ushort ToUInt16(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 2, from, to); - return BitConverter.ToUInt16(data, 0); - } - - /// - /// Converts the given array of bytes to a 32-bit unsigned integer. - /// - public static uint ToUInt32(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 4, from, to); - return BitConverter.ToUInt32(data, 0); - } - - /// - /// Converts the given array of bytes to a 64-bit unsigned integer. - /// - public static ulong ToUInt64(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 8, from, to); - return BitConverter.ToUInt64(data, 0); - } - - /// - /// Converts the given array of bytes to a 16-bit signed integer. - /// - public static short ToInt16(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 2, from, to); - return BitConverter.ToInt16(data, 0); - } - - /// - /// Converts the given array of bytes to a 32-bit signed integer. - /// - public static int ToInt32(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 4, from, to); - return BitConverter.ToInt32(data, 0); - } - - /// - /// Converts the given array of bytes to a 64-bit signed integer. - /// - public static long ToInt64(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 8, from, to); - return BitConverter.ToInt64(data, 0); - } - - /// - /// Converts the given array of bytes to a single precision floating number. - /// - public static float ToSingle(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 4, from, to); - return BitConverter.ToSingle(data, 0); - } - - /// - /// Converts the given array of bytes to a double precision floating number. - /// - public static double ToDouble(byte[] value, long startIndex, ByteOrder from, ByteOrder to) - { - byte[] data = CheckData(value, startIndex, 8, from, to); - return BitConverter.ToDouble(data, 0); - } - - /// - /// Converts the given 16-bit unsigned integer to an array of bytes. - /// - public static byte[] GetBytes(ushort value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 32-bit unsigned integer to an array of bytes. - /// - public static byte[] GetBytes(uint value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 64-bit unsigned integer to an array of bytes. - /// - public static byte[] GetBytes(ulong value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 16-bit signed integer to an array of bytes. - /// - public static byte[] GetBytes(short value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 32-bit signed integer to an array of bytes. - /// - public static byte[] GetBytes(int value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given 64-bit signed integer to an array of bytes. - /// - public static byte[] GetBytes(long value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given single precision floating-point number to an array of bytes. - /// - public static byte[] GetBytes(float value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - - /// - /// Converts the given double precision floating-point number to an array of bytes. - /// - public static byte[] GetBytes(double value, ByteOrder from, ByteOrder to) - { - byte[] data = BitConverter.GetBytes(value); - data = CheckData(data, from, to); - return data; - } - #endregion - - #region Instance Methods - /// - /// Converts the given array of bytes to a 16-bit unsigned integer. - /// - public char ToChar(byte[] value, long startIndex) - { - return BitConverterEx.ToChar(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 16-bit unsigned integer. - /// - public ushort ToUInt16(byte[] value, long startIndex) - { - return BitConverterEx.ToUInt16(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 32-bit unsigned integer. - /// - public uint ToUInt32(byte[] value, long startIndex) - { - return BitConverterEx.ToUInt32(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 64-bit unsigned integer. - /// - public ulong ToUInt64(byte[] value, long startIndex) - { - return BitConverterEx.ToUInt64(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 16-bit signed integer. - /// - public short ToInt16(byte[] value, long startIndex) - { - return BitConverterEx.ToInt16(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 32-bit signed integer. - /// - public int ToInt32(byte[] value, long startIndex) - { - return BitConverterEx.ToInt32(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a 64-bit signed integer. - /// - public long ToInt64(byte[] value, long startIndex) - { - return BitConverterEx.ToInt64(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a single precision floating number. - /// - public float ToSingle(byte[] value, long startIndex) - { - return BitConverterEx.ToSingle(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given array of bytes to a double precision floating number. - /// - public double ToDouble(byte[] value, long startIndex) - { - return BitConverterEx.ToDouble(value, startIndex, mFrom, mTo); - } - - /// - /// Converts the given 16-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(ushort value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 32-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(uint value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 64-bit unsigned integer to an array of bytes. - /// - public byte[] GetBytes(ulong value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 16-bit signed integer to an array of bytes. - /// - public byte[] GetBytes(short value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 32-bit signed integer to an array of bytes. - /// - public byte[] GetBytes(int value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given 64-bit signed integer to an array of bytes. - /// - public byte[] GetBytes(long value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given single precision floating-point number to an array of bytes. - /// - public byte[] GetBytes(float value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - - /// - /// Converts the given double precision floating-point number to an array of bytes. - /// - public byte[] GetBytes(double value) - { - return BitConverterEx.GetBytes(value, mFrom, mTo); - } - #endregion - - #region Private Helpers - /// - /// Reverse the array of bytes as needed. - /// - private static byte[] CheckData(byte[] value, long startIndex, long length, ByteOrder from, ByteOrder to) - { - byte[] data = new byte[length]; - Array.Copy(value, startIndex, data, 0, length); - if (from != to) - Array.Reverse(data); - return data; - } - - /// - /// Reverse the array of bytes as needed. - /// - private static byte[] CheckData(byte[] value, ByteOrder from, ByteOrder to) - { - return CheckData(value, 0, value.Length, from, to); - } - #endregion + LittleEndian = 1, + BigEndian = 2, } + + #endregion + + #region Member Variables + + private readonly ByteOrder mFrom; + private readonly ByteOrder mTo; + + #endregion + + #region Constructors + + public BitConverterEx(ByteOrder from, ByteOrder to) + { + mFrom = from; + mTo = to; + } + + #endregion + + #region Properties + + /// + /// Indicates the byte order in which data is stored in this platform. + /// + public static ByteOrder SystemByteOrder => + BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian; + + #endregion + + #region Predefined Values + + /// + /// Returns a bit converter that converts between little-endian and system byte-order. + /// + public static BitConverterEx LittleEndian => new BitConverterEx(ByteOrder.LittleEndian, SystemByteOrder); + + /// + /// Returns a bit converter that converts between big-endian and system byte-order. + /// + public static BitConverterEx BigEndian => new BitConverterEx(ByteOrder.BigEndian, SystemByteOrder); + + /// + /// Returns a bit converter that does not do any byte-order conversion. + /// + public static BitConverterEx SystemEndian => new BitConverterEx(SystemByteOrder, SystemByteOrder); + + #endregion + + #region Static Methods + + /// + /// Converts the given array of bytes to a Unicode character. + /// + public static char ToChar(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 2, from, to); + return BitConverter.ToChar(data, 0); + } + + /// + /// Converts the given array of bytes to a 16-bit unsigned integer. + /// + public static ushort ToUInt16(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 2, from, to); + return BitConverter.ToUInt16(data, 0); + } + + /// + /// Converts the given array of bytes to a 32-bit unsigned integer. + /// + public static uint ToUInt32(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 4, from, to); + return BitConverter.ToUInt32(data, 0); + } + + /// + /// Converts the given array of bytes to a 64-bit unsigned integer. + /// + public static ulong ToUInt64(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 8, from, to); + return BitConverter.ToUInt64(data, 0); + } + + /// + /// Converts the given array of bytes to a 16-bit signed integer. + /// + public static short ToInt16(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 2, from, to); + return BitConverter.ToInt16(data, 0); + } + + /// + /// Converts the given array of bytes to a 32-bit signed integer. + /// + public static int ToInt32(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 4, from, to); + return BitConverter.ToInt32(data, 0); + } + + /// + /// Converts the given array of bytes to a 64-bit signed integer. + /// + public static long ToInt64(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 8, from, to); + return BitConverter.ToInt64(data, 0); + } + + /// + /// Converts the given array of bytes to a single precision floating number. + /// + public static float ToSingle(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 4, from, to); + return BitConverter.ToSingle(data, 0); + } + + /// + /// Converts the given array of bytes to a double precision floating number. + /// + public static double ToDouble(byte[] value, long startIndex, ByteOrder from, ByteOrder to) + { + var data = CheckData(value, startIndex, 8, from, to); + return BitConverter.ToDouble(data, 0); + } + + /// + /// Converts the given 16-bit unsigned integer to an array of bytes. + /// + public static byte[] GetBytes(ushort value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 32-bit unsigned integer to an array of bytes. + /// + public static byte[] GetBytes(uint value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 64-bit unsigned integer to an array of bytes. + /// + public static byte[] GetBytes(ulong value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 16-bit signed integer to an array of bytes. + /// + public static byte[] GetBytes(short value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 32-bit signed integer to an array of bytes. + /// + public static byte[] GetBytes(int value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given 64-bit signed integer to an array of bytes. + /// + public static byte[] GetBytes(long value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given single precision floating-point number to an array of bytes. + /// + public static byte[] GetBytes(float value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + /// + /// Converts the given double precision floating-point number to an array of bytes. + /// + public static byte[] GetBytes(double value, ByteOrder from, ByteOrder to) + { + var data = BitConverter.GetBytes(value); + data = CheckData(data, from, to); + return data; + } + + #endregion + + #region Instance Methods + + /// + /// Converts the given array of bytes to a 16-bit unsigned integer. + /// + public char ToChar(byte[] value, long startIndex) => ToChar(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 16-bit unsigned integer. + /// + public ushort ToUInt16(byte[] value, long startIndex) => ToUInt16(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 32-bit unsigned integer. + /// + public uint ToUInt32(byte[] value, long startIndex) => ToUInt32(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 64-bit unsigned integer. + /// + public ulong ToUInt64(byte[] value, long startIndex) => ToUInt64(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 16-bit signed integer. + /// + public short ToInt16(byte[] value, long startIndex) => ToInt16(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 32-bit signed integer. + /// + public int ToInt32(byte[] value, long startIndex) => ToInt32(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a 64-bit signed integer. + /// + public long ToInt64(byte[] value, long startIndex) => ToInt64(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a single precision floating number. + /// + public float ToSingle(byte[] value, long startIndex) => ToSingle(value, startIndex, mFrom, mTo); + + /// + /// Converts the given array of bytes to a double precision floating number. + /// + public double ToDouble(byte[] value, long startIndex) => ToDouble(value, startIndex, mFrom, mTo); + + /// + /// Converts the given 16-bit unsigned integer to an array of bytes. + /// + public byte[] GetBytes(ushort value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 32-bit unsigned integer to an array of bytes. + /// + public byte[] GetBytes(uint value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 64-bit unsigned integer to an array of bytes. + /// + public byte[] GetBytes(ulong value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 16-bit signed integer to an array of bytes. + /// + public byte[] GetBytes(short value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 32-bit signed integer to an array of bytes. + /// + public byte[] GetBytes(int value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given 64-bit signed integer to an array of bytes. + /// + public byte[] GetBytes(long value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given single precision floating-point number to an array of bytes. + /// + public byte[] GetBytes(float value) => GetBytes(value, mFrom, mTo); + + /// + /// Converts the given double precision floating-point number to an array of bytes. + /// + public byte[] GetBytes(double value) => GetBytes(value, mFrom, mTo); + + #endregion + + #region Private Helpers + + /// + /// Reverse the array of bytes as needed. + /// + private static byte[] CheckData(byte[] value, long startIndex, long length, ByteOrder from, ByteOrder to) + { + var data = new byte[length]; + Array.Copy(value, startIndex, data, 0, length); + if (from != to) + { + Array.Reverse(data); + } + + return data; + } + + /// + /// Reverse the array of bytes as needed. + /// + private static byte[] CheckData(byte[] value, ByteOrder from, ByteOrder to) => + CheckData(value, 0, value.Length, from, to); + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifBitConverter.cs b/src/Umbraco.Core/Media/Exif/ExifBitConverter.cs index 74465a6684..e8dc7c4eb9 100644 --- a/src/Umbraco.Core/Media/Exif/ExifBitConverter.cs +++ b/src/Umbraco.Core/Media/Exif/ExifBitConverter.cs @@ -1,358 +1,392 @@ -using System; using System.Globalization; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Converts between exif data types and array of bytes. +/// +internal class ExifBitConverter : BitConverterEx { - /// - /// Converts between exif data types and array of bytes. - /// - internal class ExifBitConverter : BitConverterEx + #region Constructors + + public ExifBitConverter(ByteOrder from, ByteOrder to) + : base(from, to) { - #region Constructors - public ExifBitConverter(ByteOrder from, ByteOrder to) - : base(from, to) - { - - } - #endregion - - #region Static Methods - /// - /// Returns an ASCII string converted from the given byte array. - /// - public static string ToAscii(byte[] data, bool endatfirstnull, Encoding encoding) - { - int len = data.Length; - if (endatfirstnull) - { - len = Array.IndexOf(data, (byte)0); - if (len == -1) len = data.Length; - } - return encoding.GetString(data, 0, len); - } - - /// - /// Returns an ASCII string converted from the given byte array. - /// - public static string ToAscii(byte[] data, Encoding encoding) - { - return ToAscii(data, true, encoding); - } - - /// - /// Returns a string converted from the given byte array. - /// from the numeric value of each byte. - /// - public static string ToString(byte[] data) - { - StringBuilder sb = new StringBuilder(); - foreach (byte b in data) - sb.Append(b); - return sb.ToString(); - } - - /// - /// Returns a DateTime object converted from the given byte array. - /// - public static DateTime ToDateTime(byte[] data, bool hastime) - { - string str = ToAscii(data, Encoding.ASCII); - string[] parts = str.Split(new char[] { ':', ' ' }); - try - { - if (hastime && parts.Length == 6) - { - // yyyy:MM:dd HH:mm:ss - // This is the expected format though some cameras - // can use single digits. See Issue 21. - return new DateTime(int.Parse(parts[0], CultureInfo.InvariantCulture), int.Parse(parts[1], CultureInfo.InvariantCulture), int.Parse(parts[2], CultureInfo.InvariantCulture), int.Parse(parts[3], CultureInfo.InvariantCulture), int.Parse(parts[4], CultureInfo.InvariantCulture), int.Parse(parts[5], CultureInfo.InvariantCulture)); - } - else if (!hastime && parts.Length == 3) - { - // yyyy:MM:dd - return new DateTime(int.Parse(parts[0], CultureInfo.InvariantCulture), int.Parse(parts[1], CultureInfo.InvariantCulture), int.Parse(parts[2], CultureInfo.InvariantCulture)); - } - else - { - return DateTime.MinValue; - } - } - catch (ArgumentOutOfRangeException) - { - return DateTime.MinValue; - } - catch (ArgumentException) - { - return DateTime.MinValue; - } - } - - /// - /// Returns a DateTime object converted from the given byte array. - /// - public static DateTime ToDateTime(byte[] data) - { - return ToDateTime(data, true); - } - - /// - /// Returns an unsigned rational number converted from the first - /// eight bytes of the given byte array. The first four bytes are - /// assumed to be the numerator and the next four bytes are the - /// denominator. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static MathEx.UFraction32 ToURational(byte[] data, ByteOrder frombyteorder) - { - byte[] num = new byte[4]; - byte[] den = new byte[4]; - Array.Copy(data, 0, num, 0, 4); - Array.Copy(data, 4, den, 0, 4); - return new MathEx.UFraction32(ToUInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder), ToUInt32(den, 0, frombyteorder, BitConverterEx.SystemByteOrder)); - } - - /// - /// Returns a signed rational number converted from the first - /// eight bytes of the given byte array. The first four bytes are - /// assumed to be the numerator and the next four bytes are the - /// denominator. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static MathEx.Fraction32 ToSRational(byte[] data, ByteOrder frombyteorder) - { - byte[] num = new byte[4]; - byte[] den = new byte[4]; - Array.Copy(data, 0, num, 0, 4); - Array.Copy(data, 4, den, 0, 4); - return new MathEx.Fraction32(ToInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder), ToInt32(den, 0, frombyteorder, BitConverterEx.SystemByteOrder)); - } - - /// - /// Returns an array of 16-bit unsigned integers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static ushort[] ToUShortArray(byte[] data, int count, ByteOrder frombyteorder) - { - ushort[] numbers = new ushort[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[2]; - Array.Copy(data, i * 2, num, 0, 2); - numbers[i] = ToUInt16(num, 0, frombyteorder, BitConverterEx.SystemByteOrder); - } - return numbers; - } - - /// - /// Returns an array of 32-bit unsigned integers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static uint[] ToUIntArray(byte[] data, int count, ByteOrder frombyteorder) - { - uint[] numbers = new uint[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[4]; - Array.Copy(data, i * 4, num, 0, 4); - numbers[i] = ToUInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder); - } - return numbers; - } - - /// - /// Returns an array of 32-bit signed integers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static int[] ToSIntArray(byte[] data, int count, ByteOrder byteorder) - { - int[] numbers = new int[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[4]; - Array.Copy(data, i * 4, num, 0, 4); - numbers[i] = ToInt32(num, 0, byteorder, BitConverterEx.SystemByteOrder); - } - return numbers; - } - - /// - /// Returns an array of unsigned rational numbers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static MathEx.UFraction32[] ToURationalArray(byte[] data, int count, ByteOrder frombyteorder) - { - MathEx.UFraction32[] numbers = new MathEx.UFraction32[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[4]; - byte[] den = new byte[4]; - Array.Copy(data, i * 8, num, 0, 4); - Array.Copy(data, i * 8 + 4, den, 0, 4); - numbers[i].Set(ToUInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder), ToUInt32(den, 0, frombyteorder, BitConverterEx.SystemByteOrder)); - } - return numbers; - } - - /// - /// Returns an array of signed rational numbers converted from - /// the given byte array. - /// Numbers are converted from the given byte-order to platform byte-order. - /// - public static MathEx.Fraction32[] ToSRationalArray(byte[] data, int count, ByteOrder frombyteorder) - { - MathEx.Fraction32[] numbers = new MathEx.Fraction32[count]; - for (uint i = 0; i < count; i++) - { - byte[] num = new byte[4]; - byte[] den = new byte[4]; - Array.Copy(data, i * 8, num, 0, 4); - Array.Copy(data, i * 8 + 4, den, 0, 4); - numbers[i].Set(ToInt32(num, 0, frombyteorder, BitConverterEx.SystemByteOrder), ToInt32(den, 0, frombyteorder, BitConverterEx.SystemByteOrder)); - } - return numbers; - } - - /// - /// Converts the given ascii string to an array of bytes optionally adding a null terminator. - /// - public static byte[] GetBytes(string value, bool addnull, Encoding encoding) - { - if (addnull) value += '\0'; - return encoding.GetBytes(value); - } - - /// - /// Converts the given ascii string to an array of bytes without adding a null terminator. - /// - public static byte[] GetBytes(string value, Encoding encoding) - { - return GetBytes(value, false, encoding); - } - - /// - /// Converts the given datetime to an array of bytes with a null terminator. - /// - public static byte[] GetBytes(DateTime value, bool hastime) - { - string str = ""; - if (hastime) - str = value.ToString("yyyy:MM:dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture); - else - str = value.ToString("yyyy:MM:dd", System.Globalization.CultureInfo.InvariantCulture); - return GetBytes(str, true, Encoding.ASCII); - } - - /// - /// Converts the given unsigned rational number to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(MathEx.UFraction32 value, ByteOrder tobyteorder) - { - byte[] num = GetBytes(value.Numerator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] den = GetBytes(value.Denominator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] data = new byte[8]; - Array.Copy(num, 0, data, 0, 4); - Array.Copy(den, 0, data, 4, 4); - return data; - } - - /// - /// Converts the given signed rational number to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(MathEx.Fraction32 value, ByteOrder tobyteorder) - { - byte[] num = GetBytes(value.Numerator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] den = GetBytes(value.Denominator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] data = new byte[8]; - Array.Copy(num, 0, data, 0, 4); - Array.Copy(den, 0, data, 4, 4); - return data; - } - - /// - /// Converts the given array of 16-bit unsigned integers to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(ushort[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[2 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i], BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 2, 2); - } - return data; - } - - /// - /// Converts the given array of 32-bit unsigned integers to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(uint[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[4 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i], BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 4, 4); - } - return data; - } - - /// - /// Converts the given array of 32-bit signed integers to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(int[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[4 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i], BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 4, 4); - } - return data; - } - - /// - /// Converts the given array of unsigned rationals to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(MathEx.UFraction32[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[8 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i].Numerator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] den = GetBytes(value[i].Denominator, BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 8, 4); - Array.Copy(den, 0, data, i * 8 + 4, 4); - } - return data; - } - - /// - /// Converts the given array of signed rationals to an array of bytes. - /// Numbers are converted from the platform byte-order to the given byte-order. - /// - public static byte[] GetBytes(MathEx.Fraction32[] value, ByteOrder tobyteorder) - { - byte[] data = new byte[8 * value.Length]; - for (int i = 0; i < value.Length; i++) - { - byte[] num = GetBytes(value[i].Numerator, BitConverterEx.SystemByteOrder, tobyteorder); - byte[] den = GetBytes(value[i].Denominator, BitConverterEx.SystemByteOrder, tobyteorder); - Array.Copy(num, 0, data, i * 8, 4); - Array.Copy(den, 0, data, i * 8 + 4, 4); - } - return data; - } - #endregion } + + #endregion + + #region Static Methods + + /// + /// Returns an ASCII string converted from the given byte array. + /// + public static string ToAscii(byte[] data, bool endatfirstnull, Encoding encoding) + { + var len = data.Length; + if (endatfirstnull) + { + len = Array.IndexOf(data, (byte)0); + if (len == -1) + { + len = data.Length; + } + } + + return encoding.GetString(data, 0, len); + } + + /// + /// Returns an ASCII string converted from the given byte array. + /// + public static string ToAscii(byte[] data, Encoding encoding) => ToAscii(data, true, encoding); + + /// + /// Returns a string converted from the given byte array. + /// from the numeric value of each byte. + /// + public static string ToString(byte[] data) + { + var sb = new StringBuilder(); + foreach (var b in data) + { + sb.Append(b); + } + + return sb.ToString(); + } + + /// + /// Returns a DateTime object converted from the given byte array. + /// + public static DateTime ToDateTime(byte[] data, bool hastime) + { + var str = ToAscii(data, Encoding.ASCII); + var parts = str.Split(':', ' '); + try + { + if (hastime && parts.Length == 6) + { + // yyyy:MM:dd HH:mm:ss + // This is the expected format though some cameras + // can use single digits. See Issue 21. + return new DateTime( + int.Parse(parts[0], CultureInfo.InvariantCulture), + int.Parse(parts[1], CultureInfo.InvariantCulture), + int.Parse(parts[2], CultureInfo.InvariantCulture), + int.Parse(parts[3], CultureInfo.InvariantCulture), + int.Parse(parts[4], CultureInfo.InvariantCulture), + int.Parse(parts[5], CultureInfo.InvariantCulture)); + } + + if (!hastime && parts.Length == 3) + { + // yyyy:MM:dd + return new DateTime( + int.Parse(parts[0], CultureInfo.InvariantCulture), + int.Parse(parts[1], CultureInfo.InvariantCulture), + int.Parse(parts[2], CultureInfo.InvariantCulture)); + } + + return DateTime.MinValue; + } + catch (ArgumentOutOfRangeException) + { + return DateTime.MinValue; + } + catch (ArgumentException) + { + return DateTime.MinValue; + } + } + + /// + /// Returns a DateTime object converted from the given byte array. + /// + public static DateTime ToDateTime(byte[] data) => ToDateTime(data, true); + + /// + /// Returns an unsigned rational number converted from the first + /// eight bytes of the given byte array. The first four bytes are + /// assumed to be the numerator and the next four bytes are the + /// denominator. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static MathEx.UFraction32 ToURational(byte[] data, ByteOrder frombyteorder) + { + var num = new byte[4]; + var den = new byte[4]; + Array.Copy(data, 0, num, 0, 4); + Array.Copy(data, 4, den, 0, 4); + return new MathEx.UFraction32( + ToUInt32(num, 0, frombyteorder, SystemByteOrder), + ToUInt32(den, 0, frombyteorder, SystemByteOrder)); + } + + /// + /// Returns a signed rational number converted from the first + /// eight bytes of the given byte array. The first four bytes are + /// assumed to be the numerator and the next four bytes are the + /// denominator. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static MathEx.Fraction32 ToSRational(byte[] data, ByteOrder frombyteorder) + { + var num = new byte[4]; + var den = new byte[4]; + Array.Copy(data, 0, num, 0, 4); + Array.Copy(data, 4, den, 0, 4); + return new MathEx.Fraction32( + ToInt32(num, 0, frombyteorder, SystemByteOrder), + ToInt32(den, 0, frombyteorder, SystemByteOrder)); + } + + /// + /// Returns an array of 16-bit unsigned integers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static ushort[] ToUShortArray(byte[] data, int count, ByteOrder frombyteorder) + { + var numbers = new ushort[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[2]; + Array.Copy(data, i * 2, num, 0, 2); + numbers[i] = ToUInt16(num, 0, frombyteorder, SystemByteOrder); + } + + return numbers; + } + + /// + /// Returns an array of 32-bit unsigned integers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static uint[] ToUIntArray(byte[] data, int count, ByteOrder frombyteorder) + { + var numbers = new uint[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[4]; + Array.Copy(data, i * 4, num, 0, 4); + numbers[i] = ToUInt32(num, 0, frombyteorder, SystemByteOrder); + } + + return numbers; + } + + /// + /// Returns an array of 32-bit signed integers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static int[] ToSIntArray(byte[] data, int count, ByteOrder byteorder) + { + var numbers = new int[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[4]; + Array.Copy(data, i * 4, num, 0, 4); + numbers[i] = ToInt32(num, 0, byteorder, SystemByteOrder); + } + + return numbers; + } + + /// + /// Returns an array of unsigned rational numbers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static MathEx.UFraction32[] ToURationalArray(byte[] data, int count, ByteOrder frombyteorder) + { + var numbers = new MathEx.UFraction32[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[4]; + var den = new byte[4]; + Array.Copy(data, i * 8, num, 0, 4); + Array.Copy(data, (i * 8) + 4, den, 0, 4); + numbers[i].Set( + ToUInt32(num, 0, frombyteorder, SystemByteOrder), + ToUInt32(den, 0, frombyteorder, SystemByteOrder)); + } + + return numbers; + } + + /// + /// Returns an array of signed rational numbers converted from + /// the given byte array. + /// Numbers are converted from the given byte-order to platform byte-order. + /// + public static MathEx.Fraction32[] ToSRationalArray(byte[] data, int count, ByteOrder frombyteorder) + { + var numbers = new MathEx.Fraction32[count]; + for (uint i = 0; i < count; i++) + { + var num = new byte[4]; + var den = new byte[4]; + Array.Copy(data, i * 8, num, 0, 4); + Array.Copy(data, (i * 8) + 4, den, 0, 4); + numbers[i].Set( + ToInt32(num, 0, frombyteorder, SystemByteOrder), + ToInt32(den, 0, frombyteorder, SystemByteOrder)); + } + + return numbers; + } + + /// + /// Converts the given ascii string to an array of bytes optionally adding a null terminator. + /// + public static byte[] GetBytes(string value, bool addnull, Encoding encoding) + { + if (addnull) + { + value += '\0'; + } + + return encoding.GetBytes(value); + } + + /// + /// Converts the given ascii string to an array of bytes without adding a null terminator. + /// + public static byte[] GetBytes(string value, Encoding encoding) => GetBytes(value, false, encoding); + + /// + /// Converts the given datetime to an array of bytes with a null terminator. + /// + public static byte[] GetBytes(DateTime value, bool hastime) + { + var str = string.Empty; + if (hastime) + { + str = value.ToString("yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture); + } + else + { + str = value.ToString("yyyy:MM:dd", CultureInfo.InvariantCulture); + } + + return GetBytes(str, true, Encoding.ASCII); + } + + /// + /// Converts the given unsigned rational number to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(MathEx.UFraction32 value, ByteOrder tobyteorder) + { + var num = GetBytes(value.Numerator, SystemByteOrder, tobyteorder); + var den = GetBytes(value.Denominator, SystemByteOrder, tobyteorder); + var data = new byte[8]; + Array.Copy(num, 0, data, 0, 4); + Array.Copy(den, 0, data, 4, 4); + return data; + } + + /// + /// Converts the given signed rational number to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(MathEx.Fraction32 value, ByteOrder tobyteorder) + { + var num = GetBytes(value.Numerator, SystemByteOrder, tobyteorder); + var den = GetBytes(value.Denominator, SystemByteOrder, tobyteorder); + var data = new byte[8]; + Array.Copy(num, 0, data, 0, 4); + Array.Copy(den, 0, data, 4, 4); + return data; + } + + /// + /// Converts the given array of 16-bit unsigned integers to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(ushort[] value, ByteOrder tobyteorder) + { + var data = new byte[2 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 2, 2); + } + + return data; + } + + /// + /// Converts the given array of 32-bit unsigned integers to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(uint[] value, ByteOrder tobyteorder) + { + var data = new byte[4 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 4, 4); + } + + return data; + } + + /// + /// Converts the given array of 32-bit signed integers to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(int[] value, ByteOrder tobyteorder) + { + var data = new byte[4 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i], SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 4, 4); + } + + return data; + } + + /// + /// Converts the given array of unsigned rationals to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(MathEx.UFraction32[] value, ByteOrder tobyteorder) + { + var data = new byte[8 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i].Numerator, SystemByteOrder, tobyteorder); + var den = GetBytes(value[i].Denominator, SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 8, 4); + Array.Copy(den, 0, data, (i * 8) + 4, 4); + } + + return data; + } + + /// + /// Converts the given array of signed rationals to an array of bytes. + /// Numbers are converted from the platform byte-order to the given byte-order. + /// + public static byte[] GetBytes(MathEx.Fraction32[] value, ByteOrder tobyteorder) + { + var data = new byte[8 * value.Length]; + for (var i = 0; i < value.Length; i++) + { + var num = GetBytes(value[i].Numerator, SystemByteOrder, tobyteorder); + var den = GetBytes(value[i].Denominator, SystemByteOrder, tobyteorder); + Array.Copy(num, 0, data, i * 8, 4); + Array.Copy(den, 0, data, (i * 8) + 4, 4); + } + + return data; + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifEnums.cs b/src/Umbraco.Core/Media/Exif/ExifEnums.cs index 1ce0ec4891..5e27b8dd24 100644 --- a/src/Umbraco.Core/Media/Exif/ExifEnums.cs +++ b/src/Umbraco.Core/Media/Exif/ExifEnums.cs @@ -1,292 +1,297 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +internal enum Compression : ushort { - internal enum Compression : ushort - { - Uncompressed = 1, - CCITT1D = 2, - Group3Fax = 3, - Group4Fax = 4, - LZW = 5, - JPEG = 6, - PackBits = 32773, - } - - internal enum PhotometricInterpretation : ushort - { - WhiteIsZero = 0, - BlackIsZero = 1, - RGB = 2, - RGBPalette = 3, - TransparencyMask = 4, - CMYK = 5, - YCbCr = 6, - CIELab = 8, - } - - internal enum Orientation : ushort - { - Normal = 1, - MirroredVertically = 2, - Rotated180 = 3, - MirroredHorizontally = 4, - RotatedLeftAndMirroredVertically = 5, - RotatedRight = 6, - RotatedLeft = 7, - RotatedRightAndMirroredVertically = 8, - } - - internal enum PlanarConfiguration : ushort - { - ChunkyFormat = 1, - PlanarFormat = 2, - } - - internal enum YCbCrPositioning : ushort - { - Centered = 1, - CoSited = 2, - } - - internal enum ResolutionUnit : ushort - { - Inches = 2, - Centimeters = 3, - } - - internal enum ColorSpace : ushort - { - sRGB = 1, - Uncalibrated = 0xfff, - } - - internal enum ExposureProgram : ushort - { - NotDefined = 0, - Manual = 1, - Normal = 2, - AperturePriority = 3, - ShutterPriority = 4, - /// - /// Biased toward depth of field. - /// - Creative = 5, - /// - /// Biased toward fast shutter speed. - /// - Action = 6, - /// - /// For closeup photos with the background out of focus. - /// - Portrait = 7, - /// - /// For landscape photos with the background in focus. - /// - Landscape = 8, - } - - internal enum MeteringMode : ushort - { - Unknown = 0, - Average = 1, - CenterWeightedAverage = 2, - Spot = 3, - MultiSpot = 4, - Pattern = 5, - Partial = 6, - Other = 255, - } - - internal enum LightSource : ushort - { - Unknown = 0, - Daylight = 1, - Fluorescent = 2, - Tungsten = 3, - Flash = 4, - FineWeather = 9, - CloudyWeather = 10, - Shade = 11, - /// - /// D 5700 – 7100K - /// - DaylightFluorescent = 12, - /// - /// N 4600 – 5400K - /// - DayWhiteFluorescent = 13, - /// - /// W 3900 – 4500K - /// - CoolWhiteFluorescent = 14, - /// - /// WW 3200 – 3700K - /// - WhiteFluorescent = 15, - StandardLightA = 17, - StandardLightB = 18, - StandardLightC = 19, - D55 = 20, - D65 = 21, - D75 = 22, - D50 = 23, - ISOStudioTungsten = 24, - OtherLightSource = 255, - } - - [Flags] - internal enum Flash : ushort - { - FlashDidNotFire = 0, - StrobeReturnLightNotDetected = 4, - StrobeReturnLightDetected = 2, - FlashFired = 1, - CompulsoryFlashMode = 8, - AutoMode = 16, - NoFlashFunction = 32, - RedEyeReductionMode = 64, - } - - internal enum SensingMethod : ushort - { - NotDefined = 1, - OneChipColorAreaSensor = 2, - TwoChipColorAreaSensor = 3, - ThreeChipColorAreaSensor = 4, - ColorSequentialAreaSensor = 5, - TriLinearSensor = 7, - ColorSequentialLinearSensor = 8, - } - - internal enum FileSource : byte // UNDEFINED - { - DSC = 3, - } - - internal enum SceneType : byte // UNDEFINED - { - DirectlyPhotographedImage = 1, - } - - internal enum CustomRendered : ushort - { - NormalProcess = 0, - CustomProcess = 1, - } - - internal enum ExposureMode : ushort - { - Auto = 0, - Manual = 1, - AutoBracket = 2, - } - - internal enum WhiteBalance : ushort - { - Auto = 0, - Manual = 1, - } - - internal enum SceneCaptureType : ushort - { - Standard = 0, - Landscape = 1, - Portrait = 2, - NightScene = 3, - } - - internal enum GainControl : ushort - { - None = 0, - LowGainUp = 1, - HighGainUp = 2, - LowGainDown = 3, - HighGainDown = 4, - } - - internal enum Contrast : ushort - { - Normal = 0, - Soft = 1, - Hard = 2, - } - - internal enum Saturation : ushort - { - Normal = 0, - Low = 1, - High = 2, - } - - internal enum Sharpness : ushort - { - Normal = 0, - Soft = 1, - Hard = 2, - } - - internal enum SubjectDistanceRange : ushort - { - Unknown = 0, - Macro = 1, - CloseView = 2, - DistantView = 3, - } - - internal enum GPSLatitudeRef : byte // ASCII - { - North = 78, // 'N' - South = 83, // 'S' - } - - internal enum GPSLongitudeRef : byte // ASCII - { - West = 87, // 'W' - East = 69, // 'E' - } - - internal enum GPSAltitudeRef : byte - { - AboveSeaLevel = 0, - BelowSeaLevel = 1, - } - - internal enum GPSStatus : byte // ASCII - { - MeasurementInProgress = 65, // 'A' - MeasurementInteroperability = 86, // 'V' - } - - internal enum GPSMeasureMode : byte // ASCII - { - TwoDimensional = 50, // '2' - ThreeDimensional = 51, // '3' - } - - internal enum GPSSpeedRef : byte // ASCII - { - KilometersPerHour = 75, // 'K' - MilesPerHour = 77, // 'M' - Knots = 78, // 'N' - } - - internal enum GPSDirectionRef : byte // ASCII - { - TrueDirection = 84, // 'T' - MagneticDirection = 77, // 'M' - } - - internal enum GPSDistanceRef : byte // ASCII - { - Kilometers = 75, // 'K' - Miles = 77, // 'M' - Knots = 78, // 'N' - } - - internal enum GPSDifferential : ushort - { - MeasurementWithoutDifferentialCorrection = 0, - DifferentialCorrectionApplied = 1, - } + Uncompressed = 1, + CCITT1D = 2, + Group3Fax = 3, + Group4Fax = 4, + LZW = 5, + JPEG = 6, + PackBits = 32773, +} + +internal enum PhotometricInterpretation : ushort +{ + WhiteIsZero = 0, + BlackIsZero = 1, + RGB = 2, + RGBPalette = 3, + TransparencyMask = 4, + CMYK = 5, + YCbCr = 6, + CIELab = 8, +} + +internal enum Orientation : ushort +{ + Normal = 1, + MirroredVertically = 2, + Rotated180 = 3, + MirroredHorizontally = 4, + RotatedLeftAndMirroredVertically = 5, + RotatedRight = 6, + RotatedLeft = 7, + RotatedRightAndMirroredVertically = 8, +} + +internal enum PlanarConfiguration : ushort +{ + ChunkyFormat = 1, + PlanarFormat = 2, +} + +internal enum YCbCrPositioning : ushort +{ + Centered = 1, + CoSited = 2, +} + +internal enum ResolutionUnit : ushort +{ + Inches = 2, + Centimeters = 3, +} + +internal enum ColorSpace : ushort +{ + SRGB = 1, + Uncalibrated = 0xfff, +} + +internal enum ExposureProgram : ushort +{ + NotDefined = 0, + Manual = 1, + Normal = 2, + AperturePriority = 3, + ShutterPriority = 4, + + /// + /// Biased toward depth of field. + /// + Creative = 5, + + /// + /// Biased toward fast shutter speed. + /// + Action = 6, + + /// + /// For closeup photos with the background out of focus. + /// + Portrait = 7, + + /// + /// For landscape photos with the background in focus. + /// + Landscape = 8, +} + +internal enum MeteringMode : ushort +{ + Unknown = 0, + Average = 1, + CenterWeightedAverage = 2, + Spot = 3, + MultiSpot = 4, + Pattern = 5, + Partial = 6, + Other = 255, +} + +internal enum LightSource : ushort +{ + Unknown = 0, + Daylight = 1, + Fluorescent = 2, + Tungsten = 3, + Flash = 4, + FineWeather = 9, + CloudyWeather = 10, + Shade = 11, + + /// + /// D 5700 – 7100K + /// + DaylightFluorescent = 12, + + /// + /// N 4600 – 5400K + /// + DayWhiteFluorescent = 13, + + /// + /// W 3900 – 4500K + /// + CoolWhiteFluorescent = 14, + + /// + /// WW 3200 – 3700K + /// + WhiteFluorescent = 15, + StandardLightA = 17, + StandardLightB = 18, + StandardLightC = 19, + D55 = 20, + D65 = 21, + D75 = 22, + D50 = 23, + ISOStudioTungsten = 24, + OtherLightSource = 255, +} + +[Flags] +internal enum Flash : ushort +{ + FlashDidNotFire = 0, + StrobeReturnLightNotDetected = 4, + StrobeReturnLightDetected = 2, + FlashFired = 1, + CompulsoryFlashMode = 8, + AutoMode = 16, + NoFlashFunction = 32, + RedEyeReductionMode = 64, +} + +internal enum SensingMethod : ushort +{ + NotDefined = 1, + OneChipColorAreaSensor = 2, + TwoChipColorAreaSensor = 3, + ThreeChipColorAreaSensor = 4, + ColorSequentialAreaSensor = 5, + TriLinearSensor = 7, + ColorSequentialLinearSensor = 8, +} + +internal enum FileSource : byte // UNDEFINED +{ + DSC = 3, +} + +internal enum SceneType : byte // UNDEFINED +{ + DirectlyPhotographedImage = 1, +} + +internal enum CustomRendered : ushort +{ + NormalProcess = 0, + CustomProcess = 1, +} + +internal enum ExposureMode : ushort +{ + Auto = 0, + Manual = 1, + AutoBracket = 2, +} + +internal enum WhiteBalance : ushort +{ + Auto = 0, + Manual = 1, +} + +internal enum SceneCaptureType : ushort +{ + Standard = 0, + Landscape = 1, + Portrait = 2, + NightScene = 3, +} + +internal enum GainControl : ushort +{ + None = 0, + LowGainUp = 1, + HighGainUp = 2, + LowGainDown = 3, + HighGainDown = 4, +} + +internal enum Contrast : ushort +{ + Normal = 0, + Soft = 1, + Hard = 2, +} + +internal enum Saturation : ushort +{ + Normal = 0, + Low = 1, + High = 2, +} + +internal enum Sharpness : ushort +{ + Normal = 0, + Soft = 1, + Hard = 2, +} + +internal enum SubjectDistanceRange : ushort +{ + Unknown = 0, + Macro = 1, + CloseView = 2, + DistantView = 3, +} + +internal enum GPSLatitudeRef : byte // ASCII +{ + North = 78, // 'N' + South = 83, // 'S' +} + +internal enum GPSLongitudeRef : byte // ASCII +{ + West = 87, // 'W' + East = 69, // 'E' +} + +internal enum GPSAltitudeRef : byte +{ + AboveSeaLevel = 0, + BelowSeaLevel = 1, +} + +internal enum GPSStatus : byte // ASCII +{ + MeasurementInProgress = 65, // 'A' + MeasurementInteroperability = 86, // 'V' +} + +internal enum GPSMeasureMode : byte // ASCII +{ + TwoDimensional = 50, // '2' + ThreeDimensional = 51, // '3' +} + +internal enum GPSSpeedRef : byte // ASCII +{ + KilometersPerHour = 75, // 'K' + MilesPerHour = 77, // 'M' + Knots = 78, // 'N' +} + +internal enum GPSDirectionRef : byte // ASCII +{ + TrueDirection = 84, // 'T' + MagneticDirection = 77, // 'M' +} + +internal enum GPSDistanceRef : byte // ASCII +{ + Kilometers = 75, // 'K' + Miles = 77, // 'M' + Knots = 78, // 'N' +} + +internal enum GPSDifferential : ushort +{ + MeasurementWithoutDifferentialCorrection = 0, + DifferentialCorrectionApplied = 1, } diff --git a/src/Umbraco.Core/Media/Exif/ExifExceptions.cs b/src/Umbraco.Core/Media/Exif/ExifExceptions.cs index 3d0472c100..bd4426e15a 100644 --- a/src/Umbraco.Core/Media/Exif/ExifExceptions.cs +++ b/src/Umbraco.Core/Media/Exif/ExifExceptions.cs @@ -1,47 +1,57 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// The exception that is thrown when the format of the JPEG/EXIF file could not be understood. +/// +/// +[Serializable] +public class NotValidExifFileException : Exception { /// - /// The exception that is thrown when the format of the JPEG/EXIF file could not be understood. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class NotValidExifFileException : Exception + public NotValidExifFileException() + : base("Not a valid JPEG/EXIF file.") { - /// - /// Initializes a new instance of the class. - /// - public NotValidExifFileException() - : base("Not a valid JPEG/EXIF file.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public NotValidExifFileException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotValidExifFileException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected NotValidExifFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public NotValidExifFileException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public NotValidExifFileException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected NotValidExifFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } diff --git a/src/Umbraco.Core/Media/Exif/ExifExtendedProperty.cs b/src/Umbraco.Core/Media/Exif/ExifExtendedProperty.cs index 9aa62f4ea3..ffa31f0cc1 100644 --- a/src/Umbraco.Core/Media/Exif/ExifExtendedProperty.cs +++ b/src/Umbraco.Core/Media/Exif/ExifExtendedProperty.cs @@ -1,374 +1,487 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif -{ - /// - /// Represents an enumerated value. - /// - internal class ExifEnumProperty : ExifProperty +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents an enumerated value. +/// +internal class ExifEnumProperty : ExifProperty where T : notnull +{ + protected bool mIsBitField; + protected T mValue; + + public ExifEnumProperty(ExifTag tag, T value, bool isbitfield) + : base(tag) { - protected T mValue; - protected bool mIsBitField; - protected override object _Value { get { return Value; } set { Value = (T)value; } } - public new T Value { get { return mValue; } set { mValue = value; } } - public bool IsBitField { get { return mIsBitField; } } + mValue = value; + mIsBitField = isbitfield; + } - static public implicit operator T(ExifEnumProperty obj) { return (T)obj.mValue; } + public ExifEnumProperty(ExifTag tag, T value) + : this(tag, value, false) + { + } - public override string? ToString() { return mValue.ToString(); } + public new T Value + { + get => mValue; + set => mValue = value; + } - public ExifEnumProperty(ExifTag tag, T value, bool isbitfield) - : base(tag) + protected override object _Value + { + get => Value; + set => Value = (T)value; + } + + public bool IsBitField => mIsBitField; + + public override ExifInterOperability Interoperability + { + get { - mValue = value; - mIsBitField = isbitfield; - } + var tagid = ExifTagFactory.GetTagID(mTag); - public ExifEnumProperty(ExifTag tag, T value) - : this(tag, value, false) - { + Type type = typeof(T); + Type basetype = Enum.GetUnderlyingType(type); - } - - public override ExifInterOperability Interoperability - { - get + if (type == typeof(FileSource) || type == typeof(SceneType)) { - ushort tagid = ExifTagFactory.GetTagID(mTag); - - Type type = typeof(T); - Type basetype = Enum.GetUnderlyingType(type); - - if (type == typeof(FileSource) || type == typeof(SceneType)) - { - // UNDEFINED - return new ExifInterOperability(tagid, 7, 1, new byte[] { (byte)((object)mValue) }); - } - else if (type == typeof(GPSLatitudeRef) || type == typeof(GPSLongitudeRef) || - type == typeof(GPSStatus) || type == typeof(GPSMeasureMode) || - type == typeof(GPSSpeedRef) || type == typeof(GPSDirectionRef) || - type == typeof(GPSDistanceRef)) - { - // ASCII - return new ExifInterOperability(tagid, 2, 2, new byte[] { (byte)((object)mValue), 0 }); - } - else if (basetype == typeof(byte)) - { - // BYTE - return new ExifInterOperability(tagid, 1, 1, new byte[] { (byte)((object)mValue) }); - } - else if (basetype == typeof(ushort)) - { - // SHORT - return new ExifInterOperability(tagid, 3, 1, ExifBitConverter.GetBytes((ushort)((object)mValue), BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - else - throw new InvalidOperationException($"An invalid enum type ({basetype.FullName}) was provided for type {type.FullName}"); + // UNDEFINED + return new ExifInterOperability(tagid, 7, 1, new[] { (byte)(object)mValue }); } + + if (type == typeof(GPSLatitudeRef) || type == typeof(GPSLongitudeRef) || + type == typeof(GPSStatus) || type == typeof(GPSMeasureMode) || + type == typeof(GPSSpeedRef) || type == typeof(GPSDirectionRef) || + type == typeof(GPSDistanceRef)) + { + // ASCII + return new ExifInterOperability(tagid, 2, 2, new byte[] { (byte)(object)mValue, 0 }); + } + + if (basetype == typeof(byte)) + { + // BYTE + return new ExifInterOperability(tagid, 1, 1, new[] { (byte)(object)mValue }); + } + + if (basetype == typeof(ushort)) + { + // SHORT + return new ExifInterOperability( + tagid, + 3, + 1, + BitConverterEx.GetBytes((ushort)(object)mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); + } + + throw new InvalidOperationException( + $"An invalid enum type ({basetype.FullName}) was provided for type {type.FullName}"); } } - /// - /// Represents an ASCII string. (EXIF Specification: UNDEFINED) Used for the UserComment field. - /// - internal class ExifEncodedString : ExifProperty + public static implicit operator T(ExifEnumProperty obj) => obj.mValue; + + public override string? ToString() => mValue.ToString(); +} + +/// +/// Represents an ASCII string. (EXIF Specification: UNDEFINED) Used for the UserComment field. +/// +internal class ExifEncodedString : ExifProperty +{ + protected string mValue; + + public ExifEncodedString(ExifTag tag, string value, Encoding encoding) + : base(tag) { - protected string mValue; - private Encoding mEncoding; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value; } } - public Encoding Encoding { get { return mEncoding; } set { mEncoding = value; } } - - static public implicit operator string(ExifEncodedString obj) { return obj.mValue; } - - public override string ToString() { return mValue; } - - public ExifEncodedString(ExifTag tag, string value, Encoding encoding) - : base(tag) - { - mValue = value; - mEncoding = encoding; - } - - public override ExifInterOperability Interoperability - { - get - { - string enc = ""; - if (mEncoding == null) - enc = "\0\0\0\0\0\0\0\0"; - else if (mEncoding.EncodingName == "US-ASCII") - enc = "ASCII\0\0\0"; - else if (mEncoding.EncodingName == "Japanese (JIS 0208-1990 and 0212-1990)") - enc = "JIS\0\0\0\0\0"; - else if (mEncoding.EncodingName == "Unicode") - enc = "Unicode\0"; - else - enc = "\0\0\0\0\0\0\0\0"; - - byte[] benc = Encoding.ASCII.GetBytes(enc); - byte[] bstr = (mEncoding == null ? Encoding.ASCII.GetBytes(mValue) : mEncoding.GetBytes(mValue)); - byte[] data = new byte[benc.Length + bstr.Length]; - Array.Copy(benc, 0, data, 0, benc.Length); - Array.Copy(bstr, 0, data, benc.Length, bstr.Length); - - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)data.Length, data); - } - } + mValue = value; + Encoding = encoding; } - /// - /// Represents an ASCII string formatted as DateTime. (EXIF Specification: ASCII) Used for the date time fields. - /// - internal class ExifDateTime : ExifProperty + public new string Value { - protected DateTime mValue; - protected override object _Value { get { return Value; } set { Value = (DateTime)value; } } - public new DateTime Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator DateTime(ExifDateTime obj) { return obj.mValue; } - - public override string ToString() { return mValue.ToString("yyyy.MM.dd HH:mm:ss"); } - - public ExifDateTime(ExifTag tag, DateTime value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 2, (uint)20, ExifBitConverter.GetBytes(mValue, true)); - } - } + get => mValue; + set => mValue = value; } - /// - /// Represents the exif version as a 4 byte ASCII string. (EXIF Specification: UNDEFINED) - /// Used for the ExifVersion, FlashpixVersion, InteroperabilityVersion and GPSVersionID fields. - /// - internal class ExifVersion : ExifProperty + protected override object _Value { - protected string mValue; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value.Substring(0, 4); } } + get => Value; + set => Value = (string)value; + } - public ExifVersion(ExifTag tag, string value) - : base(tag) + public Encoding Encoding { get; set; } + + public override ExifInterOperability Interoperability + { + get { - if (value.Length > 4) - mValue = value.Substring(0, 4); - else if (value.Length < 4) - mValue = value + new string(' ', 4 - value.Length); + var enc = string.Empty; + if (Encoding == null) + { + enc = "\0\0\0\0\0\0\0\0"; + } + else if (Encoding.EncodingName == "US-ASCII") + { + enc = "ASCII\0\0\0"; + } + else if (Encoding.EncodingName == "Japanese (JIS 0208-1990 and 0212-1990)") + { + enc = "JIS\0\0\0\0\0"; + } + else if (Encoding.EncodingName == "Unicode") + { + enc = "Unicode\0"; + } else - mValue = value; - } - - public override string ToString() - { - return mValue; - } - - public override ExifInterOperability Interoperability - { - get { - if (mTag == ExifTag.ExifVersion || mTag == ExifTag.FlashpixVersion || mTag == ExifTag.InteroperabilityVersion) - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, 4, Encoding.ASCII.GetBytes(mValue)); - else - { - byte[] data = new byte[4]; - for (int i = 0; i < 4; i++) - data[i] = byte.Parse(mValue[0].ToString()); - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, 4, data); - } + enc = "\0\0\0\0\0\0\0\0"; } + + var benc = Encoding.ASCII.GetBytes(enc); + var bstr = Encoding == null ? Encoding.ASCII.GetBytes(mValue) : Encoding.GetBytes(mValue); + var data = new byte[benc.Length + bstr.Length]; + Array.Copy(benc, 0, data, 0, benc.Length); + Array.Copy(bstr, 0, data, benc.Length, bstr.Length); + + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)data.Length, data); } } - /// - /// Represents the location and area of the subject (EXIF Specification: 2xSHORT) - /// The coordinate values, width, and height are expressed in relation to the - /// upper left as origin, prior to rotation processing as per the Rotation tag. - /// - internal class ExifPointSubjectArea : ExifUShortArray + public static implicit operator string(ExifEncodedString obj) => obj.mValue; + + public override string ToString() => mValue; +} + +/// +/// Represents an ASCII string formatted as DateTime. (EXIF Specification: ASCII) Used for the date time fields. +/// +internal class ExifDateTime : ExifProperty +{ + protected DateTime mValue; + + public ExifDateTime(ExifTag tag, DateTime value) + : base(tag) => + mValue = value; + + public new DateTime Value { - protected new ushort[] Value { get { return mValue; } set { mValue = value; } } - public ushort X { get { return mValue[0]; } set { mValue[0] = value; } } - public ushort Y { get { return mValue[1]; } set { mValue[1] = value; } } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("({0:d}, {1:d})", mValue[0], mValue[1]); - return sb.ToString(); - } - - public ExifPointSubjectArea(ExifTag tag, ushort[] value) - : base(tag, value) - { - - } - - public ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) - : base(tag, new ushort[] {x, y}) - { - - } + get => mValue; + set => mValue = value; } - /// - /// Represents the location and area of the subject (EXIF Specification: 3xSHORT) - /// The coordinate values, width, and height are expressed in relation to the - /// upper left as origin, prior to rotation processing as per the Rotation tag. - /// - internal class ExifCircularSubjectArea : ExifPointSubjectArea + protected override object _Value { - public ushort Diamater { get { return mValue[2]; } set { mValue[2] = value; } } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("({0:d}, {1:d}) {2:d}", mValue[0], mValue[1], mValue[2]); - return sb.ToString(); - } - - public ExifCircularSubjectArea(ExifTag tag, ushort[] value) - : base(tag, value) - { - - } - - public ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) - : base(tag, new ushort[] { x, y, d }) - { - - } + get => Value; + set => Value = (DateTime)value; } - /// - /// Represents the location and area of the subject (EXIF Specification: 4xSHORT) - /// The coordinate values, width, and height are expressed in relation to the - /// upper left as origin, prior to rotation processing as per the Rotation tag. - /// - internal class ExifRectangularSubjectArea : ExifPointSubjectArea + public override ExifInterOperability Interoperability => + new(ExifTagFactory.GetTagID(mTag), 2, 20, ExifBitConverter.GetBytes(mValue, true)); + + public static implicit operator DateTime(ExifDateTime obj) => obj.mValue; + + public override string ToString() => mValue.ToString("yyyy.MM.dd HH:mm:ss"); +} + +/// +/// Represents the exif version as a 4 byte ASCII string. (EXIF Specification: UNDEFINED) +/// Used for the ExifVersion, FlashpixVersion, InteroperabilityVersion and GPSVersionID fields. +/// +internal class ExifVersion : ExifProperty +{ + protected string mValue; + + public ExifVersion(ExifTag tag, string value) + : base(tag) { - public ushort Width { get { return mValue[2]; } set { mValue[2] = value; } } - public ushort Height { get { return mValue[3]; } set { mValue[3] = value; } } - - public override string ToString() + if (value.Length > 4) { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("({0:d}, {1:d}) ({2:d} x {3:d})", mValue[0], mValue[1], mValue[2], mValue[3]); - return sb.ToString(); + mValue = value[..4]; } - - public ExifRectangularSubjectArea(ExifTag tag, ushort[] value) - : base(tag, value) + else if (value.Length < 4) { - + mValue = value + new string(' ', 4 - value.Length); } - - public ExifRectangularSubjectArea(ExifTag tag, ushort x, ushort y, ushort w, ushort h) - : base(tag, new ushort[] { x, y, w, h }) - { - - } - } - - /// - /// Represents GPS latitudes and longitudes (EXIF Specification: 3xRATIONAL) - /// - internal class GPSLatitudeLongitude : ExifURationalArray - { - protected new MathEx.UFraction32[] Value { get { return mValue; } set { mValue = value; } } - public MathEx.UFraction32 Degrees { get { return mValue[0]; } set { mValue[0] = value; } } - public MathEx.UFraction32 Minutes { get { return mValue[1]; } set { mValue[1] = value; } } - public MathEx.UFraction32 Seconds { get { return mValue[2]; } set { mValue[2] = value; } } - - public static explicit operator float(GPSLatitudeLongitude obj) { return obj.ToFloat(); } - public float ToFloat() - { - return (float)Degrees + ((float)Minutes) / 60.0f + ((float)Seconds) / 3600.0f; - } - - public override string ToString() - { - return string.Format("{0:F2}°{1:F2}'{2:F2}\"", (float)Degrees, (float)Minutes, (float)Seconds); - } - - public GPSLatitudeLongitude(ExifTag tag, MathEx.UFraction32[] value) - : base(tag, value) - { - - } - - public GPSLatitudeLongitude(ExifTag tag, float d, float m, float s) - : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) - { - - } - } - - /// - /// Represents a GPS time stamp as UTC (EXIF Specification: 3xRATIONAL) - /// - internal class GPSTimeStamp : ExifURationalArray - { - protected new MathEx.UFraction32[] Value { get { return mValue; } set { mValue = value; } } - public MathEx.UFraction32 Hour { get { return mValue[0]; } set { mValue[0] = value; } } - public MathEx.UFraction32 Minute { get { return mValue[1]; } set { mValue[1] = value; } } - public MathEx.UFraction32 Second { get { return mValue[2]; } set { mValue[2] = value; } } - - public override string ToString() - { - return string.Format("{0:F2}:{1:F2}:{2:F2}\"", (float)Hour, (float)Minute, (float)Second); - } - - public GPSTimeStamp(ExifTag tag, MathEx.UFraction32[] value) - : base(tag, value) - { - - } - - public GPSTimeStamp(ExifTag tag, float h, float m, float s) - : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(h), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) - { - - } - } - - /// - /// Represents an ASCII string. (EXIF Specification: BYTE) - /// Used by Windows XP. - /// - internal class WindowsByteString : ExifProperty - { - protected string mValue; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator string(WindowsByteString obj) { return obj.mValue; } - - public override string ToString() { return mValue; } - - public WindowsByteString(ExifTag tag, string value) - : base(tag) + else { mValue = value; } + } - public override ExifInterOperability Interoperability + public new string Value + { + get => mValue; + set => mValue = value[..4]; + } + + protected override object _Value + { + get => Value; + set => Value = (string)value; + } + + public override ExifInterOperability Interoperability + { + get { - get + if (mTag == ExifTag.ExifVersion || mTag == ExifTag.FlashpixVersion || + mTag == ExifTag.InteroperabilityVersion) { - byte[] data = Encoding.Unicode.GetBytes(mValue); - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, 4, Encoding.ASCII.GetBytes(mValue)); } + + var data = new byte[4]; + for (var i = 0; i < 4; i++) + { + data[i] = byte.Parse(mValue[0].ToString()); + } + + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, 4, data); } } + + public override string ToString() => mValue; +} + +/// +/// Represents the location and area of the subject (EXIF Specification: 2xSHORT) +/// The coordinate values, width, and height are expressed in relation to the +/// upper left as origin, prior to rotation processing as per the Rotation tag. +/// +internal class ExifPointSubjectArea : ExifUShortArray +{ + public ExifPointSubjectArea(ExifTag tag, ushort[] value) + : base(tag, value) + { + } + + public ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) + : base(tag, new[] { x, y }) + { + } + + public ushort X + { + get => mValue[0]; + set => mValue[0] = value; + } + + protected new ushort[] Value + { + get => mValue; + set => mValue = value; + } + + public ushort Y + { + get => mValue[1]; + set => mValue[1] = value; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("({0:d}, {1:d})", mValue[0], mValue[1]); + return sb.ToString(); + } +} + +/// +/// Represents the location and area of the subject (EXIF Specification: 3xSHORT) +/// The coordinate values, width, and height are expressed in relation to the +/// upper left as origin, prior to rotation processing as per the Rotation tag. +/// +internal class ExifCircularSubjectArea : ExifPointSubjectArea +{ + public ExifCircularSubjectArea(ExifTag tag, ushort[] value) + : base(tag, value) + { + } + + public ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) + : base(tag, new[] { x, y, d }) + { + } + + public ushort Diamater + { + get => mValue[2]; + set => mValue[2] = value; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("({0:d}, {1:d}) {2:d}", mValue[0], mValue[1], mValue[2]); + return sb.ToString(); + } +} + +/// +/// Represents the location and area of the subject (EXIF Specification: 4xSHORT) +/// The coordinate values, width, and height are expressed in relation to the +/// upper left as origin, prior to rotation processing as per the Rotation tag. +/// +internal class ExifRectangularSubjectArea : ExifPointSubjectArea +{ + public ExifRectangularSubjectArea(ExifTag tag, ushort[] value) + : base(tag, value) + { + } + + public ExifRectangularSubjectArea(ExifTag tag, ushort x, ushort y, ushort w, ushort h) + : base(tag, new[] { x, y, w, h }) + { + } + + public ushort Width + { + get => mValue[2]; + set => mValue[2] = value; + } + + public ushort Height + { + get => mValue[3]; + set => mValue[3] = value; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("({0:d}, {1:d}) ({2:d} x {3:d})", mValue[0], mValue[1], mValue[2], mValue[3]); + return sb.ToString(); + } +} + +/// +/// Represents GPS latitudes and longitudes (EXIF Specification: 3xRATIONAL) +/// +internal class GPSLatitudeLongitude : ExifURationalArray +{ + public GPSLatitudeLongitude(ExifTag tag, MathEx.UFraction32[] value) + : base(tag, value) + { + } + + public GPSLatitudeLongitude(ExifTag tag, float d, float m, float s) + : base(tag, new[] { new(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) + { + } + + public MathEx.UFraction32 Degrees + { + get => mValue[0]; + set => mValue[0] = value; + } + + protected new MathEx.UFraction32[] Value + { + get => mValue; + set => mValue = value; + } + + public MathEx.UFraction32 Minutes + { + get => mValue[1]; + set => mValue[1] = value; + } + + public MathEx.UFraction32 Seconds + { + get => mValue[2]; + set => mValue[2] = value; + } + + public static explicit operator float(GPSLatitudeLongitude obj) => obj.ToFloat(); + + public float ToFloat() => (float)Degrees + ((float)Minutes / 60.0f) + ((float)Seconds / 3600.0f); + + public override string ToString() => + string.Format("{0:F2}°{1:F2}'{2:F2}\"", (float)Degrees, (float)Minutes, (float)Seconds); +} + +/// +/// Represents a GPS time stamp as UTC (EXIF Specification: 3xRATIONAL) +/// +internal class GPSTimeStamp : ExifURationalArray +{ + public GPSTimeStamp(ExifTag tag, MathEx.UFraction32[] value) + : base(tag, value) + { + } + + public GPSTimeStamp(ExifTag tag, float h, float m, float s) + : base(tag, new[] { new(h), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) + { + } + + public MathEx.UFraction32 Hour + { + get => mValue[0]; + set => mValue[0] = value; + } + + protected new MathEx.UFraction32[] Value + { + get => mValue; + set => mValue = value; + } + + public MathEx.UFraction32 Minute + { + get => mValue[1]; + set => mValue[1] = value; + } + + public MathEx.UFraction32 Second + { + get => mValue[2]; + set => mValue[2] = value; + } + + public override string ToString() => + string.Format("{0:F2}:{1:F2}:{2:F2}\"", (float)Hour, (float)Minute, (float)Second); +} + +/// +/// Represents an ASCII string. (EXIF Specification: BYTE) +/// Used by Windows XP. +/// +internal class WindowsByteString : ExifProperty +{ + protected string mValue; + + public WindowsByteString(ExifTag tag, string value) + : base(tag) => + mValue = value; + + public new string Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (string)value; + } + + public override ExifInterOperability Interoperability + { + get + { + var data = Encoding.Unicode.GetBytes(mValue); + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); + } + } + + public static implicit operator string(WindowsByteString obj) => obj.mValue; + + public override string ToString() => mValue; } diff --git a/src/Umbraco.Core/Media/Exif/ExifFileTypeDescriptor.cs b/src/Umbraco.Core/Media/Exif/ExifFileTypeDescriptor.cs index 61d6b70f30..a07ade9963 100644 --- a/src/Umbraco.Core/Media/Exif/ExifFileTypeDescriptor.cs +++ b/src/Umbraco.Core/Media/Exif/ExifFileTypeDescriptor.cs @@ -1,131 +1,108 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Provides a custom type descriptor for an ExifFile instance. +/// +internal sealed class ExifFileTypeDescriptionProvider : TypeDescriptionProvider { - /// - /// Provides a custom type descriptor for an ExifFile instance. - /// - internal sealed class ExifFileTypeDescriptionProvider : TypeDescriptionProvider + public ExifFileTypeDescriptionProvider() + : this(TypeDescriptor.GetProvider(typeof(ImageFile))) { - public ExifFileTypeDescriptionProvider() - : this(TypeDescriptor.GetProvider(typeof(ImageFile))) - { - } + } - public ExifFileTypeDescriptionProvider(TypeDescriptionProvider parent) - : base(parent) - { - } - - /// - /// Gets a custom type descriptor for the given type and object. - /// - /// The type of object for which to retrieve the type descriptor. - /// An instance of the type. Can be null if no instance was passed to the . - /// - /// An that can provide metadata for the type. - /// - public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object? instance) - { - return new ExifFileTypeDescriptor(base.GetTypeDescriptor(objectType, instance), instance); - } + public ExifFileTypeDescriptionProvider(TypeDescriptionProvider parent) + : base(parent) + { } /// - /// Expands ExifProperty objects contained in an ExifFile as separate properties. + /// Gets a custom type descriptor for the given type and object. /// - internal sealed class ExifFileTypeDescriptor : CustomTypeDescriptor + /// The type of object for which to retrieve the type descriptor. + /// + /// An instance of the type. Can be null if no instance was passed to the + /// . + /// + /// + /// An that can provide metadata for the type. + /// + public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object? instance) => + new ExifFileTypeDescriptor(base.GetTypeDescriptor(objectType, instance), instance); +} + +/// +/// Expands ExifProperty objects contained in an ExifFile as separate properties. +/// +internal sealed class ExifFileTypeDescriptor : CustomTypeDescriptor +{ + private readonly ImageFile? owner; + + public ExifFileTypeDescriptor(ICustomTypeDescriptor? parent, object? instance) + : base(parent) => + owner = (ImageFile?)instance; + + public override PropertyDescriptorCollection GetProperties(Attribute[]? attributes) => GetProperties(); + + /// + /// Returns a collection of property descriptors for the object represented by this type descriptor. + /// + /// + /// A containing the property descriptions for the + /// object represented by this type descriptor. The default is + /// . + /// + public override PropertyDescriptorCollection GetProperties() { - ImageFile? owner; + // Enumerate the original set of properties and create our new set with it + var properties = new List(); - public ExifFileTypeDescriptor(ICustomTypeDescriptor? parent, object? instance) - : base(parent) + if (owner is not null) { - owner = (ImageFile?)instance; - } - public override PropertyDescriptorCollection GetProperties(Attribute[]? attributes) - { - return GetProperties(); - } - /// - /// Returns a collection of property descriptors for the object represented by this type descriptor. - /// - /// - /// A containing the property descriptions for the object represented by this type descriptor. The default is . - /// - public override PropertyDescriptorCollection GetProperties() - { - // Enumerate the original set of properties and create our new set with it - List properties = new List(); - - if (owner is not null) + foreach (ExifProperty prop in owner.Properties) { - foreach (ExifProperty prop in owner.Properties) - { - ExifPropertyDescriptor pd = new ExifPropertyDescriptor(prop); - properties.Add(pd); - } - } - - // Finally return the list - return new PropertyDescriptorCollection(properties.ToArray(), true); - } - } - internal sealed class ExifPropertyDescriptor : PropertyDescriptor - { - object originalValue; - ExifProperty linkedProperty; - - public ExifPropertyDescriptor(ExifProperty property) - : base(property.Name, new Attribute[] { new BrowsableAttribute(true) }) - { - linkedProperty = property; - originalValue = property.Value; - } - - public override bool CanResetValue(object component) - { - return true; - } - - public override Type ComponentType - { - get { return typeof(JPEGFile); } - } - - public override object GetValue(object? component) - { - return linkedProperty.Value; - } - - public override bool IsReadOnly - { - get { return false; } - } - - public override Type PropertyType - { - get { return linkedProperty.Value.GetType(); } - } - - public override void ResetValue(object component) - { - linkedProperty.Value = originalValue; - } - - public override void SetValue(object? component, object? value) - { - if (value is not null) - { - linkedProperty.Value = value; + var pd = new ExifPropertyDescriptor(prop); + properties.Add(pd); } } - public override bool ShouldSerializeValue(object component) - { - return false; - } + // Finally return the list + return new PropertyDescriptorCollection(properties.ToArray(), true); } } + +internal sealed class ExifPropertyDescriptor : PropertyDescriptor +{ + private readonly ExifProperty linkedProperty; + private readonly object originalValue; + + public ExifPropertyDescriptor(ExifProperty property) + : base(property.Name, new Attribute[] { new BrowsableAttribute(true) }) + { + linkedProperty = property; + originalValue = property.Value; + } + + public override Type ComponentType => typeof(JPEGFile); + + public override bool IsReadOnly => false; + + public override Type PropertyType => linkedProperty.Value.GetType(); + + public override bool CanResetValue(object component) => true; + + public override object GetValue(object? component) => linkedProperty.Value; + + public override void ResetValue(object component) => linkedProperty.Value = originalValue; + + public override void SetValue(object? component, object? value) + { + if (value is not null) + { + linkedProperty.Value = value; + } + } + + public override bool ShouldSerializeValue(object component) => false; +} diff --git a/src/Umbraco.Core/Media/Exif/ExifInterOperability.cs b/src/Umbraco.Core/Media/Exif/ExifInterOperability.cs index 160ee38636..d2d9f8be6a 100644 --- a/src/Umbraco.Core/Media/Exif/ExifInterOperability.cs +++ b/src/Umbraco.Core/Media/Exif/ExifInterOperability.cs @@ -1,60 +1,55 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents interoperability data for an exif tag in the platform byte order. +/// +internal struct ExifInterOperability { - /// - /// Represents interoperability data for an exif tag in the platform byte order. - /// - internal struct ExifInterOperability + public ExifInterOperability(ushort tagid, ushort typeid, uint count, byte[] data) { - private ushort mTagID; - private ushort mTypeID; - private uint mCount; - private byte[] mData; - - /// - /// Gets the tag ID defined in the Exif standard. - /// - public ushort TagID { get { return mTagID; } } - /// - /// Gets the type code defined in the Exif standard. - /// - /// 1 = BYTE (byte) - /// 2 = ASCII (byte array) - /// 3 = SHORT (ushort) - /// 4 = LONG (uint) - /// 5 = RATIONAL (2 x uint: numerator, denominator) - /// 6 = BYTE (sbyte) - /// 7 = UNDEFINED (byte array) - /// 8 = SSHORT (short) - /// 9 = SLONG (int) - /// 10 = SRATIONAL (2 x int: numerator, denominator) - /// 11 = FLOAT (float) - /// 12 = DOUBLE (double) - /// - /// - public ushort TypeID { get { return mTypeID; } } - /// - /// Gets the byte count or number of components. - /// - public uint Count { get { return mCount; } } - /// - /// Gets the field value as an array of bytes. - /// - public byte[] Data { get { return mData; } } - /// - /// Returns the string representation of this instance. - /// - /// - public override string ToString() - { - return string.Format("Tag: {0}, Type: {1}, Count: {2}, Data Length: {3}", mTagID, mTypeID, mCount, mData.Length); - } - - public ExifInterOperability(ushort tagid, ushort typeid, uint count, byte[] data) - { - mTagID = tagid; - mTypeID = typeid; - mCount = count; - mData = data; - } + TagID = tagid; + TypeID = typeid; + Count = count; + Data = data; } + + /// + /// Gets the tag ID defined in the Exif standard. + /// + public ushort TagID { get; } + + /// + /// Gets the type code defined in the Exif standard. + /// + /// 1 = BYTE (byte) + /// 2 = ASCII (byte array) + /// 3 = SHORT (ushort) + /// 4 = LONG (uint) + /// 5 = RATIONAL (2 x uint: numerator, denominator) + /// 6 = BYTE (sbyte) + /// 7 = UNDEFINED (byte array) + /// 8 = SSHORT (short) + /// 9 = SLONG (int) + /// 10 = SRATIONAL (2 x int: numerator, denominator) + /// 11 = FLOAT (float) + /// 12 = DOUBLE (double) + /// + /// + public ushort TypeID { get; } + + /// + /// Gets the byte count or number of components. + /// + public uint Count { get; } + + /// + /// Gets the field value as an array of bytes. + /// + public byte[] Data { get; } + + /// + /// Returns the string representation of this instance. + /// + /// + public override string ToString() => string.Format("Tag: {0}, Type: {1}, Count: {2}, Data Length: {3}", TagID, TypeID, Count, Data.Length); } diff --git a/src/Umbraco.Core/Media/Exif/ExifProperty.cs b/src/Umbraco.Core/Media/Exif/ExifProperty.cs index a3c28aabbc..6d742bb3ba 100644 --- a/src/Umbraco.Core/Media/Exif/ExifProperty.cs +++ b/src/Umbraco.Core/Media/Exif/ExifProperty.cs @@ -1,578 +1,672 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the abstract base class for an Exif property. +/// +internal abstract class ExifProperty { - /// - /// Represents the abstract base class for an Exif property. - /// - internal abstract class ExifProperty + protected IFD mIFD; + protected string? mName; + protected ExifTag mTag; + + public ExifProperty(ExifTag tag) { - protected ExifTag mTag; - protected IFD mIFD; - protected string? mName; - - /// - /// Gets the Exif tag associated with this property. - /// - public ExifTag Tag { get { return mTag; } } - /// - /// Gets the IFD section containing this property. - /// - public IFD IFD { get { return mIFD; } } - /// - /// Gets or sets the name of this property. - /// - public string Name - { - get - { - if (string.IsNullOrEmpty(mName)) - return ExifTagFactory.GetTagName(mTag); - else - return mName; - } - set - { - mName = value; - } - } - protected abstract object _Value { get; set; } - /// - /// Gets or sets the value of this property. - /// - public object Value { get { return _Value; } set { _Value = value; } } - /// - /// Gets interoperability data for this property. - /// - public abstract ExifInterOperability Interoperability { get; } - - public ExifProperty(ExifTag tag) - { - mTag = tag; - mIFD = ExifTagFactory.GetTagIFD(tag); - } + mTag = tag; + mIFD = ExifTagFactory.GetTagIFD(tag); } /// - /// Represents an 8-bit unsigned integer. (EXIF Specification: BYTE) + /// Gets the Exif tag associated with this property. /// - internal class ExifByte : ExifProperty + public ExifTag Tag => mTag; + + /// + /// Gets the IFD section containing this property. + /// + public IFD IFD => mIFD; + + /// + /// Gets or sets the name of this property. + /// + public string Name { - protected byte mValue; - protected override object _Value { get { return Value; } set { Value = Convert.ToByte(value); } } - public new byte Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator byte(ExifByte obj) { return obj.mValue; } - - public override string ToString() { return mValue.ToString(); } - - public ExifByte(ExifTag tag, byte value) - : base(tag) + get { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get + if (string.IsNullOrEmpty(mName)) { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, 1, new byte[] { mValue }); + return ExifTagFactory.GetTagName(mTag); } + + return mName; } + set => mName = value; } /// - /// Represents an array of 8-bit unsigned integers. (EXIF Specification: BYTE with count > 1) + /// Gets or sets the value of this property. /// - internal class ExifByteArray : ExifProperty + public object Value { - protected byte[] mValue; - protected override object _Value { get { return Value; } set { Value = (byte[])value; } } - public new byte[] Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator byte[](ExifByteArray obj) { return obj.mValue; } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (byte b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifByteArray(ExifTag tag, byte[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.Length, mValue); - } - } + get => _Value; + set => _Value = value; } + protected abstract object _Value { get; set; } + /// - /// Represents an ASCII string. (EXIF Specification: ASCII) + /// Gets interoperability data for this property. /// - internal class ExifAscii : ExifProperty + public abstract ExifInterOperability Interoperability { get; } +} + +/// +/// Represents an 8-bit unsigned integer. (EXIF Specification: BYTE) +/// +internal class ExifByte : ExifProperty +{ + protected byte mValue; + + public ExifByte(ExifTag tag, byte value) + : base(tag) => + mValue = value; + + public new byte Value { - protected string mValue; - protected override object _Value { get { return Value; } set { Value = (string)value; } } - public new string Value { get { return mValue; } set { mValue = value; } } - - public Encoding Encoding { get; private set; } - - static public implicit operator string(ExifAscii obj) { return obj.mValue; } - - public override string ToString() { return mValue; } - - public ExifAscii(ExifTag tag, string value, Encoding encoding) - : base(tag) - { - mValue = value; - Encoding = encoding; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 2, (uint)mValue.Length + 1, ExifBitConverter.GetBytes(mValue, true, Encoding)); - } - } + get => mValue; + set => mValue = value; } - /// - /// Represents a 16-bit unsigned integer. (EXIF Specification: SHORT) - /// - internal class ExifUShort : ExifProperty + protected override object _Value { - protected ushort mValue; - protected override object _Value { get { return Value; } set { Value = Convert.ToUInt16(value); } } - public new ushort Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator ushort(ExifUShort obj) { return obj.mValue; } - - public override string ToString() { return mValue.ToString(); } - - public ExifUShort(ExifTag tag, ushort value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } + get => Value; + set => Value = Convert.ToByte(value); } - /// - /// Represents an array of 16-bit unsigned integers. - /// (EXIF Specification: SHORT with count > 1) - /// - internal class ExifUShortArray : ExifProperty + public override ExifInterOperability Interoperability => + new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, 1, new[] { mValue }); + + public static implicit operator byte(ExifByte obj) => obj.mValue; + + public override string ToString() => mValue.ToString(); +} + +/// +/// Represents an array of 8-bit unsigned integers. (EXIF Specification: BYTE with count > 1) +/// +internal class ExifByteArray : ExifProperty +{ + protected byte[] mValue; + + public ExifByteArray(ExifTag tag, byte[] value) + : base(tag) => + mValue = value; + + public new byte[] Value { - protected ushort[] mValue; - protected override object _Value { get { return Value; } set { Value = (ushort[])value; } } - public new ushort[] Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator ushort[](ExifUShortArray obj) { return obj.mValue; } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (ushort b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifUShortArray(ExifTag tag, ushort[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + get => mValue; + set => mValue = value; } - /// - /// Represents a 32-bit unsigned integer. (EXIF Specification: LONG) - /// - internal class ExifUInt : ExifProperty + protected override object _Value { - protected uint mValue; - protected override object _Value { get { return Value; } set { Value = Convert.ToUInt32(value); } } - public new uint Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator uint(ExifUInt obj) { return obj.mValue; } - - public override string ToString() { return mValue.ToString(); } - - public ExifUInt(ExifTag tag, uint value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 4, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } + get => Value; + set => Value = (byte[])value; } - /// - /// Represents an array of 16-bit unsigned integers. - /// (EXIF Specification: LONG with count > 1) - /// - internal class ExifUIntArray : ExifProperty + public override ExifInterOperability Interoperability => + new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.Length, mValue); + + public static implicit operator byte[](ExifByteArray obj) => obj.mValue; + + public override string ToString() { - protected uint[] mValue; - protected override object _Value { get { return Value; } set { Value = (uint[])value; } } - public new uint[] Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator uint[](ExifUIntArray obj) { return obj.mValue; } - - public override string ToString() + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (uint b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); + sb.Append(b); + sb.Append(' '); } - public ExifUIntArray(ExifTag tag, uint[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents a rational number defined with a 32-bit unsigned numerator - /// and denominator. (EXIF Specification: RATIONAL) - /// - internal class ExifURational : ExifProperty - { - protected MathEx.UFraction32 mValue; - protected override object _Value { get { return Value; } set { Value = (MathEx.UFraction32)value; } } - public new MathEx.UFraction32 Value { get { return mValue; } set { mValue = value; } } - - public override string ToString() { return mValue.ToString(); } - public float ToFloat() { return (float)mValue; } - - static public explicit operator float(ExifURational obj) { return (float)obj.mValue; } - - public uint[] ToArray() - { - return new uint[] { mValue.Numerator, mValue.Denominator }; - } - - public ExifURational(ExifTag tag, uint numerator, uint denominator) - : base(tag) - { - mValue = new MathEx.UFraction32(numerator, denominator); - } - - public ExifURational(ExifTag tag, MathEx.UFraction32 value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 5, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents an array of unsigned rational numbers. - /// (EXIF Specification: RATIONAL with count > 1) - /// - internal class ExifURationalArray : ExifProperty - { - protected MathEx.UFraction32[] mValue; - protected override object _Value { get { return Value; } set { Value = (MathEx.UFraction32[])value; } } - public new MathEx.UFraction32[] Value { get { return mValue; } set { mValue = value; } } - - static public explicit operator float[](ExifURationalArray obj) - { - float[] result = new float[obj.mValue.Length]; - for (int i = 0; i < obj.mValue.Length; i++) - result[i] = (float)obj.mValue[i]; - return result; - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (MathEx.UFraction32 b in mValue) - { - sb.Append(b.ToString()); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifURationalArray(ExifTag tag, MathEx.UFraction32[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 5, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents a byte array that can take any value. (EXIF Specification: UNDEFINED) - /// - internal class ExifUndefined : ExifProperty - { - protected byte[] mValue; - protected override object _Value { get { return Value; } set { Value = (byte[])value; } } - public new byte[] Value { get { return mValue; } set { mValue = value; } } - - static public implicit operator byte[](ExifUndefined obj) { return obj.mValue; } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (byte b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifUndefined(ExifTag tag, byte[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)mValue.Length, mValue); - } - } - } - - /// - /// Represents a 32-bit signed integer. (EXIF Specification: SLONG) - /// - internal class ExifSInt : ExifProperty - { - protected int mValue; - protected override object _Value { get { return Value; } set { Value = Convert.ToInt32(value); } } - public new int Value { get { return mValue; } set { mValue = value; } } - - public override string ToString() { return mValue.ToString(); } - - static public implicit operator int(ExifSInt obj) { return obj.mValue; } - - public ExifSInt(ExifTag tag, int value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 9, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents an array of 32-bit signed integers. - /// (EXIF Specification: SLONG with count > 1) - /// - internal class ExifSIntArray : ExifProperty - { - protected int[] mValue; - protected override object _Value { get { return Value; } set { Value = (int[])value; } } - public new int[] Value { get { return mValue; } set { mValue = value; } } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (int b in mValue) - { - sb.Append(b); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - static public implicit operator int[](ExifSIntArray obj) { return obj.mValue; } - - public ExifSIntArray(ExifTag tag, int[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 9, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents a rational number defined with a 32-bit signed numerator - /// and denominator. (EXIF Specification: SRATIONAL) - /// - internal class ExifSRational : ExifProperty - { - protected MathEx.Fraction32 mValue; - protected override object _Value { get { return Value; } set { Value = (MathEx.Fraction32)value; } } - public new MathEx.Fraction32 Value { get { return mValue; } set { mValue = value; } } - - public override string ToString() { return mValue.ToString(); } - public float ToFloat() { return (float)mValue; } - - static public explicit operator float(ExifSRational obj) { return (float)obj.mValue; } - - public int[] ToArray() - { - return new int[] { mValue.Numerator, mValue.Denominator }; - } - - public ExifSRational(ExifTag tag, int numerator, int denominator) - : base(tag) - { - mValue = new MathEx.Fraction32(numerator, denominator); - } - - public ExifSRational(ExifTag tag, MathEx.Fraction32 value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 10, 1, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } - } - - /// - /// Represents an array of signed rational numbers. - /// (EXIF Specification: SRATIONAL with count > 1) - /// - internal class ExifSRationalArray : ExifProperty - { - protected MathEx.Fraction32[] mValue; - protected override object _Value { get { return Value; } set { Value = (MathEx.Fraction32[])value; } } - public new MathEx.Fraction32[] Value { get { return mValue; } set { mValue = value; } } - - static public explicit operator float[](ExifSRationalArray obj) - { - float[] result = new float[obj.mValue.Length]; - for (int i = 0; i < obj.mValue.Length; i++) - result[i] = (float)obj.mValue[i]; - return result; - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - foreach (MathEx.Fraction32 b in mValue) - { - sb.Append(b.ToString()); - sb.Append(' '); - } - sb.Remove(sb.Length - 1, 1); - sb.Append(']'); - return sb.ToString(); - } - - public ExifSRationalArray(ExifTag tag, MathEx.Fraction32[] value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 10, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); - } - } + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents an ASCII string. (EXIF Specification: ASCII) +/// +internal class ExifAscii : ExifProperty +{ + protected string mValue; + + public ExifAscii(ExifTag tag, string value, Encoding encoding) + : base(tag) + { + mValue = value; + Encoding = encoding; + } + + public new string Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (string)value; + } + + public Encoding Encoding { get; } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 2, + (uint)mValue.Length + 1, + ExifBitConverter.GetBytes(mValue, true, Encoding)); + + public static implicit operator string(ExifAscii obj) => obj.mValue; + + public override string ToString() => mValue; +} + +/// +/// Represents a 16-bit unsigned integer. (EXIF Specification: SHORT) +/// +internal class ExifUShort : ExifProperty +{ + protected ushort mValue; + + public ExifUShort(ExifTag tag, ushort value) + : base(tag) => + mValue = value; + + public new ushort Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = Convert.ToUInt16(value); + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 3, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); + + public static implicit operator ushort(ExifUShort obj) => obj.mValue; + + public override string ToString() => mValue.ToString(); +} + +/// +/// Represents an array of 16-bit unsigned integers. +/// (EXIF Specification: SHORT with count > 1) +/// +internal class ExifUShortArray : ExifProperty +{ + protected ushort[] mValue; + + public ExifUShortArray(ExifTag tag, ushort[] value) + : base(tag) => + mValue = value; + + public new ushort[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (ushort[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 3, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static implicit operator ushort[](ExifUShortArray obj) => obj.mValue; + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) + { + sb.Append(b); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a 32-bit unsigned integer. (EXIF Specification: LONG) +/// +internal class ExifUInt : ExifProperty +{ + protected uint mValue; + + public ExifUInt(ExifTag tag, uint value) + : base(tag) => + mValue = value; + + public new uint Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = Convert.ToUInt32(value); + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 4, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); + + public static implicit operator uint(ExifUInt obj) => obj.mValue; + + public override string ToString() => mValue.ToString(); +} + +/// +/// Represents an array of 16-bit unsigned integers. +/// (EXIF Specification: LONG with count > 1) +/// +internal class ExifUIntArray : ExifProperty +{ + protected uint[] mValue; + + public ExifUIntArray(ExifTag tag, uint[] value) + : base(tag) => + mValue = value; + + public new uint[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (uint[])value; + } + + public override ExifInterOperability Interoperability => new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 3, (uint)mValue.Length, ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static implicit operator uint[](ExifUIntArray obj) => obj.mValue; + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) + { + sb.Append(b); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a rational number defined with a 32-bit unsigned numerator +/// and denominator. (EXIF Specification: RATIONAL) +/// +internal class ExifURational : ExifProperty +{ + protected MathEx.UFraction32 mValue; + + public ExifURational(ExifTag tag, uint numerator, uint denominator) + : base(tag) => + mValue = new MathEx.UFraction32(numerator, denominator); + + public ExifURational(ExifTag tag, MathEx.UFraction32 value) + : base(tag) => + mValue = value; + + public new MathEx.UFraction32 Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (MathEx.UFraction32)value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 5, + 1, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static explicit operator float(ExifURational obj) => (float)obj.mValue; + + public override string ToString() => mValue.ToString(); + + public float ToFloat() => (float)mValue; + + public uint[] ToArray() => new[] { mValue.Numerator, mValue.Denominator }; +} + +/// +/// Represents an array of unsigned rational numbers. +/// (EXIF Specification: RATIONAL with count > 1) +/// +internal class ExifURationalArray : ExifProperty +{ + protected MathEx.UFraction32[] mValue; + + public ExifURationalArray(ExifTag tag, MathEx.UFraction32[] value) + : base(tag) => + mValue = value; + + public new MathEx.UFraction32[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (MathEx.UFraction32[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 5, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static explicit operator float[](ExifURationalArray obj) + { + var result = new float[obj.mValue.Length]; + for (var i = 0; i < obj.mValue.Length; i++) + { + result[i] = (float)obj.mValue[i]; + } + + return result; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (MathEx.UFraction32 b in mValue) + { + sb.Append(b.ToString()); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a byte array that can take any value. (EXIF Specification: UNDEFINED) +/// +internal class ExifUndefined : ExifProperty +{ + protected byte[] mValue; + + public ExifUndefined(ExifTag tag, byte[] value) + : base(tag) => + mValue = value; + + public new byte[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (byte[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 7, (uint)mValue.Length, mValue); + + public static implicit operator byte[](ExifUndefined obj) => obj.mValue; + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) + { + sb.Append(b); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a 32-bit signed integer. (EXIF Specification: SLONG) +/// +internal class ExifSInt : ExifProperty +{ + protected int mValue; + + public ExifSInt(ExifTag tag, int value) + : base(tag) => + mValue = value; + + public new int Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = Convert.ToInt32(value); + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 9, + 1, + BitConverterEx.GetBytes(mValue, BitConverterEx.SystemByteOrder, BitConverterEx.SystemByteOrder)); + + public static implicit operator int(ExifSInt obj) => obj.mValue; + + public override string ToString() => mValue.ToString(); +} + +/// +/// Represents an array of 32-bit signed integers. +/// (EXIF Specification: SLONG with count > 1) +/// +internal class ExifSIntArray : ExifProperty +{ + protected int[] mValue; + + public ExifSIntArray(ExifTag tag, int[] value) + : base(tag) => + mValue = value; + + public new int[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (int[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 9, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static implicit operator int[](ExifSIntArray obj) => obj.mValue; + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (var b in mValue) + { + sb.Append(b); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); + } +} + +/// +/// Represents a rational number defined with a 32-bit signed numerator +/// and denominator. (EXIF Specification: SRATIONAL) +/// +internal class ExifSRational : ExifProperty +{ + protected MathEx.Fraction32 mValue; + + public ExifSRational(ExifTag tag, int numerator, int denominator) + : base(tag) => + mValue = new MathEx.Fraction32(numerator, denominator); + + public ExifSRational(ExifTag tag, MathEx.Fraction32 value) + : base(tag) => + mValue = value; + + public new MathEx.Fraction32 Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (MathEx.Fraction32)value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 10, + 1, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static explicit operator float(ExifSRational obj) => (float)obj.mValue; + + public override string ToString() => mValue.ToString(); + + public float ToFloat() => (float)mValue; + + public int[] ToArray() => new[] { mValue.Numerator, mValue.Denominator }; +} + +/// +/// Represents an array of signed rational numbers. +/// (EXIF Specification: SRATIONAL with count > 1) +/// +internal class ExifSRationalArray : ExifProperty +{ + protected MathEx.Fraction32[] mValue; + + public ExifSRationalArray(ExifTag tag, MathEx.Fraction32[] value) + : base(tag) => + mValue = value; + + public new MathEx.Fraction32[] Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (MathEx.Fraction32[])value; + } + + public override ExifInterOperability Interoperability => + new ExifInterOperability( + ExifTagFactory.GetTagID(mTag), + 10, + (uint)mValue.Length, + ExifBitConverter.GetBytes(mValue, BitConverterEx.SystemByteOrder)); + + public static explicit operator float[](ExifSRationalArray obj) + { + var result = new float[obj.mValue.Length]; + for (var i = 0; i < obj.mValue.Length; i++) + { + result[i] = (float)obj.mValue[i]; + } + + return result; + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + foreach (MathEx.Fraction32 b in mValue) + { + sb.Append(b.ToString()); + sb.Append(' '); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(']'); + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs b/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs index 7114b2eb14..f77f0c89cd 100644 --- a/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs +++ b/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs @@ -1,384 +1,493 @@ -using System; -using System.Collections.Generic; +using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents a collection of objects. +/// +internal class ExifPropertyCollection : IDictionary { - /// - /// Represents a collection of objects. - /// - internal class ExifPropertyCollection : IDictionary + #region Constructor + + internal ExifPropertyCollection(ImageFile parentFile) { - #region Member Variables - private ImageFile parent; - private Dictionary items; - #endregion - - #region Constructor - internal ExifPropertyCollection (ImageFile parentFile) - { - parent = parentFile; - items = new Dictionary (); - } - #endregion - - #region Properties - /// - /// Gets the number of elements contained in the collection. - /// - public int Count { - get { return items.Count; } - } - /// - /// Gets a collection containing the keys in this collection. - /// - public ICollection Keys { - get { return items.Keys; } - } - /// - /// Gets a collection containing the values in this collection. - /// - public ICollection Values { - get { return items.Values; } - } - /// - /// Gets or sets the with the specified key. - /// - public ExifProperty this[ExifTag key] { - get { return items[key]; } - set { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, value); - } - } - #endregion - - #region ExifProperty Collection Setters - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, byte value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifByte (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, string value) - { - if (items.ContainsKey (key)) - items.Remove (key); - if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) { - items.Add (key, new WindowsByteString (key, value)); - } else { - items.Add (key, new ExifAscii (key, value, parent.Encoding)); - } - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, ushort value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifUShort (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, int value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifSInt (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, uint value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifUInt (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, float value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifURational (key, new MathEx.UFraction32 (value))); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, double value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifURational (key, new MathEx.UFraction32 (value))); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, object value) - { - Type type = value.GetType (); - if (type.IsEnum) { - Type etype = typeof(ExifEnumProperty<>).MakeGenericType (new Type[] { type }); - object? prop = Activator.CreateInstance (etype, new object[] { key, value }); - if (items.ContainsKey (key)) - items.Remove (key); - if (prop is ExifProperty exifProperty) - { - items.Add (key, exifProperty); - } - } else - throw new ArgumentException ("No exif property exists for this tag.", "value"); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - /// String encoding. - public void Set (ExifTag key, string value, Encoding encoding) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifEncodedString (key, value, encoding)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// The value of tag. - public void Set (ExifTag key, DateTime value) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifDateTime (key, value)); - } - /// - /// Sets the with the specified key. - /// - /// The tag to set. - /// Angular degrees (or clock hours for a timestamp). - /// Angular minutes (or clock minutes for a timestamp). - /// Angular seconds (or clock seconds for a timestamp). - public void Set (ExifTag key, float d, float m, float s) - { - if (items.ContainsKey (key)) - items.Remove (key); - items.Add (key, new ExifURationalArray (key, new MathEx.UFraction32[] { new MathEx.UFraction32 (d), new MathEx.UFraction32 (m), new MathEx.UFraction32 (s) })); - } - #endregion - - #region Instance Methods - /// - /// Adds the specified item to the collection. - /// - /// The to add to the collection. - public void Add (ExifProperty item) - { - ExifProperty? oldItem = null; - if (items.TryGetValue (item.Tag, out oldItem)) - items[item.Tag] = item; - else - items.Add (item.Tag, item); - } - /// - /// Removes all items from the collection. - /// - public void Clear () - { - items.Clear (); - } - /// - /// Determines whether the collection contains an element with the specified key. - /// - /// The key to locate in the collection. - /// - /// true if the collection contains an element with the key; otherwise, false. - /// - /// - /// is null. - public bool ContainsKey (ExifTag key) - { - return items.ContainsKey (key); - } - /// - /// Removes the element with the specified key from the collection. - /// - /// The key of the element to remove. - /// - /// true if the element is successfully removed; otherwise, false. This method also returns false if was not found in the original collection. - /// - /// - /// is null. - public bool Remove (ExifTag key) - { - return items.Remove (key); - } - /// - /// Removes all items with the given IFD from the collection. - /// - /// The IFD section to remove. - public void Remove (IFD ifd) - { - List toRemove = new List (); - foreach (KeyValuePair item in items) { - if (item.Value.IFD == ifd) - toRemove.Add (item.Key); - } - foreach (ExifTag tag in toRemove) - items.Remove (tag); - } - /// - /// Gets the value associated with the specified key. - /// - /// The key whose value to get. - /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. - /// - /// true if the collection contains an element with the specified key; otherwise, false. - /// - /// - /// is null. - public bool TryGetValue (ExifTag key, [MaybeNullWhen(false)] out ExifProperty value) - { - return items.TryGetValue (key, out value); - } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator () - { - return Values.GetEnumerator (); - } - #endregion - - #region Hidden Interface - /// - /// Adds an element with the provided key and value to the . - /// - /// The object to use as the key of the element to add. - /// The object to use as the value of the element to add. - /// - /// is null. - /// An element with the same key already exists in the . - /// The is read-only. - void IDictionary.Add (ExifTag key, ExifProperty value) - { - Add (value); - } - /// - /// Adds an item to the . - /// - /// The object to add to the . - /// The is read-only. - void ICollection>.Add (KeyValuePair item) - { - Add (item.Value); - } - bool ICollection>.Contains (KeyValuePair item) - { - throw new NotSupportedException (); - } - /// - /// Copies the elements of the to an , starting at a particular index. - /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. - /// The zero-based index in at which copying begins. - /// - /// is null. - /// - /// is less than 0. - /// - /// is multidimensional.-or- is equal to or greater than the length of .-or-The number of elements in the source is greater than the available space from to the end of the destination .-or-Type cannot be cast automatically to the type of the destination . - void ICollection>.CopyTo (KeyValuePair[] array, int arrayIndex) - { - if (array == null) - throw new ArgumentNullException ("array"); - if (arrayIndex < 0) - throw new ArgumentOutOfRangeException ("arrayIndex"); - if (array.Rank > 1) - throw new ArgumentException ("Destination array is multidimensional.", "array"); - if (arrayIndex >= array.Length) - throw new ArgumentException ("arrayIndex is equal to or greater than the length of destination array", "array"); - if (arrayIndex + items.Count > array.Length) - throw new ArgumentException ("There is not enough space in destination array.", "array"); - - int i = 0; - foreach (KeyValuePair item in items) { - if (i >= arrayIndex) { - array[i] = item; - } - i++; - } - } - /// - /// Gets a value indicating whether the is read-only. - /// - /// true if the is read-only; otherwise, false. - bool ICollection>.IsReadOnly { - get { return false; } - } - /// - /// Removes the first occurrence of a specific object from the . - /// - /// The object to remove from the . - /// - /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . - /// - /// The is read-only. - bool ICollection>.Remove (KeyValuePair item) - { - throw new NotSupportedException (); - } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () - { - return GetEnumerator (); - } - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// A that can be used to iterate through the collection. - /// - IEnumerator> IEnumerable>.GetEnumerator () - { - return items.GetEnumerator (); - } - #endregion + parent = parentFile; + items = new Dictionary(); } + + #endregion + + #region Member Variables + + private readonly ImageFile parent; + private readonly Dictionary items; + + #endregion + + #region Properties + + /// + /// Gets the number of elements contained in the collection. + /// + public int Count => items.Count; + + /// + /// Gets a collection containing the keys in this collection. + /// + public ICollection Keys => items.Keys; + + /// + /// Gets a collection containing the values in this collection. + /// + public ICollection Values => items.Values; + + /// + /// Gets or sets the with the specified key. + /// + public ExifProperty this[ExifTag key] + { + get => items[key]; + set + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, value); + } + } + + #endregion + + #region ExifProperty Collection Setters + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, byte value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifByte(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, string value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || + key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) + { + items.Add(key, new WindowsByteString(key, value)); + } + else + { + items.Add(key, new ExifAscii(key, value, parent.Encoding)); + } + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, ushort value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifUShort(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, int value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifSInt(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, uint value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifUInt(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, float value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifURational(key, new MathEx.UFraction32(value))); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, double value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifURational(key, new MathEx.UFraction32(value))); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, object value) + { + Type type = value.GetType(); + if (type.IsEnum) + { + Type etype = typeof(ExifEnumProperty<>).MakeGenericType(type); + var prop = Activator.CreateInstance(etype, key, value); + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + if (prop is ExifProperty exifProperty) + { + items.Add(key, exifProperty); + } + } + else + { + throw new ArgumentException("No exif property exists for this tag.", "value"); + } + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + /// String encoding. + public void Set(ExifTag key, string value, Encoding encoding) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifEncodedString(key, value, encoding)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// The value of tag. + public void Set(ExifTag key, DateTime value) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifDateTime(key, value)); + } + + /// + /// Sets the with the specified key. + /// + /// The tag to set. + /// Angular degrees (or clock hours for a timestamp). + /// Angular minutes (or clock minutes for a timestamp). + /// Angular seconds (or clock seconds for a timestamp). + public void Set(ExifTag key, float d, float m, float s) + { + if (items.ContainsKey(key)) + { + items.Remove(key); + } + + items.Add(key, new ExifURationalArray(key, new[] { new(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) })); + } + + #endregion + + #region Instance Methods + + /// + /// Adds the specified item to the collection. + /// + /// The to add to the collection. + public void Add(ExifProperty item) + { + ExifProperty? oldItem = null; + if (items.TryGetValue(item.Tag, out oldItem)) + { + items[item.Tag] = item; + } + else + { + items.Add(item.Tag, item); + } + } + + /// + /// Removes all items from the collection. + /// + public void Clear() => items.Clear(); + + /// + /// Determines whether the collection contains an element with the specified key. + /// + /// The key to locate in the collection. + /// + /// true if the collection contains an element with the key; otherwise, false. + /// + /// + /// is null. + /// + public bool ContainsKey(ExifTag key) => items.ContainsKey(key); + + /// + /// Removes the element with the specified key from the collection. + /// + /// The key of the element to remove. + /// + /// true if the element is successfully removed; otherwise, false. This method also returns false if + /// was not found in the original collection. + /// + /// + /// is null. + /// + public bool Remove(ExifTag key) => items.Remove(key); + + /// + /// Removes all items with the given IFD from the collection. + /// + /// The IFD section to remove. + public void Remove(IFD ifd) + { + var toRemove = new List(); + foreach (KeyValuePair item in items) + { + if (item.Value.IFD == ifd) + { + toRemove.Add(item.Key); + } + } + + foreach (ExifTag tag in toRemove) + { + items.Remove(tag); + } + } + + /// + /// Gets the value associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, the value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the parameter. This parameter is passed + /// uninitialized. + /// + /// + /// true if the collection contains an element with the specified key; otherwise, false. + /// + /// + /// is null. + /// + public bool TryGetValue(ExifTag key, [MaybeNullWhen(false)] out ExifProperty value) => + items.TryGetValue(key, out value); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() => Values.GetEnumerator(); + + #endregion + + #region Hidden Interface + + /// + /// Adds an element with the provided key and value to the . + /// + /// The object to use as the key of the element to add. + /// The object to use as the value of the element to add. + /// + /// is null. + /// + /// + /// An element with the same key already exists in the + /// . + /// + /// + /// The is + /// read-only. + /// + void IDictionary.Add(ExifTag key, ExifProperty value) => Add(value); + + /// + /// Adds an item to the . + /// + /// The object to add to the . + /// + /// The is + /// read-only. + /// + void ICollection>.Add(KeyValuePair item) => + Add(item.Value); + + bool ICollection>.Contains(KeyValuePair item) => + throw new NotSupportedException(); + + /// + /// Copies the elements of the to an + /// , starting at a particular index. + /// + /// + /// The one-dimensional that is the destination of the elements copied + /// from . The must have + /// zero-based indexing. + /// + /// The zero-based index in at which copying begins. + /// + /// is null. + /// + /// + /// is less than 0. + /// + /// + /// is multidimensional.-or- is equal to or greater than the + /// length of .-or-The number of elements in the source + /// is greater than the available space from + /// to the end of the destination .-or-Type + /// cannot be cast automatically to the type of the destination . + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException("array"); + } + + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException("arrayIndex"); + } + + if (array.Rank > 1) + { + throw new ArgumentException("Destination array is multidimensional.", "array"); + } + + if (arrayIndex >= array.Length) + { + throw new ArgumentException("arrayIndex is equal to or greater than the length of destination array", "array"); + } + + if (arrayIndex + items.Count > array.Length) + { + throw new ArgumentException("There is not enough space in destination array.", "array"); + } + + var i = 0; + foreach (KeyValuePair item in items) + { + if (i >= arrayIndex) + { + array[i] = item; + } + + i++; + } + } + + /// + /// Gets a value indicating whether the is read-only. + /// + /// true if the is read-only; otherwise, false. + bool ICollection>.IsReadOnly => false; + + /// + /// Removes the first occurrence of a specific object from the + /// . + /// + /// The object to remove from the . + /// + /// true if was successfully removed from the + /// ; otherwise, false. This method also returns false if + /// is not found in the original . + /// + /// + /// The is + /// read-only. + /// + bool ICollection>.Remove(KeyValuePair item) => + throw new NotSupportedException(); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + IEnumerator> IEnumerable>.GetEnumerator() => + items.GetEnumerator(); + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs b/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs index f47cab1c35..4290dcaf7c 100644 --- a/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs +++ b/src/Umbraco.Core/Media/Exif/ExifPropertyFactory.cs @@ -1,253 +1,598 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif -{ - /// - /// Creates exif properties from interoperability parameters. - /// - internal static class ExifPropertyFactory - { - #region Static Methods - /// - /// Creates an ExifProperty from the given interoperability parameters. - /// - /// The tag id of the exif property. - /// The type id of the exif property. - /// Byte or component count. - /// Field data as an array of bytes. - /// Byte order of value. - /// IFD section containing this property. - /// The encoding to be used for text metadata when the source encoding is unknown. - /// an ExifProperty initialized from the interoperability parameters. - public static ExifProperty Get(ushort tag, ushort type, uint count, byte[] value, BitConverterEx.ByteOrder byteOrder, IFD ifd, Encoding encoding) - { - BitConverterEx conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); - // Find the exif tag corresponding to given tag id - ExifTag etag = ExifTagFactory.GetExifTag(ifd, tag); +namespace Umbraco.Cms.Core.Media.Exif; - if (ifd == IFD.Zeroth) +/// +/// Creates exif properties from interoperability parameters. +/// +internal static class ExifPropertyFactory +{ + #region Static Methods + + /// + /// Creates an ExifProperty from the given interoperability parameters. + /// + /// The tag id of the exif property. + /// The type id of the exif property. + /// Byte or component count. + /// Field data as an array of bytes. + /// Byte order of value. + /// IFD section containing this property. + /// The encoding to be used for text metadata when the source encoding is unknown. + /// an ExifProperty initialized from the interoperability parameters. + public static ExifProperty Get(ushort tag, ushort type, uint count, byte[] value, BitConverterEx.ByteOrder byteOrder, IFD ifd, Encoding encoding) + { + var conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); + + // Find the exif tag corresponding to given tag id + ExifTag etag = ExifTagFactory.GetExifTag(ifd, tag); + + if (ifd == IFD.Zeroth) + { + // Compression + if (tag == 0x103) { - if (tag == 0x103) // Compression - return new ExifEnumProperty(ExifTag.Compression, (Compression)conv.ToUInt16(value, 0)); - else if (tag == 0x106) // PhotometricInterpretation - return new ExifEnumProperty(ExifTag.PhotometricInterpretation, (PhotometricInterpretation)conv.ToUInt16(value, 0)); - else if (tag == 0x112) // Orientation - return new ExifEnumProperty(ExifTag.Orientation, (Orientation)conv.ToUInt16(value, 0)); - else if (tag == 0x11c) // PlanarConfiguration - return new ExifEnumProperty(ExifTag.PlanarConfiguration, (PlanarConfiguration)conv.ToUInt16(value, 0)); - else if (tag == 0x213) // YCbCrPositioning - return new ExifEnumProperty(ExifTag.YCbCrPositioning, (YCbCrPositioning)conv.ToUInt16(value, 0)); - else if (tag == 0x128) // ResolutionUnit - return new ExifEnumProperty(ExifTag.ResolutionUnit, (ResolutionUnit)conv.ToUInt16(value, 0)); - else if (tag == 0x132) // DateTime - return new ExifDateTime(ExifTag.DateTime, ExifBitConverter.ToDateTime(value)); - else if (tag == 0x9c9b || tag == 0x9c9c || // Windows tags - tag == 0x9c9d || tag == 0x9c9e || tag == 0x9c9f) - return new WindowsByteString(etag, Encoding.Unicode.GetString(value).TrimEnd(Constants.CharArrays.NullTerminator)); + return new ExifEnumProperty(ExifTag.Compression, (Compression)conv.ToUInt16(value, 0)); } - else if (ifd == IFD.EXIF) + + // PhotometricInterpretation + if (tag == 0x106) { - if (tag == 0x9000) // ExifVersion - return new ExifVersion(ExifTag.ExifVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); - else if (tag == 0xa000) // FlashpixVersion - return new ExifVersion(ExifTag.FlashpixVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); - else if (tag == 0xa001) // ColorSpace - return new ExifEnumProperty(ExifTag.ColorSpace, (ColorSpace)conv.ToUInt16(value, 0)); - else if (tag == 0x9286) // UserComment + return new ExifEnumProperty( + ExifTag.PhotometricInterpretation, + (PhotometricInterpretation)conv.ToUInt16(value, 0)); + } + + // Orientation + if (tag == 0x112) + { + return new ExifEnumProperty(ExifTag.Orientation, (Orientation)conv.ToUInt16(value, 0)); + } + + // PlanarConfiguration + if (tag == 0x11c) + { + return new ExifEnumProperty( + ExifTag.PlanarConfiguration, + (PlanarConfiguration)conv.ToUInt16(value, 0)); + } + + // YCbCrPositioning + if (tag == 0x213) + { + return new ExifEnumProperty( + ExifTag.YCbCrPositioning, + (YCbCrPositioning)conv.ToUInt16(value, 0)); + } + + // ResolutionUnit + if (tag == 0x128) + { + return new ExifEnumProperty( + ExifTag.ResolutionUnit, + (ResolutionUnit)conv.ToUInt16(value, 0)); + } + + // DateTime + if (tag == 0x132) + { + return new ExifDateTime(ExifTag.DateTime, ExifBitConverter.ToDateTime(value)); + } + + if (tag == 0x9c9b || tag == 0x9c9c || // Windows tags + tag == 0x9c9d || tag == 0x9c9e || tag == 0x9c9f) + { + return new WindowsByteString( + etag, + Encoding.Unicode.GetString(value).TrimEnd(Constants.CharArrays.NullTerminator)); + } + } + else if (ifd == IFD.EXIF) + { + // ExifVersion + if (tag == 0x9000) + { + return new ExifVersion(ExifTag.ExifVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); + } + + // FlashpixVersion + if (tag == 0xa000) + { + return new ExifVersion(ExifTag.FlashpixVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); + } + + // ColorSpace + if (tag == 0xa001) + { + return new ExifEnumProperty(ExifTag.ColorSpace, (ColorSpace)conv.ToUInt16(value, 0)); + } + + // UserComment + if (tag == 0x9286) + { + // Default to ASCII + Encoding enc = Encoding.ASCII; + bool hasenc; + if (value.Length < 8) { - // Default to ASCII - Encoding enc = Encoding.ASCII; - bool hasenc; - if (value.Length < 8) - hasenc = false; + hasenc = false; + } + else + { + hasenc = true; + var encstr = enc.GetString(value, 0, 8); + if (string.Compare(encstr, "ASCII\0\0\0", StringComparison.OrdinalIgnoreCase) == 0) + { + enc = Encoding.ASCII; + } + else if (string.Compare(encstr, "JIS\0\0\0\0\0", StringComparison.OrdinalIgnoreCase) == 0) + { + enc = Encoding.GetEncoding("Japanese (JIS 0208-1990 and 0212-1990)"); + } + else if (string.Compare(encstr, "Unicode\0", StringComparison.OrdinalIgnoreCase) == 0) + { + enc = Encoding.Unicode; + } else { - hasenc = true; - string encstr = enc.GetString(value, 0, 8); - if (string.Compare(encstr, "ASCII\0\0\0", StringComparison.OrdinalIgnoreCase) == 0) - enc = Encoding.ASCII; - else if (string.Compare(encstr, "JIS\0\0\0\0\0", StringComparison.OrdinalIgnoreCase) == 0) - enc = Encoding.GetEncoding("Japanese (JIS 0208-1990 and 0212-1990)"); - else if (string.Compare(encstr, "Unicode\0", StringComparison.OrdinalIgnoreCase) == 0) - enc = Encoding.Unicode; - else - hasenc = false; + hasenc = false; } - - string val = (hasenc ? enc.GetString(value, 8, value.Length - 8) : enc.GetString(value)).Trim(Constants.CharArrays.NullTerminator); - - return new ExifEncodedString(ExifTag.UserComment, val, enc); } - else if (tag == 0x9003) // DateTimeOriginal - return new ExifDateTime(ExifTag.DateTimeOriginal, ExifBitConverter.ToDateTime(value)); - else if (tag == 0x9004) // DateTimeDigitized - return new ExifDateTime(ExifTag.DateTimeDigitized, ExifBitConverter.ToDateTime(value)); - else if (tag == 0x8822) // ExposureProgram - return new ExifEnumProperty(ExifTag.ExposureProgram, (ExposureProgram)conv.ToUInt16(value, 0)); - else if (tag == 0x9207) // MeteringMode - return new ExifEnumProperty(ExifTag.MeteringMode, (MeteringMode)conv.ToUInt16(value, 0)); - else if (tag == 0x9208) // LightSource - return new ExifEnumProperty(ExifTag.LightSource, (LightSource)conv.ToUInt16(value, 0)); - else if (tag == 0x9209) // Flash - return new ExifEnumProperty(ExifTag.Flash, (Flash)conv.ToUInt16(value, 0), true); - else if (tag == 0x9214) // SubjectArea + + var val = (hasenc ? enc.GetString(value, 8, value.Length - 8) : enc.GetString(value)).Trim( + Constants.CharArrays.NullTerminator); + + return new ExifEncodedString(ExifTag.UserComment, val, enc); + } + + // DateTimeOriginal + if (tag == 0x9003) + { + return new ExifDateTime(ExifTag.DateTimeOriginal, ExifBitConverter.ToDateTime(value)); + } + + // DateTimeDigitized + if (tag == 0x9004) + { + return new ExifDateTime(ExifTag.DateTimeDigitized, ExifBitConverter.ToDateTime(value)); + } + + // ExposureProgram + if (tag == 0x8822) + { + return new ExifEnumProperty( + ExifTag.ExposureProgram, + (ExposureProgram)conv.ToUInt16(value, 0)); + } + + // MeteringMode + if (tag == 0x9207) + { + return new ExifEnumProperty(ExifTag.MeteringMode, (MeteringMode)conv.ToUInt16(value, 0)); + } + + // LightSource + if (tag == 0x9208) + { + return new ExifEnumProperty(ExifTag.LightSource, (LightSource)conv.ToUInt16(value, 0)); + } + + // Flash + if (tag == 0x9209) + { + return new ExifEnumProperty(ExifTag.Flash, (Flash)conv.ToUInt16(value, 0), true); + } + + // SubjectArea + if (tag == 0x9214) + { + if (count == 3) { - if (count == 3) - return new ExifCircularSubjectArea(ExifTag.SubjectArea, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); - else if (count == 4) - return new ExifRectangularSubjectArea(ExifTag.SubjectArea, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); - else // count == 2 - return new ExifPointSubjectArea(ExifTag.SubjectArea, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + return new ExifCircularSubjectArea( + ExifTag.SubjectArea, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - else if (tag == 0xa210) // FocalPlaneResolutionUnit - return new ExifEnumProperty(ExifTag.FocalPlaneResolutionUnit, (ResolutionUnit)conv.ToUInt16(value, 0), true); - else if (tag == 0xa214) // SubjectLocation - return new ExifPointSubjectArea(ExifTag.SubjectLocation, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); - else if (tag == 0xa217) // SensingMethod - return new ExifEnumProperty(ExifTag.SensingMethod, (SensingMethod)conv.ToUInt16(value, 0), true); - else if (tag == 0xa300) // FileSource - return new ExifEnumProperty(ExifTag.FileSource, (FileSource)conv.ToUInt16(value, 0), true); - else if (tag == 0xa301) // SceneType - return new ExifEnumProperty(ExifTag.SceneType, (SceneType)conv.ToUInt16(value, 0), true); - else if (tag == 0xa401) // CustomRendered - return new ExifEnumProperty(ExifTag.CustomRendered, (CustomRendered)conv.ToUInt16(value, 0), true); - else if (tag == 0xa402) // ExposureMode - return new ExifEnumProperty(ExifTag.ExposureMode, (ExposureMode)conv.ToUInt16(value, 0), true); - else if (tag == 0xa403) // WhiteBalance - return new ExifEnumProperty(ExifTag.WhiteBalance, (WhiteBalance)conv.ToUInt16(value, 0), true); - else if (tag == 0xa406) // SceneCaptureType - return new ExifEnumProperty(ExifTag.SceneCaptureType, (SceneCaptureType)conv.ToUInt16(value, 0), true); - else if (tag == 0xa407) // GainControl - return new ExifEnumProperty(ExifTag.GainControl, (GainControl)conv.ToUInt16(value, 0), true); - else if (tag == 0xa408) // Contrast - return new ExifEnumProperty(ExifTag.Contrast, (Contrast)conv.ToUInt16(value, 0), true); - else if (tag == 0xa409) // Saturation - return new ExifEnumProperty(ExifTag.Saturation, (Saturation)conv.ToUInt16(value, 0), true); - else if (tag == 0xa40a) // Sharpness - return new ExifEnumProperty(ExifTag.Sharpness, (Sharpness)conv.ToUInt16(value, 0), true); - else if (tag == 0xa40c) // SubjectDistanceRange - return new ExifEnumProperty(ExifTag.SubjectDistanceRange, (SubjectDistanceRange)conv.ToUInt16(value, 0), true); - } - else if (ifd == IFD.GPS) - { - if (tag == 0) // GPSVersionID - return new ExifVersion(ExifTag.GPSVersionID, ExifBitConverter.ToString(value)); - else if (tag == 1) // GPSLatitudeRef - return new ExifEnumProperty(ExifTag.GPSLatitudeRef, (GPSLatitudeRef)value[0]); - else if (tag == 2) // GPSLatitude - return new GPSLatitudeLongitude(ExifTag.GPSLatitude, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 3) // GPSLongitudeRef - return new ExifEnumProperty(ExifTag.GPSLongitudeRef, (GPSLongitudeRef)value[0]); - else if (tag == 4) // GPSLongitude - return new GPSLatitudeLongitude(ExifTag.GPSLongitude, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 5) // GPSAltitudeRef - return new ExifEnumProperty(ExifTag.GPSAltitudeRef, (GPSAltitudeRef)value[0]); - else if (tag == 7) // GPSTimeStamp - return new GPSTimeStamp(ExifTag.GPSTimeStamp, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 9) // GPSStatus - return new ExifEnumProperty(ExifTag.GPSStatus, (GPSStatus)value[0]); - else if (tag == 10) // GPSMeasureMode - return new ExifEnumProperty(ExifTag.GPSMeasureMode, (GPSMeasureMode)value[0]); - else if (tag == 12) // GPSSpeedRef - return new ExifEnumProperty(ExifTag.GPSSpeedRef, (GPSSpeedRef)value[0]); - else if (tag == 14) // GPSTrackRef - return new ExifEnumProperty(ExifTag.GPSTrackRef, (GPSDirectionRef)value[0]); - else if (tag == 16) // GPSImgDirectionRef - return new ExifEnumProperty(ExifTag.GPSImgDirectionRef, (GPSDirectionRef)value[0]); - else if (tag == 19) // GPSDestLatitudeRef - return new ExifEnumProperty(ExifTag.GPSDestLatitudeRef, (GPSLatitudeRef)value[0]); - else if (tag == 20) // GPSDestLatitude - return new GPSLatitudeLongitude(ExifTag.GPSDestLatitude, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 21) // GPSDestLongitudeRef - return new ExifEnumProperty(ExifTag.GPSDestLongitudeRef, (GPSLongitudeRef)value[0]); - else if (tag == 22) // GPSDestLongitude - return new GPSLatitudeLongitude(ExifTag.GPSDestLongitude, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); - else if (tag == 23) // GPSDestBearingRef - return new ExifEnumProperty(ExifTag.GPSDestBearingRef, (GPSDirectionRef)value[0]); - else if (tag == 25) // GPSDestDistanceRef - return new ExifEnumProperty(ExifTag.GPSDestDistanceRef, (GPSDistanceRef)value[0]); - else if (tag == 29) // GPSDate - return new ExifDateTime(ExifTag.GPSDateStamp, ExifBitConverter.ToDateTime(value, false)); - else if (tag == 30) // GPSDifferential - return new ExifEnumProperty(ExifTag.GPSDifferential, (GPSDifferential)conv.ToUInt16(value, 0)); - } - else if (ifd == IFD.Interop) - { - if (tag == 1) // InteroperabilityIndex - return new ExifAscii(ExifTag.InteroperabilityIndex, ExifBitConverter.ToAscii(value, Encoding.ASCII), Encoding.ASCII); - else if (tag == 2) // InteroperabilityVersion - return new ExifVersion(ExifTag.InteroperabilityVersion, ExifBitConverter.ToAscii(value, Encoding.ASCII)); - } - else if (ifd == IFD.First) - { - if (tag == 0x103) // Compression - return new ExifEnumProperty(ExifTag.ThumbnailCompression, (Compression)conv.ToUInt16(value, 0)); - else if (tag == 0x106) // PhotometricInterpretation - return new ExifEnumProperty(ExifTag.ThumbnailPhotometricInterpretation, (PhotometricInterpretation)conv.ToUInt16(value, 0)); - else if (tag == 0x112) // Orientation - return new ExifEnumProperty(ExifTag.ThumbnailOrientation, (Orientation)conv.ToUInt16(value, 0)); - else if (tag == 0x11c) // PlanarConfiguration - return new ExifEnumProperty(ExifTag.ThumbnailPlanarConfiguration, (PlanarConfiguration)conv.ToUInt16(value, 0)); - else if (tag == 0x213) // YCbCrPositioning - return new ExifEnumProperty(ExifTag.ThumbnailYCbCrPositioning, (YCbCrPositioning)conv.ToUInt16(value, 0)); - else if (tag == 0x128) // ResolutionUnit - return new ExifEnumProperty(ExifTag.ThumbnailResolutionUnit, (ResolutionUnit)conv.ToUInt16(value, 0)); - else if (tag == 0x132) // DateTime - return new ExifDateTime(ExifTag.ThumbnailDateTime, ExifBitConverter.ToDateTime(value)); + + if (count == 4) + { + return new ExifRectangularSubjectArea( + ExifTag.SubjectArea, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + } + + return new ExifPointSubjectArea( + ExifTag.SubjectArea, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - if (type == 1) // 1 = BYTE An 8-bit unsigned integer. + // FocalPlaneResolutionUnit + if (tag == 0xa210) { - if (count == 1) - return new ExifByte(etag, value[0]); - else - return new ExifByteArray(etag, value); + return new ExifEnumProperty( + ExifTag.FocalPlaneResolutionUnit, + (ResolutionUnit)conv.ToUInt16(value, 0), + true); } - else if (type == 2) // 2 = ASCII An 8-bit byte containing one 7-bit ASCII code. + + // SubjectLocation + if (tag == 0xa214) { - return new ExifAscii(etag, ExifBitConverter.ToAscii(value, encoding), encoding); + return new ExifPointSubjectArea( + ExifTag.SubjectLocation, + ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); } - else if (type == 3) // 3 = SHORT A 16-bit (2-byte) unsigned integer. + + // SensingMethod + if (tag == 0xa217) { - if (count == 1) - return new ExifUShort(etag, conv.ToUInt16(value, 0)); - else - return new ExifUShortArray(etag, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + return new ExifEnumProperty( + ExifTag.SensingMethod, + (SensingMethod)conv.ToUInt16(value, 0), + true); } - else if (type == 4) // 4 = LONG A 32-bit (4-byte) unsigned integer. + + // FileSource + if (tag == 0xa300) { - if (count == 1) - return new ExifUInt(etag, conv.ToUInt32(value, 0)); - else - return new ExifUIntArray(etag, ExifBitConverter.ToUIntArray(value, (int)count, byteOrder)); + return new ExifEnumProperty(ExifTag.FileSource, (FileSource)conv.ToUInt16(value, 0), true); } - else if (type == 5) // 5 = RATIONAL Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator. + + // SceneType + if (tag == 0xa301) { - if (count == 1) - return new ExifURational(etag, ExifBitConverter.ToURational(value, byteOrder)); - else - return new ExifURationalArray(etag, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + return new ExifEnumProperty(ExifTag.SceneType, (SceneType)conv.ToUInt16(value, 0), true); } - else if (type == 7) // 7 = UNDEFINED An 8-bit byte that can take any value depending on the field definition. - return new ExifUndefined(etag, value); - else if (type == 9) // 9 = SLONG A 32-bit (4-byte) signed integer (2's complement notation). + + // CustomRendered + if (tag == 0xa401) { - if (count == 1) - return new ExifSInt(etag, conv.ToInt32(value, 0)); - else - return new ExifSIntArray(etag, ExifBitConverter.ToSIntArray(value, (int)count, byteOrder)); + return new ExifEnumProperty( + ExifTag.CustomRendered, + (CustomRendered)conv.ToUInt16(value, 0), + true); } - else if (type == 10) // 10 = SRATIONAL Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + + // ExposureMode + if (tag == 0xa402) { - if (count == 1) - return new ExifSRational(etag, ExifBitConverter.ToSRational(value, byteOrder)); - else - return new ExifSRationalArray(etag, ExifBitConverter.ToSRationalArray(value, (int)count, byteOrder)); + return new ExifEnumProperty(ExifTag.ExposureMode, (ExposureMode)conv.ToUInt16(value, 0), true); + } + + // WhiteBalance + if (tag == 0xa403) + { + return new ExifEnumProperty(ExifTag.WhiteBalance, (WhiteBalance)conv.ToUInt16(value, 0), true); + } + + // SceneCaptureType + if (tag == 0xa406) + { + return new ExifEnumProperty( + ExifTag.SceneCaptureType, + (SceneCaptureType)conv.ToUInt16(value, 0), + true); + } + + // GainControl + if (tag == 0xa407) + { + return new ExifEnumProperty(ExifTag.GainControl, (GainControl)conv.ToUInt16(value, 0), true); + } + + // Contrast + if (tag == 0xa408) + { + return new ExifEnumProperty(ExifTag.Contrast, (Contrast)conv.ToUInt16(value, 0), true); + } + + // Saturation + if (tag == 0xa409) + { + return new ExifEnumProperty(ExifTag.Saturation, (Saturation)conv.ToUInt16(value, 0), true); + } + + // Sharpness + if (tag == 0xa40a) + { + return new ExifEnumProperty(ExifTag.Sharpness, (Sharpness)conv.ToUInt16(value, 0), true); + } + + // SubjectDistanceRange + if (tag == 0xa40c) + { + return new ExifEnumProperty( + ExifTag.SubjectDistanceRange, + (SubjectDistanceRange)conv.ToUInt16(value, 0), + true); } - else - throw new ArgumentException("Unknown property type."); } - #endregion + else if (ifd == IFD.GPS) + { + // GPSVersionID + if (tag == 0) + { + return new ExifVersion(ExifTag.GPSVersionID, ExifBitConverter.ToString(value)); + } + + // GPSLatitudeRef + if (tag == 1) + { + return new ExifEnumProperty(ExifTag.GPSLatitudeRef, (GPSLatitudeRef)value[0]); + } + + // GPSLatitude + if (tag == 2) + { + return new GPSLatitudeLongitude( + ExifTag.GPSLatitude, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSLongitudeRef + if (tag == 3) + { + return new ExifEnumProperty(ExifTag.GPSLongitudeRef, (GPSLongitudeRef)value[0]); + } + + // GPSLongitude + if (tag == 4) + { + return new GPSLatitudeLongitude( + ExifTag.GPSLongitude, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSAltitudeRef + if (tag == 5) + { + return new ExifEnumProperty(ExifTag.GPSAltitudeRef, (GPSAltitudeRef)value[0]); + } + + // GPSTimeStamp + if (tag == 7) + { + return new GPSTimeStamp( + ExifTag.GPSTimeStamp, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSStatus + if (tag == 9) + { + return new ExifEnumProperty(ExifTag.GPSStatus, (GPSStatus)value[0]); + } + + // GPSMeasureMode + if (tag == 10) + { + return new ExifEnumProperty(ExifTag.GPSMeasureMode, (GPSMeasureMode)value[0]); + } + + // GPSSpeedRef + if (tag == 12) + { + return new ExifEnumProperty(ExifTag.GPSSpeedRef, (GPSSpeedRef)value[0]); + } + + // GPSTrackRef + if (tag == 14) + { + return new ExifEnumProperty(ExifTag.GPSTrackRef, (GPSDirectionRef)value[0]); + } + + // GPSImgDirectionRef + if (tag == 16) + { + return new ExifEnumProperty(ExifTag.GPSImgDirectionRef, (GPSDirectionRef)value[0]); + } + + // GPSDestLatitudeRef + if (tag == 19) + { + return new ExifEnumProperty(ExifTag.GPSDestLatitudeRef, (GPSLatitudeRef)value[0]); + } + + // GPSDestLatitude + if (tag == 20) + { + return new GPSLatitudeLongitude( + ExifTag.GPSDestLatitude, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSDestLongitudeRef + if (tag == 21) + { + return new ExifEnumProperty(ExifTag.GPSDestLongitudeRef, (GPSLongitudeRef)value[0]); + } + + // GPSDestLongitude + if (tag == 22) + { + return new GPSLatitudeLongitude( + ExifTag.GPSDestLongitude, + ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // GPSDestBearingRef + if (tag == 23) + { + return new ExifEnumProperty(ExifTag.GPSDestBearingRef, (GPSDirectionRef)value[0]); + } + + // GPSDestDistanceRef + if (tag == 25) + { + return new ExifEnumProperty(ExifTag.GPSDestDistanceRef, (GPSDistanceRef)value[0]); + } + + // GPSDate + if (tag == 29) + { + return new ExifDateTime(ExifTag.GPSDateStamp, ExifBitConverter.ToDateTime(value, false)); + } + + // GPSDifferential + if (tag == 30) + { + return new ExifEnumProperty( + ExifTag.GPSDifferential, + (GPSDifferential)conv.ToUInt16(value, 0)); + } + } + else if (ifd == IFD.Interop) + { + // InteroperabilityIndex + if (tag == 1) + { + return new ExifAscii(ExifTag.InteroperabilityIndex, ExifBitConverter.ToAscii(value, Encoding.ASCII), Encoding.ASCII); + } + + // InteroperabilityVersion + if (tag == 2) + { + return new ExifVersion( + ExifTag.InteroperabilityVersion, + ExifBitConverter.ToAscii(value, Encoding.ASCII)); + } + } + else if (ifd == IFD.First) + { + // Compression + if (tag == 0x103) + { + return new ExifEnumProperty( + ExifTag.ThumbnailCompression, + (Compression)conv.ToUInt16(value, 0)); + } + + // PhotometricInterpretation + if (tag == 0x106) + { + return new ExifEnumProperty( + ExifTag.ThumbnailPhotometricInterpretation, + (PhotometricInterpretation)conv.ToUInt16(value, 0)); + } + + // Orientation + if (tag == 0x112) + { + return new ExifEnumProperty( + ExifTag.ThumbnailOrientation, + (Orientation)conv.ToUInt16(value, 0)); + } + + // PlanarConfiguration + if (tag == 0x11c) + { + return new ExifEnumProperty( + ExifTag.ThumbnailPlanarConfiguration, + (PlanarConfiguration)conv.ToUInt16(value, 0)); + } + + // YCbCrPositioning + if (tag == 0x213) + { + return new ExifEnumProperty( + ExifTag.ThumbnailYCbCrPositioning, + (YCbCrPositioning)conv.ToUInt16(value, 0)); + } + + // ResolutionUnit + if (tag == 0x128) + { + return new ExifEnumProperty( + ExifTag.ThumbnailResolutionUnit, + (ResolutionUnit)conv.ToUInt16(value, 0)); + } + + // DateTime + if (tag == 0x132) + { + return new ExifDateTime(ExifTag.ThumbnailDateTime, ExifBitConverter.ToDateTime(value)); + } + } + + // 1 = BYTE An 8-bit unsigned integer. + if (type == 1) + { + if (count == 1) + { + return new ExifByte(etag, value[0]); + } + + return new ExifByteArray(etag, value); + } + + // 2 = ASCII An 8-bit byte containing one 7-bit ASCII code. + if (type == 2) + { + return new ExifAscii(etag, ExifBitConverter.ToAscii(value, encoding), encoding); + } + + // 3 = SHORT A 16-bit (2-byte) unsigned integer. + if (type == 3) + { + if (count == 1) + { + return new ExifUShort(etag, conv.ToUInt16(value, 0)); + } + + return new ExifUShortArray(etag, ExifBitConverter.ToUShortArray(value, (int)count, byteOrder)); + } + + // 4 = LONG A 32-bit (4-byte) unsigned integer. + if (type == 4) + { + if (count == 1) + { + return new ExifUInt(etag, conv.ToUInt32(value, 0)); + } + + return new ExifUIntArray(etag, ExifBitConverter.ToUIntArray(value, (int)count, byteOrder)); + } + + // 5 = RATIONAL Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator. + if (type == 5) + { + if (count == 1) + { + return new ExifURational(etag, ExifBitConverter.ToURational(value, byteOrder)); + } + + return new ExifURationalArray(etag, ExifBitConverter.ToURationalArray(value, (int)count, byteOrder)); + } + + // 7 = UNDEFINED An 8-bit byte that can take any value depending on the field definition. + if (type == 7) + { + return new ExifUndefined(etag, value); + } + + // 9 = SLONG A 32-bit (4-byte) signed integer (2's complement notation). + if (type == 9) + { + if (count == 1) + { + return new ExifSInt(etag, conv.ToInt32(value, 0)); + } + + return new ExifSIntArray(etag, ExifBitConverter.ToSIntArray(value, (int)count, byteOrder)); + } + + // 10 = SRATIONAL Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + if (type == 10) + { + if (count == 1) + { + return new ExifSRational(etag, ExifBitConverter.ToSRational(value, byteOrder)); + } + + return new ExifSRationalArray(etag, ExifBitConverter.ToSRationalArray(value, (int)count, byteOrder)); + } + + throw new ArgumentException("Unknown property type."); } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ExifTag.cs b/src/Umbraco.Core/Media/Exif/ExifTag.cs index 22215044b2..0ffd754836 100644 --- a/src/Umbraco.Core/Media/Exif/ExifTag.cs +++ b/src/Umbraco.Core/Media/Exif/ExifTag.cs @@ -1,290 +1,310 @@ - -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the tags associated with exif fields. +/// +internal enum ExifTag { + // **************************** + // Zeroth IFD + // **************************** + NewSubfileType = IFD.Zeroth + 254, + SubfileType = IFD.Zeroth + 255, + ImageWidth = IFD.Zeroth + 256, + ImageLength = IFD.Zeroth + 257, + BitsPerSample = IFD.Zeroth + 258, + Compression = IFD.Zeroth + 259, + PhotometricInterpretation = IFD.Zeroth + 262, + Threshholding = IFD.Zeroth + 263, + CellWidth = IFD.Zeroth + 264, + CellLength = IFD.Zeroth + 265, + FillOrder = IFD.Zeroth + 266, + DocumentName = IFD.Zeroth + 269, + ImageDescription = IFD.Zeroth + 270, + Make = IFD.Zeroth + 271, + Model = IFD.Zeroth + 272, + StripOffsets = IFD.Zeroth + 273, + Orientation = IFD.Zeroth + 274, + SamplesPerPixel = IFD.Zeroth + 277, + RowsPerStrip = IFD.Zeroth + 278, + StripByteCounts = IFD.Zeroth + 279, + MinSampleValue = IFD.Zeroth + 280, + MaxSampleValue = IFD.Zeroth + 281, + XResolution = IFD.Zeroth + 282, + YResolution = IFD.Zeroth + 283, + PlanarConfiguration = IFD.Zeroth + 284, + PageName = IFD.Zeroth + 285, + XPosition = IFD.Zeroth + 286, + YPosition = IFD.Zeroth + 287, + FreeOffsets = IFD.Zeroth + 288, + FreeByteCounts = IFD.Zeroth + 289, + GrayResponseUnit = IFD.Zeroth + 290, + GrayResponseCurve = IFD.Zeroth + 291, + T4Options = IFD.Zeroth + 292, + T6Options = IFD.Zeroth + 293, + ResolutionUnit = IFD.Zeroth + 296, + PageNumber = IFD.Zeroth + 297, + TransferFunction = IFD.Zeroth + 301, + Software = IFD.Zeroth + 305, + DateTime = IFD.Zeroth + 306, + Artist = IFD.Zeroth + 315, + HostComputer = IFD.Zeroth + 316, + Predictor = IFD.Zeroth + 317, + WhitePoint = IFD.Zeroth + 318, + PrimaryChromaticities = IFD.Zeroth + 319, + ColorMap = IFD.Zeroth + 320, + HalftoneHints = IFD.Zeroth + 321, + TileWidth = IFD.Zeroth + 322, + TileLength = IFD.Zeroth + 323, + TileOffsets = IFD.Zeroth + 324, + TileByteCounts = IFD.Zeroth + 325, + InkSet = IFD.Zeroth + 332, + InkNames = IFD.Zeroth + 333, + NumberOfInks = IFD.Zeroth + 334, + DotRange = IFD.Zeroth + 336, + TargetPrinter = IFD.Zeroth + 337, + ExtraSamples = IFD.Zeroth + 338, + SampleFormat = IFD.Zeroth + 339, + SMinSampleValue = IFD.Zeroth + 340, + SMaxSampleValue = IFD.Zeroth + 341, + TransferRange = IFD.Zeroth + 342, + JPEGProc = IFD.Zeroth + 512, + JPEGInterchangeFormat = IFD.Zeroth + 513, + JPEGInterchangeFormatLength = IFD.Zeroth + 514, + JPEGRestartInterval = IFD.Zeroth + 515, + JPEGLosslessPredictors = IFD.Zeroth + 517, + JPEGPointTransforms = IFD.Zeroth + 518, + JPEGQTables = IFD.Zeroth + 519, + JPEGDCTables = IFD.Zeroth + 520, + JPEGACTables = IFD.Zeroth + 521, + YCbCrCoefficients = IFD.Zeroth + 529, + YCbCrSubSampling = IFD.Zeroth + 530, + YCbCrPositioning = IFD.Zeroth + 531, + ReferenceBlackWhite = IFD.Zeroth + 532, + Copyright = IFD.Zeroth + 33432, + + // Pointers to other IFDs + EXIFIFDPointer = IFD.Zeroth + 34665, + GPSIFDPointer = IFD.Zeroth + 34853, + + // Windows Tags + WindowsTitle = IFD.Zeroth + 0x9c9b, + WindowsComment = IFD.Zeroth + 0x9c9c, + WindowsAuthor = IFD.Zeroth + 0x9c9d, + WindowsKeywords = IFD.Zeroth + 0x9c9e, + WindowsSubject = IFD.Zeroth + 0x9c9f, + + // Rating + Rating = IFD.Zeroth + 0x4746, + RatingPercent = IFD.Zeroth + 0x4749, + + // Microsoft specifying padding and offset tags + ZerothIFDPadding = IFD.Zeroth + 0xea1c, + + // **************************** + // EXIF Tags + // **************************** + ExifVersion = IFD.EXIF + 36864, + FlashpixVersion = IFD.EXIF + 40960, + ColorSpace = IFD.EXIF + 40961, + ComponentsConfiguration = IFD.EXIF + 37121, + CompressedBitsPerPixel = IFD.EXIF + 37122, + PixelXDimension = IFD.EXIF + 40962, + PixelYDimension = IFD.EXIF + 40963, + MakerNote = IFD.EXIF + 37500, + UserComment = IFD.EXIF + 37510, + RelatedSoundFile = IFD.EXIF + 40964, + DateTimeOriginal = IFD.EXIF + 36867, + DateTimeDigitized = IFD.EXIF + 36868, + SubSecTime = IFD.EXIF + 37520, + SubSecTimeOriginal = IFD.EXIF + 37521, + SubSecTimeDigitized = IFD.EXIF + 37522, + ExposureTime = IFD.EXIF + 33434, + FNumber = IFD.EXIF + 33437, + ExposureProgram = IFD.EXIF + 34850, + SpectralSensitivity = IFD.EXIF + 34852, + ISOSpeedRatings = IFD.EXIF + 34855, + OECF = IFD.EXIF + 34856, + ShutterSpeedValue = IFD.EXIF + 37377, + ApertureValue = IFD.EXIF + 37378, + BrightnessValue = IFD.EXIF + 37379, + ExposureBiasValue = IFD.EXIF + 37380, + MaxApertureValue = IFD.EXIF + 37381, + SubjectDistance = IFD.EXIF + 37382, + MeteringMode = IFD.EXIF + 37383, + LightSource = IFD.EXIF + 37384, + Flash = IFD.EXIF + 37385, + FocalLength = IFD.EXIF + 37386, + SubjectArea = IFD.EXIF + 37396, + FlashEnergy = IFD.EXIF + 41483, + SpatialFrequencyResponse = IFD.EXIF + 41484, + FocalPlaneXResolution = IFD.EXIF + 41486, + FocalPlaneYResolution = IFD.EXIF + 41487, + FocalPlaneResolutionUnit = IFD.EXIF + 41488, + SubjectLocation = IFD.EXIF + 41492, + ExposureIndex = IFD.EXIF + 41493, + SensingMethod = IFD.EXIF + 41495, + FileSource = IFD.EXIF + 41728, + SceneType = IFD.EXIF + 41729, + CFAPattern = IFD.EXIF + 41730, + CustomRendered = IFD.EXIF + 41985, + ExposureMode = IFD.EXIF + 41986, + WhiteBalance = IFD.EXIF + 41987, + DigitalZoomRatio = IFD.EXIF + 41988, + FocalLengthIn35mmFilm = IFD.EXIF + 41989, + SceneCaptureType = IFD.EXIF + 41990, + GainControl = IFD.EXIF + 41991, + Contrast = IFD.EXIF + 41992, + Saturation = IFD.EXIF + 41993, + Sharpness = IFD.EXIF + 41994, + DeviceSettingDescription = IFD.EXIF + 41995, + SubjectDistanceRange = IFD.EXIF + 41996, + ImageUniqueID = IFD.EXIF + 42016, + InteroperabilityIFDPointer = IFD.EXIF + 40965, + + // Microsoft specifying padding and offset tags + ExifIFDPadding = IFD.EXIF + 0xea1c, + OffsetSchema = IFD.EXIF + 0xea1d, + + // **************************** + // GPS Tags + // **************************** + GPSVersionID = IFD.GPS + 0, + GPSLatitudeRef = IFD.GPS + 1, + GPSLatitude = IFD.GPS + 2, + GPSLongitudeRef = IFD.GPS + 3, + GPSLongitude = IFD.GPS + 4, + GPSAltitudeRef = IFD.GPS + 5, + GPSAltitude = IFD.GPS + 6, + GPSTimeStamp = IFD.GPS + 7, + GPSSatellites = IFD.GPS + 8, + GPSStatus = IFD.GPS + 9, + GPSMeasureMode = IFD.GPS + 10, + GPSDOP = IFD.GPS + 11, + GPSSpeedRef = IFD.GPS + 12, + GPSSpeed = IFD.GPS + 13, + GPSTrackRef = IFD.GPS + 14, + GPSTrack = IFD.GPS + 15, + GPSImgDirectionRef = IFD.GPS + 16, + GPSImgDirection = IFD.GPS + 17, + GPSMapDatum = IFD.GPS + 18, + GPSDestLatitudeRef = IFD.GPS + 19, + GPSDestLatitude = IFD.GPS + 20, + GPSDestLongitudeRef = IFD.GPS + 21, + GPSDestLongitude = IFD.GPS + 22, + GPSDestBearingRef = IFD.GPS + 23, + GPSDestBearing = IFD.GPS + 24, + GPSDestDistanceRef = IFD.GPS + 25, + GPSDestDistance = IFD.GPS + 26, + GPSProcessingMethod = IFD.GPS + 27, + GPSAreaInformation = IFD.GPS + 28, + GPSDateStamp = IFD.GPS + 29, + GPSDifferential = IFD.GPS + 30, + + // **************************** + // InterOp Tags + // **************************** + InteroperabilityIndex = IFD.Interop + 1, + InteroperabilityVersion = IFD.Interop + 2, + RelatedImageWidth = IFD.Interop + 0x1001, + RelatedImageHeight = IFD.Interop + 0x1002, + + // **************************** + // First IFD TIFF Tags + // **************************** + ThumbnailImageWidth = IFD.First + 256, + ThumbnailImageLength = IFD.First + 257, + ThumbnailBitsPerSample = IFD.First + 258, + ThumbnailCompression = IFD.First + 259, + ThumbnailPhotometricInterpretation = IFD.First + 262, + ThumbnailOrientation = IFD.First + 274, + ThumbnailSamplesPerPixel = IFD.First + 277, + ThumbnailPlanarConfiguration = IFD.First + 284, + ThumbnailYCbCrSubSampling = IFD.First + 530, + ThumbnailYCbCrPositioning = IFD.First + 531, + ThumbnailXResolution = IFD.First + 282, + ThumbnailYResolution = IFD.First + 283, + ThumbnailResolutionUnit = IFD.First + 296, + ThumbnailStripOffsets = IFD.First + 273, + ThumbnailRowsPerStrip = IFD.First + 278, + ThumbnailStripByteCounts = IFD.First + 279, + ThumbnailJPEGInterchangeFormat = IFD.First + 513, + ThumbnailJPEGInterchangeFormatLength = IFD.First + 514, + ThumbnailTransferFunction = IFD.First + 301, + ThumbnailWhitePoint = IFD.First + 318, + ThumbnailPrimaryChromaticities = IFD.First + 319, + ThumbnailYCbCrCoefficients = IFD.First + 529, + ThumbnailReferenceBlackWhite = IFD.First + 532, + ThumbnailDateTime = IFD.First + 306, + ThumbnailImageDescription = IFD.First + 270, + ThumbnailMake = IFD.First + 271, + ThumbnailModel = IFD.First + 272, + ThumbnailSoftware = IFD.First + 305, + ThumbnailArtist = IFD.First + 315, + ThumbnailCopyright = IFD.First + 33432, + + // **************************** + // JFIF Tags + // **************************** + /// - /// Represents the tags associated with exif fields. + /// Represents the JFIF version. /// - internal enum ExifTag : int - { - // **************************** - // Zeroth IFD - // **************************** - NewSubfileType = IFD.Zeroth + 254, - SubfileType = IFD.Zeroth + 255, - ImageWidth = IFD.Zeroth + 256, - ImageLength = IFD.Zeroth + 257, - BitsPerSample = IFD.Zeroth + 258, - Compression = IFD.Zeroth + 259, - PhotometricInterpretation = IFD.Zeroth + 262, - Threshholding = IFD.Zeroth + 263, - CellWidth = IFD.Zeroth + 264, - CellLength = IFD.Zeroth + 265, - FillOrder = IFD.Zeroth + 266, - DocumentName = IFD.Zeroth + 269, - ImageDescription = IFD.Zeroth + 270, - Make = IFD.Zeroth + 271, - Model = IFD.Zeroth + 272, - StripOffsets = IFD.Zeroth + 273, - Orientation = IFD.Zeroth + 274, - SamplesPerPixel = IFD.Zeroth + 277, - RowsPerStrip = IFD.Zeroth + 278, - StripByteCounts = IFD.Zeroth + 279, - MinSampleValue = IFD.Zeroth + 280, - MaxSampleValue = IFD.Zeroth + 281, - XResolution = IFD.Zeroth + 282, - YResolution = IFD.Zeroth + 283, - PlanarConfiguration = IFD.Zeroth + 284, - PageName = IFD.Zeroth + 285, - XPosition = IFD.Zeroth + 286, - YPosition = IFD.Zeroth + 287, - FreeOffsets = IFD.Zeroth + 288, - FreeByteCounts = IFD.Zeroth + 289, - GrayResponseUnit = IFD.Zeroth + 290, - GrayResponseCurve = IFD.Zeroth + 291, - T4Options = IFD.Zeroth + 292, - T6Options = IFD.Zeroth + 293, - ResolutionUnit = IFD.Zeroth + 296, - PageNumber = IFD.Zeroth + 297, - TransferFunction = IFD.Zeroth + 301, - Software = IFD.Zeroth + 305, - DateTime = IFD.Zeroth + 306, - Artist = IFD.Zeroth + 315, - HostComputer = IFD.Zeroth + 316, - Predictor = IFD.Zeroth + 317, - WhitePoint = IFD.Zeroth + 318, - PrimaryChromaticities = IFD.Zeroth + 319, - ColorMap = IFD.Zeroth + 320, - HalftoneHints = IFD.Zeroth + 321, - TileWidth = IFD.Zeroth + 322, - TileLength = IFD.Zeroth + 323, - TileOffsets = IFD.Zeroth + 324, - TileByteCounts = IFD.Zeroth + 325, - InkSet = IFD.Zeroth + 332, - InkNames = IFD.Zeroth + 333, - NumberOfInks = IFD.Zeroth + 334, - DotRange = IFD.Zeroth + 336, - TargetPrinter = IFD.Zeroth + 337, - ExtraSamples = IFD.Zeroth + 338, - SampleFormat = IFD.Zeroth + 339, - SMinSampleValue = IFD.Zeroth + 340, - SMaxSampleValue = IFD.Zeroth + 341, - TransferRange = IFD.Zeroth + 342, - JPEGProc = IFD.Zeroth + 512, - JPEGInterchangeFormat = IFD.Zeroth + 513, - JPEGInterchangeFormatLength = IFD.Zeroth + 514, - JPEGRestartInterval = IFD.Zeroth + 515, - JPEGLosslessPredictors = IFD.Zeroth + 517, - JPEGPointTransforms = IFD.Zeroth + 518, - JPEGQTables = IFD.Zeroth + 519, - JPEGDCTables = IFD.Zeroth + 520, - JPEGACTables = IFD.Zeroth + 521, - YCbCrCoefficients = IFD.Zeroth + 529, - YCbCrSubSampling = IFD.Zeroth + 530, - YCbCrPositioning = IFD.Zeroth + 531, - ReferenceBlackWhite = IFD.Zeroth + 532, - Copyright = IFD.Zeroth + 33432, - // Pointers to other IFDs - EXIFIFDPointer = IFD.Zeroth + 34665, - GPSIFDPointer = IFD.Zeroth + 34853, - // Windows Tags - WindowsTitle = IFD.Zeroth + 0x9c9b, - WindowsComment = IFD.Zeroth + 0x9c9c, - WindowsAuthor = IFD.Zeroth + 0x9c9d, - WindowsKeywords = IFD.Zeroth + 0x9c9e, - WindowsSubject = IFD.Zeroth + 0x9c9f, - // Rating - Rating = IFD.Zeroth + 0x4746, - RatingPercent = IFD.Zeroth + 0x4749, - // Microsoft specifying padding and offset tags - ZerothIFDPadding = IFD.Zeroth + 0xea1c, - // **************************** - // EXIF Tags - // **************************** - ExifVersion = IFD.EXIF + 36864, - FlashpixVersion = IFD.EXIF + 40960, - ColorSpace = IFD.EXIF + 40961, - ComponentsConfiguration = IFD.EXIF + 37121, - CompressedBitsPerPixel = IFD.EXIF + 37122, - PixelXDimension = IFD.EXIF + 40962, - PixelYDimension = IFD.EXIF + 40963, - MakerNote = IFD.EXIF + 37500, - UserComment = IFD.EXIF + 37510, - RelatedSoundFile = IFD.EXIF + 40964, - DateTimeOriginal = IFD.EXIF + 36867, - DateTimeDigitized = IFD.EXIF + 36868, - SubSecTime = IFD.EXIF + 37520, - SubSecTimeOriginal = IFD.EXIF + 37521, - SubSecTimeDigitized = IFD.EXIF + 37522, - ExposureTime = IFD.EXIF + 33434, - FNumber = IFD.EXIF + 33437, - ExposureProgram = IFD.EXIF + 34850, - SpectralSensitivity = IFD.EXIF + 34852, - ISOSpeedRatings = IFD.EXIF + 34855, - OECF = IFD.EXIF + 34856, - ShutterSpeedValue = IFD.EXIF + 37377, - ApertureValue = IFD.EXIF + 37378, - BrightnessValue = IFD.EXIF + 37379, - ExposureBiasValue = IFD.EXIF + 37380, - MaxApertureValue = IFD.EXIF + 37381, - SubjectDistance = IFD.EXIF + 37382, - MeteringMode = IFD.EXIF + 37383, - LightSource = IFD.EXIF + 37384, - Flash = IFD.EXIF + 37385, - FocalLength = IFD.EXIF + 37386, - SubjectArea = IFD.EXIF + 37396, - FlashEnergy = IFD.EXIF + 41483, - SpatialFrequencyResponse = IFD.EXIF + 41484, - FocalPlaneXResolution = IFD.EXIF + 41486, - FocalPlaneYResolution = IFD.EXIF + 41487, - FocalPlaneResolutionUnit = IFD.EXIF + 41488, - SubjectLocation = IFD.EXIF + 41492, - ExposureIndex = IFD.EXIF + 41493, - SensingMethod = IFD.EXIF + 41495, - FileSource = IFD.EXIF + 41728, - SceneType = IFD.EXIF + 41729, - CFAPattern = IFD.EXIF + 41730, - CustomRendered = IFD.EXIF + 41985, - ExposureMode = IFD.EXIF + 41986, - WhiteBalance = IFD.EXIF + 41987, - DigitalZoomRatio = IFD.EXIF + 41988, - FocalLengthIn35mmFilm = IFD.EXIF + 41989, - SceneCaptureType = IFD.EXIF + 41990, - GainControl = IFD.EXIF + 41991, - Contrast = IFD.EXIF + 41992, - Saturation = IFD.EXIF + 41993, - Sharpness = IFD.EXIF + 41994, - DeviceSettingDescription = IFD.EXIF + 41995, - SubjectDistanceRange = IFD.EXIF + 41996, - ImageUniqueID = IFD.EXIF + 42016, - InteroperabilityIFDPointer = IFD.EXIF + 40965, - // Microsoft specifying padding and offset tags - ExifIFDPadding = IFD.EXIF + 0xea1c, - OffsetSchema = IFD.EXIF + 0xea1d, - // **************************** - // GPS Tags - // **************************** - GPSVersionID = IFD.GPS + 0, - GPSLatitudeRef = IFD.GPS + 1, - GPSLatitude = IFD.GPS + 2, - GPSLongitudeRef = IFD.GPS + 3, - GPSLongitude = IFD.GPS + 4, - GPSAltitudeRef = IFD.GPS + 5, - GPSAltitude = IFD.GPS + 6, - GPSTimeStamp = IFD.GPS + 7, - GPSSatellites = IFD.GPS + 8, - GPSStatus = IFD.GPS + 9, - GPSMeasureMode = IFD.GPS + 10, - GPSDOP = IFD.GPS + 11, - GPSSpeedRef = IFD.GPS + 12, - GPSSpeed = IFD.GPS + 13, - GPSTrackRef = IFD.GPS + 14, - GPSTrack = IFD.GPS + 15, - GPSImgDirectionRef = IFD.GPS + 16, - GPSImgDirection = IFD.GPS + 17, - GPSMapDatum = IFD.GPS + 18, - GPSDestLatitudeRef = IFD.GPS + 19, - GPSDestLatitude = IFD.GPS + 20, - GPSDestLongitudeRef = IFD.GPS + 21, - GPSDestLongitude = IFD.GPS + 22, - GPSDestBearingRef = IFD.GPS + 23, - GPSDestBearing = IFD.GPS + 24, - GPSDestDistanceRef = IFD.GPS + 25, - GPSDestDistance = IFD.GPS + 26, - GPSProcessingMethod = IFD.GPS + 27, - GPSAreaInformation = IFD.GPS + 28, - GPSDateStamp = IFD.GPS + 29, - GPSDifferential = IFD.GPS + 30, - // **************************** - // InterOp Tags - // **************************** - InteroperabilityIndex = IFD.Interop + 1, - InteroperabilityVersion = IFD.Interop + 2, - RelatedImageWidth = IFD.Interop + 0x1001, - RelatedImageHeight = IFD.Interop + 0x1002, - // **************************** - // First IFD TIFF Tags - // **************************** - ThumbnailImageWidth = IFD.First + 256, - ThumbnailImageLength = IFD.First + 257, - ThumbnailBitsPerSample = IFD.First + 258, - ThumbnailCompression = IFD.First + 259, - ThumbnailPhotometricInterpretation = IFD.First + 262, - ThumbnailOrientation = IFD.First + 274, - ThumbnailSamplesPerPixel = IFD.First + 277, - ThumbnailPlanarConfiguration = IFD.First + 284, - ThumbnailYCbCrSubSampling = IFD.First + 530, - ThumbnailYCbCrPositioning = IFD.First + 531, - ThumbnailXResolution = IFD.First + 282, - ThumbnailYResolution = IFD.First + 283, - ThumbnailResolutionUnit = IFD.First + 296, - ThumbnailStripOffsets = IFD.First + 273, - ThumbnailRowsPerStrip = IFD.First + 278, - ThumbnailStripByteCounts = IFD.First + 279, - ThumbnailJPEGInterchangeFormat = IFD.First + 513, - ThumbnailJPEGInterchangeFormatLength = IFD.First + 514, - ThumbnailTransferFunction = IFD.First + 301, - ThumbnailWhitePoint = IFD.First + 318, - ThumbnailPrimaryChromaticities = IFD.First + 319, - ThumbnailYCbCrCoefficients = IFD.First + 529, - ThumbnailReferenceBlackWhite = IFD.First + 532, - ThumbnailDateTime = IFD.First + 306, - ThumbnailImageDescription = IFD.First + 270, - ThumbnailMake = IFD.First + 271, - ThumbnailModel = IFD.First + 272, - ThumbnailSoftware = IFD.First + 305, - ThumbnailArtist = IFD.First + 315, - ThumbnailCopyright = IFD.First + 33432, - // **************************** - // JFIF Tags - // **************************** - /// - /// Represents the JFIF version. - /// - JFIFVersion = IFD.JFIF + 1, - /// - /// Represents units for X and Y densities. - /// - JFIFUnits = IFD.JFIF + 101, - /// - /// Horizontal pixel density. - /// - XDensity = IFD.JFIF + 102, - /// - /// Vertical pixel density - /// - YDensity = IFD.JFIF + 103, - /// - /// Thumbnail horizontal pixel count. - /// - JFIFXThumbnail = IFD.JFIF + 201, - /// - /// Thumbnail vertical pixel count. - /// - JFIFYThumbnail = IFD.JFIF + 202, - /// - /// JFIF JPEG thumbnail. - /// - JFIFThumbnail = IFD.JFIF + 203, - /// - /// Code which identifies the JFIF extension. - /// - JFXXExtensionCode = IFD.JFXX + 1, - /// - /// Thumbnail horizontal pixel count. - /// - JFXXXThumbnail = IFD.JFXX + 101, - /// - /// Thumbnail vertical pixel count. - /// - JFXXYThumbnail = IFD.JFXX + 102, - /// - /// The 256-Color RGB palette. - /// - JFXXPalette = IFD.JFXX + 201, - /// - /// JFIF thumbnail. The thumbnail will be either a JPEG, - /// a 256 color palette bitmap, or a 24-bit RGB bitmap. - /// - JFXXThumbnail = IFD.JFXX + 202, - } + JFIFVersion = IFD.JFIF + 1, + + /// + /// Represents units for X and Y densities. + /// + JFIFUnits = IFD.JFIF + 101, + + /// + /// Horizontal pixel density. + /// + XDensity = IFD.JFIF + 102, + + /// + /// Vertical pixel density + /// + YDensity = IFD.JFIF + 103, + + /// + /// Thumbnail horizontal pixel count. + /// + JFIFXThumbnail = IFD.JFIF + 201, + + /// + /// Thumbnail vertical pixel count. + /// + JFIFYThumbnail = IFD.JFIF + 202, + + /// + /// JFIF JPEG thumbnail. + /// + JFIFThumbnail = IFD.JFIF + 203, + + /// + /// Code which identifies the JFIF extension. + /// + JFXXExtensionCode = IFD.JFXX + 1, + + /// + /// Thumbnail horizontal pixel count. + /// + JFXXXThumbnail = IFD.JFXX + 101, + + /// + /// Thumbnail vertical pixel count. + /// + JFXXYThumbnail = IFD.JFXX + 102, + + /// + /// The 256-Color RGB palette. + /// + JFXXPalette = IFD.JFXX + 201, + + /// + /// JFIF thumbnail. The thumbnail will be either a JPEG, + /// a 256 color palette bitmap, or a 24-bit RGB bitmap. + /// + JFXXThumbnail = IFD.JFXX + 202, } diff --git a/src/Umbraco.Core/Media/Exif/ExifTagFactory.cs b/src/Umbraco.Core/Media/Exif/ExifTagFactory.cs index 6a5ea84944..726da925aa 100644 --- a/src/Umbraco.Core/Media/Exif/ExifTagFactory.cs +++ b/src/Umbraco.Core/Media/Exif/ExifTagFactory.cs @@ -1,68 +1,63 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +internal static class ExifTagFactory { - internal static class ExifTagFactory + #region Static Methods + + /// + /// Returns the ExifTag corresponding to the given tag id. + /// + public static ExifTag GetExifTag(IFD ifd, ushort tagid) => (ExifTag)(ifd + tagid); + + /// + /// Returns the tag id corresponding to the given ExifTag. + /// + public static ushort GetTagID(ExifTag exiftag) { - #region Static Methods - /// - /// Returns the ExifTag corresponding to the given tag id. - /// - public static ExifTag GetExifTag(IFD ifd, ushort tagid) - { - return (ExifTag)(ifd + tagid); - } - - /// - /// Returns the tag id corresponding to the given ExifTag. - /// - public static ushort GetTagID(ExifTag exiftag) - { - IFD ifd = GetTagIFD(exiftag); - return (ushort)((int)exiftag - (int)ifd); - } - - /// - /// Returns the IFD section containing the given tag. - /// - public static IFD GetTagIFD(ExifTag tag) - { - return (IFD)(((int)tag / 100000) * 100000); - } - - /// - /// Returns the string representation for the given exif tag. - /// - public static string GetTagName(ExifTag tag) - { - string? name = Enum.GetName(typeof(ExifTag), tag); - if (name == null) - return "Unknown"; - else - return name; - } - - /// - /// Returns the string representation for the given tag id. - /// - public static string GetTagName(IFD ifd, ushort tagid) - { - return GetTagName(GetExifTag(ifd, tagid)); - } - - /// - /// Returns the string representation for the given exif tag including - /// IFD section and tag id. - /// - public static string GetTagLongName(ExifTag tag) - { - string? ifdname = Enum.GetName(typeof(IFD), GetTagIFD(tag)); - string? name = Enum.GetName(typeof(ExifTag), tag); - if (name == null) - name = "Unknown"; - string tagidname = GetTagID(tag).ToString(); - return ifdname + ": " + name + " (" + tagidname + ")"; - } - #endregion + IFD ifd = GetTagIFD(exiftag); + return (ushort)((int)exiftag - (int)ifd); } + + /// + /// Returns the IFD section containing the given tag. + /// + public static IFD GetTagIFD(ExifTag tag) => (IFD)((int)tag / 100000 * 100000); + + /// + /// Returns the string representation for the given exif tag. + /// + public static string GetTagName(ExifTag tag) + { + var name = Enum.GetName(typeof(ExifTag), tag); + if (name == null) + { + return "Unknown"; + } + + return name; + } + + /// + /// Returns the string representation for the given tag id. + /// + public static string GetTagName(IFD ifd, ushort tagid) => GetTagName(GetExifTag(ifd, tagid)); + + /// + /// Returns the string representation for the given exif tag including + /// IFD section and tag id. + /// + public static string GetTagLongName(ExifTag tag) + { + var ifdname = Enum.GetName(typeof(IFD), GetTagIFD(tag)); + var name = Enum.GetName(typeof(ExifTag), tag); + if (name == null) + { + name = "Unknown"; + } + + var tagidname = GetTagID(tag).ToString(); + return ifdname + ": " + name + " (" + tagidname + ")"; + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/IFD.cs b/src/Umbraco.Core/Media/Exif/IFD.cs index e275e8d52a..cda3cdcb69 100644 --- a/src/Umbraco.Core/Media/Exif/IFD.cs +++ b/src/Umbraco.Core/Media/Exif/IFD.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the IFD section containing tags. +/// +internal enum IFD { - /// - /// Represents the IFD section containing tags. - /// - internal enum IFD : int - { - Unknown = 0, - Zeroth = 100000, - EXIF = 200000, - GPS = 300000, - Interop = 400000, - First = 500000, - MakerNote = 600000, - JFIF = 700000, - JFXX = 800000, - } + Unknown = 0, + Zeroth = 100000, + EXIF = 200000, + GPS = 300000, + Interop = 400000, + First = 500000, + MakerNote = 600000, + JFIF = 700000, + JFXX = 800000, } diff --git a/src/Umbraco.Core/Media/Exif/ImageFile.cs b/src/Umbraco.Core/Media/Exif/ImageFile.cs index cb783d3ee9..23ea615be9 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFile.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFile.cs @@ -1,139 +1,144 @@ using System.ComponentModel; -using System.IO; using System.Text; using Umbraco.Cms.Core.Media.TypeDetector; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the base class for image files. +/// +[TypeDescriptionProvider(typeof(ExifFileTypeDescriptionProvider))] +internal abstract class ImageFile { + #region Constructor + /// - /// Represents the base class for image files. + /// Initializes a new instance of the class. /// - [TypeDescriptionProvider(typeof(ExifFileTypeDescriptionProvider))] - internal abstract class ImageFile + protected ImageFile() { - #region Constructor - /// - /// Initializes a new instance of the class. - /// - protected ImageFile () - { - Format = ImageFileFormat.Unknown; - Properties = new ExifPropertyCollection (this); - Encoding = Encoding.Default; - } - #endregion - - #region Properties - /// - /// Returns the format of the . - /// - public ImageFileFormat Format { get; protected set; } - /// - /// Gets the collection of Exif properties contained in the . - /// - public ExifPropertyCollection Properties { get; private set; } - /// - /// Gets or sets the embedded thumbnail image. - /// - public ImageFile? Thumbnail { get; set; } - /// - /// Gets or sets the Exif property with the given key. - /// - /// The Exif tag associated with the Exif property. - public ExifProperty this[ExifTag key] { - get { return Properties[key]; } - set { Properties[key] = value; } - } - /// - /// Gets the encoding used for text metadata when the source encoding is unknown. - /// - public Encoding Encoding { get; protected set; } - #endregion - - #region Instance Methods - - /// - /// Saves the to the specified file. - /// - /// A string that contains the name of the file. - public virtual void Save (string filename) - { - using (FileStream stream = new FileStream (filename, FileMode.Create, FileAccess.Write, FileShare.None)) { - Save (stream); - } - } - - /// - /// Saves the to the specified stream. - /// - /// A to save image data to. - public abstract void Save (Stream stream); - #endregion - - #region Static Methods - /// - /// Creates an from the specified file. - /// - /// A string that contains the name of the file. - /// The created from the file. - public static ImageFile? FromFile (string filename) - { - return FromFile(filename, Encoding.Default); - } - - /// - /// Creates an from the specified file. - /// - /// A string that contains the name of the file. - /// The encoding to be used for text metadata when the source encoding is unknown. - /// The created from the file. - public static ImageFile? FromFile(string filename, Encoding encoding) - { - using (FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - return FromStream(stream, encoding); - } - } - - /// - /// Creates an from the specified data stream. - /// - /// A that contains image data. - /// The created from the file. - public static ImageFile? FromStream(Stream stream) - { - return FromStream(stream, Encoding.Default); - } - - /// - /// Creates an from the specified data stream. - /// - /// A that contains image data. - /// The encoding to be used for text metadata when the source encoding is unknown. - /// The created from the file. - public static ImageFile? FromStream(Stream stream, Encoding encoding) - { - // JPEG - if (JpegDetector.IsOfType(stream)) - { - return new JPEGFile(stream, encoding); - } - - // TIFF - if (TIFFDetector.IsOfType(stream)) - { - return new TIFFFile(stream, encoding); - } - - // SVG - if (SvgDetector.IsOfType(stream)) - { - return new SvgFile(stream); - } - - // We don't know - return null; - } - #endregion + Format = ImageFileFormat.Unknown; + Properties = new ExifPropertyCollection(this); + Encoding = Encoding.Default; } + + #endregion + + #region Properties + + /// + /// Returns the format of the . + /// + public ImageFileFormat Format { get; protected set; } + + /// + /// Gets the collection of Exif properties contained in the . + /// + public ExifPropertyCollection Properties { get; } + + /// + /// Gets or sets the embedded thumbnail image. + /// + public ImageFile? Thumbnail { get; set; } + + /// + /// Gets or sets the Exif property with the given key. + /// + /// The Exif tag associated with the Exif property. + public ExifProperty this[ExifTag key] + { + get => Properties[key]; + set => Properties[key] = value; + } + + /// + /// Gets the encoding used for text metadata when the source encoding is unknown. + /// + public Encoding Encoding { get; protected set; } + + #endregion + + #region Instance Methods + + /// + /// Saves the to the specified file. + /// + /// A string that contains the name of the file. + public virtual void Save(string filename) + { + using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + { + Save(stream); + } + } + + /// + /// Saves the to the specified stream. + /// + /// A to save image data to. + public abstract void Save(Stream stream); + + #endregion + + #region Static Methods + + /// + /// Creates an from the specified file. + /// + /// A string that contains the name of the file. + /// The created from the file. + public static ImageFile? FromFile(string filename) => FromFile(filename, Encoding.Default); + + /// + /// Creates an from the specified file. + /// + /// A string that contains the name of the file. + /// The encoding to be used for text metadata when the source encoding is unknown. + /// The created from the file. + public static ImageFile? FromFile(string filename, Encoding encoding) + { + using (var stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + return FromStream(stream, encoding); + } + } + + /// + /// Creates an from the specified data stream. + /// + /// A that contains image data. + /// The created from the file. + public static ImageFile? FromStream(Stream stream) => FromStream(stream, Encoding.Default); + + /// + /// Creates an from the specified data stream. + /// + /// A that contains image data. + /// The encoding to be used for text metadata when the source encoding is unknown. + /// The created from the file. + public static ImageFile? FromStream(Stream stream, Encoding encoding) + { + // JPEG + if (JpegDetector.IsOfType(stream)) + { + return new JPEGFile(stream, encoding); + } + + // TIFF + if (TIFFDetector.IsOfType(stream)) + { + return new TIFFFile(stream, encoding); + } + + // SVG + if (SvgDetector.IsOfType(stream)) + { + return new SvgFile(stream); + } + + // We don't know + return null; + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ImageFileDirectory.cs b/src/Umbraco.Core/Media/Exif/ImageFileDirectory.cs index ed4564a486..299e7619f9 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFileDirectory.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFileDirectory.cs @@ -1,97 +1,100 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Represents an image file directory. +/// +internal class ImageFileDirectory { /// - /// Represents an image file directory. + /// Initializes a new instance of the class. /// - internal class ImageFileDirectory + public ImageFileDirectory() { - /// - /// The fields contained in this IFD. - /// - public List Fields { get; private set; } - /// - /// Offset to the next IFD. - /// - public uint NextIFDOffset { get; private set; } - /// - /// Compressed image data. - /// - public List Strips { get; private set; } + Fields = new List(); + Strips = new List(); + } - /// - /// Initializes a new instance of the class. - /// - public ImageFileDirectory() + /// + /// The fields contained in this IFD. + /// + public List Fields { get; } + + /// + /// Offset to the next IFD. + /// + public uint NextIFDOffset { get; private set; } + + /// + /// Compressed image data. + /// + public List Strips { get; } + + /// + /// Returns a initialized from the given byte data. + /// + /// The data. + /// The offset into . + /// The byte order of . + /// A initialized from the given byte data. + public static ImageFileDirectory FromBytes(byte[] data, uint offset, BitConverterEx.ByteOrder byteOrder) + { + var ifd = new ImageFileDirectory(); + var conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); + + var stripOffsets = new List(); + var stripLengths = new List(); + + // Count + var fieldcount = conv.ToUInt16(data, offset); + + // Read fields + for (uint i = 0; i < fieldcount; i++) { - Fields = new List(); - Strips = new List(); - } + var fieldoffset = offset + 2 + (12 * i); + var field = ImageFileDirectoryEntry.FromBytes(data, fieldoffset, byteOrder); + ifd.Fields.Add(field); - /// - /// Returns a initialized from the given byte data. - /// - /// The data. - /// The offset into . - /// The byte order of . - /// A initialized from the given byte data. - public static ImageFileDirectory FromBytes(byte[] data, uint offset, BitConverterEx.ByteOrder byteOrder) - { - ImageFileDirectory ifd = new ImageFileDirectory(); - BitConverterEx conv = new BitConverterEx(byteOrder, BitConverterEx.SystemByteOrder); - - List stripOffsets = new List(); - List stripLengths = new List(); - - // Count - ushort fieldcount = conv.ToUInt16(data, offset); - - // Read fields - for (uint i = 0; i < fieldcount; i++) + // Read strip offsets + if (field.Tag == 273) { - uint fieldoffset = offset + 2 + 12 * i; - ImageFileDirectoryEntry field = ImageFileDirectoryEntry.FromBytes(data, fieldoffset, byteOrder); - ifd.Fields.Add(field); - - // Read strip offsets - if (field.Tag == 273) + var baselen = field.Data.Length / (int)field.Count; + for (uint j = 0; j < field.Count; j++) { - int baselen = field.Data.Length / (int)field.Count; - for (uint j = 0; j < field.Count; j++) - { - byte[] val = new byte[baselen]; - Array.Copy(field.Data, j * baselen, val, 0, baselen); - uint stripOffset = (field.Type == 3 ? (uint)BitConverter.ToUInt16(val, 0) : BitConverter.ToUInt32(val, 0)); - stripOffsets.Add(stripOffset); - } - } - - // Read strip lengths - if (field.Tag == 279) - { - int baselen = field.Data.Length / (int)field.Count; - for (uint j = 0; j < field.Count; j++) - { - byte[] val = new byte[baselen]; - Array.Copy(field.Data, j * baselen, val, 0, baselen); - uint stripLength = (field.Type == 3 ? (uint)BitConverter.ToUInt16(val, 0) : BitConverter.ToUInt32(val, 0)); - stripLengths.Add(stripLength); - } + var val = new byte[baselen]; + Array.Copy(field.Data, j * baselen, val, 0, baselen); + var stripOffset = field.Type == 3 ? BitConverter.ToUInt16(val, 0) : BitConverter.ToUInt32(val, 0); + stripOffsets.Add(stripOffset); } } - // Save strips - if (stripOffsets.Count != stripLengths.Count) - throw new NotValidTIFFileException(); - for (int i = 0; i < stripOffsets.Count; i++) - ifd.Strips.Add(new TIFFStrip(data, stripOffsets[i], stripLengths[i])); - - // Offset to next ifd - ifd.NextIFDOffset = conv.ToUInt32(data, offset + 2 + 12 * fieldcount); - - return ifd; + // Read strip lengths + if (field.Tag == 279) + { + var baselen = field.Data.Length / (int)field.Count; + for (uint j = 0; j < field.Count; j++) + { + var val = new byte[baselen]; + Array.Copy(field.Data, j * baselen, val, 0, baselen); + var stripLength = field.Type == 3 ? BitConverter.ToUInt16(val, 0) : BitConverter.ToUInt32(val, 0); + stripLengths.Add(stripLength); + } + } } + + // Save strips + if (stripOffsets.Count != stripLengths.Count) + { + throw new NotValidTIFFileException(); + } + + for (var i = 0; i < stripOffsets.Count; i++) + { + ifd.Strips.Add(new TIFFStrip(data, stripOffsets[i], stripLengths[i])); + } + + // Offset to next ifd + ifd.NextIFDOffset = conv.ToUInt32(data, offset + 2 + (12 * fieldcount)); + + return ifd; } } diff --git a/src/Umbraco.Core/Media/Exif/ImageFileDirectoryEntry.cs b/src/Umbraco.Core/Media/Exif/ImageFileDirectoryEntry.cs index 7d1568afb3..a3863b6a69 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFileDirectoryEntry.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFileDirectoryEntry.cs @@ -1,117 +1,144 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Represents an entry in the image file directory. +/// +internal struct ImageFileDirectoryEntry { /// - /// Represents an entry in the image file directory. + /// The tag that identifies the field. /// - internal struct ImageFileDirectoryEntry + public ushort Tag; + + /// + /// Field type identifier. + /// + public ushort Type; + + /// + /// Count of Type. + /// + public uint Count; + + /// + /// Field data. + /// + public byte[] Data; + + /// + /// Initializes a new instance of the struct. + /// + /// The tag that identifies the field. + /// Field type identifier. + /// Count of Type. + /// Field data. + public ImageFileDirectoryEntry(ushort tag, ushort type, uint count, byte[] data) { - /// - /// The tag that identifies the field. - /// - public ushort Tag; - /// - /// Field type identifier. - /// - public ushort Type; - /// - /// Count of Type. - /// - public uint Count; - /// - /// Field data. - /// - public byte[] Data; + Tag = tag; + Type = type; + Count = count; + Data = data; + } - /// - /// Initializes a new instance of the struct. - /// - /// The tag that identifies the field. - /// Field type identifier. - /// Count of Type. - /// Field data. - public ImageFileDirectoryEntry(ushort tag, ushort type, uint count, byte[] data) + /// + /// Returns a initialized from the given byte data. + /// + /// The data. + /// The offset into . + /// The byte order of . + /// A initialized from the given byte data. + public static ImageFileDirectoryEntry FromBytes(byte[] data, uint offset, BitConverterEx.ByteOrder byteOrder) + { + // Tag ID + var tag = BitConverterEx.ToUInt16(data, offset, byteOrder, BitConverterEx.SystemByteOrder); + + // Tag Type + var type = BitConverterEx.ToUInt16(data, offset + 2, byteOrder, BitConverterEx.SystemByteOrder); + + // Count of Type + var count = BitConverterEx.ToUInt32(data, offset + 4, byteOrder, BitConverterEx.SystemByteOrder); + + // Field value or offset to field data + var value = new byte[4]; + Array.Copy(data, offset + 8, value, 0, 4); + + // Calculate the bytes we need to read + var baselength = GetBaseLength(type); + var totallength = count * baselength; + + // If field value does not fit in 4 bytes + // the value field is an offset to the actual + // field value + if (totallength > 4) { - Tag = tag; - Type = type; - Count = count; - Data = data; + var dataoffset = BitConverterEx.ToUInt32(value, 0, byteOrder, BitConverterEx.SystemByteOrder); + value = new byte[totallength]; + Array.Copy(data, dataoffset, value, 0, totallength); } - /// - /// Returns a initialized from the given byte data. - /// - /// The data. - /// The offset into . - /// The byte order of . - /// A initialized from the given byte data. - public static ImageFileDirectoryEntry FromBytes(byte[] data, uint offset, BitConverterEx.ByteOrder byteOrder) + // Reverse array order if byte orders are different + if (byteOrder != BitConverterEx.SystemByteOrder) { - // Tag ID - ushort tag = BitConverterEx.ToUInt16(data, offset, byteOrder, BitConverterEx.SystemByteOrder); - - // Tag Type - ushort type = BitConverterEx.ToUInt16(data, offset + 2, byteOrder, BitConverterEx.SystemByteOrder); - - // Count of Type - uint count = BitConverterEx.ToUInt32(data, offset + 4, byteOrder, BitConverterEx.SystemByteOrder); - - // Field value or offset to field data - byte[] value = new byte[4]; - Array.Copy(data, offset + 8, value, 0, 4); - - // Calculate the bytes we need to read - uint baselength = GetBaseLength(type); - uint totallength = count * baselength; - - // If field value does not fit in 4 bytes - // the value field is an offset to the actual - // field value - if (totallength > 4) + for (uint i = 0; i < count; i++) { - uint dataoffset = BitConverterEx.ToUInt32(value, 0, byteOrder, BitConverterEx.SystemByteOrder); - value = new byte[totallength]; - Array.Copy(data, dataoffset, value, 0, totallength); + var val = new byte[baselength]; + Array.Copy(value, i * baselength, val, 0, baselength); + Array.Reverse(val); + Array.Copy(val, 0, value, i * baselength, baselength); } - - // Reverse array order if byte orders are different - if (byteOrder != BitConverterEx.SystemByteOrder) - { - for (uint i = 0; i < count; i++) - { - byte[] val = new byte[baselength]; - Array.Copy(value, i * baselength, val, 0, baselength); - Array.Reverse(val); - Array.Copy(val, 0, value, i * baselength, baselength); - } - } - - return new ImageFileDirectoryEntry(tag, type, count, value); } - /// - /// Gets the base byte length for the given type. - /// - /// Type identifier. - private static uint GetBaseLength(ushort type) + return new ImageFileDirectoryEntry(tag, type, count, value); + } + + /// + /// Gets the base byte length for the given type. + /// + /// Type identifier. + private static uint GetBaseLength(ushort type) + { + // BYTE and SBYTE + if (type == 1 || type == 6) { - if (type == 1 || type == 6) // BYTE and SBYTE - return 1; - else if (type == 2 || type == 7) // ASCII and UNDEFINED - return 1; - else if (type == 3 || type == 8) // SHORT and SSHORT - return 2; - else if (type == 4 || type == 9) // LONG and SLONG - return 4; - else if (type == 5 || type == 10) // RATIONAL (2xLONG) and SRATIONAL (2xSLONG) - return 8; - else if (type == 11) // FLOAT - return 4; - else if (type == 12) // DOUBLE - return 8; - - throw new ArgumentException("Unknown type identifier.", "type"); + return 1; } + + // ASCII and UNDEFINED + if (type == 2 || type == 7) + { + return 1; + } + + // SHORT and SSHORT + if (type == 3 || type == 8) + { + return 2; + } + + // LONG and SLONG + if (type == 4 || type == 9) + { + return 4; + } + + // RATIONAL (2xLONG) and SRATIONAL (2xSLONG) + if (type == 5 || type == 10) + { + return 8; + } + + // FLOAT + if (type == 11) + { + return 4; + } + + // DOUBLE + if (type == 12) + { + return 8; + } + + throw new ArgumentException("Unknown type identifier.", "type"); } } diff --git a/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs b/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs index 09cfcce589..fe30c713b2 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs @@ -1,25 +1,27 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the format of the . +/// +internal enum ImageFileFormat { /// - /// Represents the format of the . + /// The file is not recognized. /// - internal enum ImageFileFormat - { - /// - /// The file is not recognized. - /// - Unknown, - /// - /// The file is a JPEG/Exif or JPEG/JFIF file. - /// - JPEG, - /// - /// The file is a TIFF File. - /// - TIFF, - /// - /// The file is a SVG File. - /// - SVG, - } + Unknown, + + /// + /// The file is a JPEG/Exif or JPEG/JFIF file. + /// + JPEG, + + /// + /// The file is a TIFF File. + /// + TIFF, + + /// + /// The file is a SVG File. + /// + SVG, } diff --git a/src/Umbraco.Core/Media/Exif/JFIFEnums.cs b/src/Umbraco.Core/Media/Exif/JFIFEnums.cs index ff6b0463ed..438d7bf3d4 100644 --- a/src/Umbraco.Core/Media/Exif/JFIFEnums.cs +++ b/src/Umbraco.Core/Media/Exif/JFIFEnums.cs @@ -1,40 +1,44 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the units for the X and Y densities +/// for a JFIF file. +/// +internal enum JFIFDensityUnit : byte { /// - /// Represents the units for the X and Y densities - /// for a JFIF file. + /// No units, XDensity and YDensity specify the pixel aspect ratio. /// - internal enum JFIFDensityUnit : byte - { - /// - /// No units, XDensity and YDensity specify the pixel aspect ratio. - /// - None = 0, - /// - /// XDensity and YDensity are dots per inch. - /// - DotsPerInch = 1, - /// - /// XDensity and YDensity are dots per cm. - /// - DotsPerCm = 2, - } + None = 0, + /// - /// Represents the JFIF extension. + /// XDensity and YDensity are dots per inch. /// - internal enum JFIFExtension : byte - { - /// - /// Thumbnail coded using JPEG. - /// - ThumbnailJPEG = 0x10, - /// - /// Thumbnail stored using a 256-Color RGB palette. - /// - ThumbnailPaletteRGB = 0x11, - /// - /// Thumbnail stored using 3 bytes/pixel (24-bit) RGB values. - /// - Thumbnail24BitRGB = 0x13, - } + DotsPerInch = 1, + + /// + /// XDensity and YDensity are dots per cm. + /// + DotsPerCm = 2, +} + +/// +/// Represents the JFIF extension. +/// +internal enum JFIFExtension : byte +{ + /// + /// Thumbnail coded using JPEG. + /// + ThumbnailJPEG = 0x10, + + /// + /// Thumbnail stored using a 256-Color RGB palette. + /// + ThumbnailPaletteRGB = 0x11, + + /// + /// Thumbnail stored using 3 bytes/pixel (24-bit) RGB values. + /// + Thumbnail24BitRGB = 0x13, } diff --git a/src/Umbraco.Core/Media/Exif/JFIFExtendedProperty.cs b/src/Umbraco.Core/Media/Exif/JFIFExtendedProperty.cs index d3a0e7fb46..71ea89228d 100644 --- a/src/Umbraco.Core/Media/Exif/JFIFExtendedProperty.cs +++ b/src/Umbraco.Core/Media/Exif/JFIFExtendedProperty.cs @@ -1,67 +1,76 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Represents the JFIF version as a 16 bit unsigned integer. (EXIF Specification: SHORT) +/// +internal class JFIFVersion : ExifUShort { - /// - /// Represents the JFIF version as a 16 bit unsigned integer. (EXIF Specification: SHORT) - /// - internal class JFIFVersion : ExifUShort + public JFIFVersion(ExifTag tag, ushort value) + : base(tag, value) { - /// - /// Gets the major version. - /// - public byte Major { get { return (byte)(mValue >> 8); } } - /// - /// Gets the minor version. - /// - public byte Minor { get { return (byte)(mValue - (mValue >> 8) * 256); } } - - public JFIFVersion(ExifTag tag, ushort value) - : base(tag, value) - { - - } - - public override string ToString() - { - return string.Format("{0}.{1:00}", Major, Minor); - } } + /// - /// Represents a JFIF thumbnail. (EXIF Specification: BYTE) + /// Gets the major version. /// - internal class JFIFThumbnailProperty : ExifProperty - { - protected JFIFThumbnail mValue; - protected override object _Value { get { return Value; } set { Value = (JFIFThumbnail)value; } } - public new JFIFThumbnail Value { get { return mValue; } set { mValue = value; } } + public byte Major => (byte)(mValue >> 8); - public override string ToString() { return mValue.Format.ToString(); } + /// + /// Gets the minor version. + /// + public byte Minor => (byte)(mValue - ((mValue >> 8) * 256)); - public JFIFThumbnailProperty(ExifTag tag, JFIFThumbnail value) - : base(tag) - { - mValue = value; - } - - public override ExifInterOperability Interoperability - { - get - { - if (mValue.Format == JFIFThumbnail.ImageFormat.BMP24Bit) - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); - else if (mValue.Format == JFIFThumbnail.ImageFormat.BMPPalette) - { - byte[] data = new byte[mValue.Palette.Length + mValue.PixelData.Length]; - Array.Copy(mValue.Palette, data, mValue.Palette.Length); - Array.Copy(mValue.PixelData, 0, data, mValue.Palette.Length, mValue.PixelData.Length); - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); - } - else if (mValue.Format == JFIFThumbnail.ImageFormat.JPEG) - return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); - else - throw new InvalidOperationException("Unknown thumbnail type."); - } - } - } + public override string ToString() => string.Format("{0}.{1:00}", Major, Minor); +} + +/// +/// Represents a JFIF thumbnail. (EXIF Specification: BYTE) +/// +internal class JFIFThumbnailProperty : ExifProperty +{ + protected JFIFThumbnail mValue; + + public JFIFThumbnailProperty(ExifTag tag, JFIFThumbnail value) + : base(tag) => + mValue = value; + + public new JFIFThumbnail Value + { + get => mValue; + set => mValue = value; + } + + protected override object _Value + { + get => Value; + set => Value = (JFIFThumbnail)value; + } + + public override ExifInterOperability Interoperability + { + get + { + if (mValue.Format == JFIFThumbnail.ImageFormat.BMP24Bit) + { + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); + } + + if (mValue.Format == JFIFThumbnail.ImageFormat.BMPPalette) + { + var data = new byte[mValue.Palette.Length + mValue.PixelData.Length]; + Array.Copy(mValue.Palette, data, mValue.Palette.Length); + Array.Copy(mValue.PixelData, 0, data, mValue.Palette.Length, mValue.PixelData.Length); + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)data.Length, data); + } + + if (mValue.Format == JFIFThumbnail.ImageFormat.JPEG) + { + return new ExifInterOperability(ExifTagFactory.GetTagID(mTag), 1, (uint)mValue.PixelData.Length, mValue.PixelData); + } + + throw new InvalidOperationException("Unknown thumbnail type."); + } + } + + public override string ToString() => mValue.Format.ToString(); } diff --git a/src/Umbraco.Core/Media/Exif/JFIFThumbnail.cs b/src/Umbraco.Core/Media/Exif/JFIFThumbnail.cs index de9fe8f76f..cafa804c3a 100644 --- a/src/Umbraco.Core/Media/Exif/JFIFThumbnail.cs +++ b/src/Umbraco.Core/Media/Exif/JFIFThumbnail.cs @@ -1,55 +1,62 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents a JFIF thumbnail. +/// +internal class JFIFThumbnail { - /// - /// Represents a JFIF thumbnail. - /// - internal class JFIFThumbnail + #region Public Enums + + public enum ImageFormat { - #region Properties - /// - /// Gets the 256 color RGB palette. - /// - public byte[] Palette { get; private set; } - /// - /// Gets raw image data. - /// - public byte[] PixelData { get; private set; } - /// - /// Gets the image format. - /// - public ImageFormat Format { get; private set; } - #endregion - - #region Public Enums - public enum ImageFormat - { - JPEG, - BMPPalette, - BMP24Bit, - } - #endregion - - #region Constructors - protected JFIFThumbnail() - { - Palette = new byte[0]; - PixelData = new byte[0]; - } - - public JFIFThumbnail(ImageFormat format, byte[] data) - : this() - { - Format = format; - PixelData = data; - } - - public JFIFThumbnail(byte[] palette, byte[] data) - : this() - { - Format = ImageFormat.BMPPalette; - Palette = palette; - PixelData = data; - } - #endregion + JPEG, + BMPPalette, + BMP24Bit, } + + #endregion + + #region Properties + + /// + /// Gets the 256 color RGB palette. + /// + public byte[] Palette { get; } + + /// + /// Gets raw image data. + /// + public byte[] PixelData { get; } + + /// + /// Gets the image format. + /// + public ImageFormat Format { get; } + + #endregion + + #region Constructors + + protected JFIFThumbnail() + { + Palette = new byte[0]; + PixelData = new byte[0]; + } + + public JFIFThumbnail(ImageFormat format, byte[] data) + : this() + { + Format = format; + PixelData = data; + } + + public JFIFThumbnail(byte[] palette, byte[] data) + : this() + { + Format = ImageFormat.BMPPalette; + Palette = palette; + PixelData = data; + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/JPEGExceptions.cs b/src/Umbraco.Core/Media/Exif/JPEGExceptions.cs index dde0326f99..c44d6d1db0 100644 --- a/src/Umbraco.Core/Media/Exif/JPEGExceptions.cs +++ b/src/Umbraco.Core/Media/Exif/JPEGExceptions.cs @@ -1,171 +1,219 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// The exception that is thrown when the format of the JPEG file could not be understood. +/// +/// +[Serializable] +public class NotValidJPEGFileException : Exception { - - /// - /// The exception that is thrown when the format of the JPEG file could not be understood. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class NotValidJPEGFileException : Exception + public NotValidJPEGFileException() + : base("Not a valid JPEG file.") { - /// - /// Initializes a new instance of the class. - /// - public NotValidJPEGFileException() - : base("Not a valid JPEG file.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public NotValidJPEGFileException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotValidJPEGFileException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected NotValidJPEGFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } /// - /// The exception that is thrown when the format of the TIFF file could not be understood. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class NotValidTIFFileException : Exception + /// The message that describes the error. + public NotValidJPEGFileException(string message) + : base(message) { - /// - /// Initializes a new instance of the class. - /// - public NotValidTIFFileException() - : base("Not a valid TIFF file.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public NotValidTIFFileException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotValidTIFFileException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected NotValidTIFFileException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } /// - /// The exception that is thrown when the format of the TIFF header could not be understood. + /// Initializes a new instance of the class. /// - /// - [Serializable] - internal class NotValidTIFFHeader : Exception + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public NotValidJPEGFileException(string message, Exception innerException) + : base(message, innerException) { - /// - /// Initializes a new instance of the class. - /// - public NotValidTIFFHeader() - : base("Not a valid TIFF header.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public NotValidTIFFHeader(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public NotValidTIFFHeader(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected NotValidTIFFHeader(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } /// - /// The exception that is thrown when the length of a section exceeds 64 kB. + /// Initializes a new instance of the class. /// - /// - [Serializable] - public class SectionExceeds64KBException : Exception + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected NotValidJPEGFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} + +/// +/// The exception that is thrown when the format of the TIFF file could not be understood. +/// +/// +[Serializable] +public class NotValidTIFFileException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public NotValidTIFFileException() + : base("Not a valid TIFF file.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public NotValidTIFFileException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public NotValidTIFFileException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected NotValidTIFFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} + +/// +/// The exception that is thrown when the length of a section exceeds 64 kB. +/// +/// +[Serializable] +public class SectionExceeds64KBException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public SectionExceeds64KBException() + : base("Section length exceeds 64 kB.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public SectionExceeds64KBException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public SectionExceeds64KBException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected SectionExceeds64KBException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +} + +/// +/// The exception that is thrown when the format of the TIFF header could not be understood. +/// +/// +[Serializable] +internal class NotValidTIFFHeader : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public NotValidTIFFHeader() + : base("Not a valid TIFF header.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public NotValidTIFFHeader(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public NotValidTIFFHeader(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected NotValidTIFFHeader(SerializationInfo info, StreamingContext context) + : base(info, context) { - /// - /// Initializes a new instance of the class. - /// - public SectionExceeds64KBException() - : base("Section length exceeds 64 kB.") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public SectionExceeds64KBException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public SectionExceeds64KBException(string message, Exception innerException) - : base(message, innerException) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected SectionExceeds64KBException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } } diff --git a/src/Umbraco.Core/Media/Exif/JPEGFile.cs b/src/Umbraco.Core/Media/Exif/JPEGFile.cs index f0f732b520..bdf7208ea0 100644 --- a/src/Umbraco.Core/Media/Exif/JPEGFile.cs +++ b/src/Umbraco.Core/Media/Exif/JPEGFile.cs @@ -1,924 +1,1110 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the binary view of a JPEG compressed file. +/// +internal class JPEGFile : ImageFile { + #region Constructor + /// - /// Represents the binary view of a JPEG compressed file. + /// Initializes a new instance of the class. /// - internal class JPEGFile : ImageFile + /// A that contains image data. + /// The encoding to be used for text metadata when the source encoding is unknown. + protected internal JPEGFile(Stream stream, Encoding encoding) { - #region Member Variables - private JPEGSection? jfifApp0; - private JPEGSection? jfxxApp0; - private JPEGSection? exifApp1; - private uint makerNoteOffset; - private long exifIFDFieldOffset, gpsIFDFieldOffset, interopIFDFieldOffset, firstIFDFieldOffset; - private long thumbOffsetLocation, thumbSizeLocation; - private uint thumbOffsetValue, thumbSizeValue; - private bool makerNoteProcessed; - #endregion + Format = ImageFileFormat.JPEG; + Sections = new List(); + TrailingData = new byte[0]; + Encoding = encoding; - #region Properties - /// - /// Gets or sets the byte-order of the Exif properties. - /// - public BitConverterEx.ByteOrder ByteOrder { get; set; } - /// - /// Gets or sets the sections contained in the . - /// - public List Sections { get; private set; } - /// - /// Gets or sets non-standard trailing data following the End of Image (EOI) marker. - /// - public byte[] TrailingData { get; private set; } - #endregion + stream.Seek(0, SeekOrigin.Begin); - #region Constructor - /// - /// Initializes a new instance of the class. - /// - /// A that contains image data. - /// The encoding to be used for text metadata when the source encoding is unknown. - protected internal JPEGFile(Stream stream, Encoding encoding) + // Read the Start of Image (SOI) marker. SOI marker is represented + // with two bytes: 0xFF, 0xD8. + var markerbytes = new byte[2]; + if (stream.Read(markerbytes, 0, 2) != 2 || markerbytes[0] != 0xFF || markerbytes[1] != 0xD8) { - Format = ImageFileFormat.JPEG; - Sections = new List(); - TrailingData = new byte[0]; - Encoding = encoding; + throw new NotValidJPEGFileException(); + } - stream.Seek(0, SeekOrigin.Begin); + stream.Seek(0, SeekOrigin.Begin); - // Read the Start of Image (SOI) marker. SOI marker is represented - // with two bytes: 0xFF, 0xD8. - byte[] markerbytes = new byte[2]; - if (stream.Read(markerbytes, 0, 2) != 2 || markerbytes[0] != 0xFF || markerbytes[1] != 0xD8) - throw new NotValidJPEGFileException(); - stream.Seek(0, SeekOrigin.Begin); - - // Search and read sections until we reach the end of file. - while (stream.Position != stream.Length) + // Search and read sections until we reach the end of file. + while (stream.Position != stream.Length) + { + // Read the next section marker. Section markers are two bytes + // with values 0xFF, 0x?? where ?? must not be 0x00 or 0xFF. + if (stream.Read(markerbytes, 0, 2) != 2 || markerbytes[0] != 0xFF || markerbytes[1] == 0x00 || + markerbytes[1] == 0xFF) { - // Read the next section marker. Section markers are two bytes - // with values 0xFF, 0x?? where ?? must not be 0x00 or 0xFF. - if (stream.Read(markerbytes, 0, 2) != 2 || markerbytes[0] != 0xFF || markerbytes[1] == 0x00 || markerbytes[1] == 0xFF) - throw new NotValidJPEGFileException(); + throw new NotValidJPEGFileException(); + } - JPEGMarker marker = (JPEGMarker)markerbytes[1]; + var marker = (JPEGMarker)markerbytes[1]; - byte[] header = new byte[0]; - // SOI, EOI and RST markers do not contain any header - if (marker != JPEGMarker.SOI && marker != JPEGMarker.EOI && !(marker >= JPEGMarker.RST0 && marker <= JPEGMarker.RST7)) + var header = new byte[0]; + + // SOI, EOI and RST markers do not contain any header + if (marker != JPEGMarker.SOI && marker != JPEGMarker.EOI && + !(marker >= JPEGMarker.RST0 && marker <= JPEGMarker.RST7)) + { + // Length of the header including the length bytes. + // This value is a 16-bit unsigned integer + // in big endian byte-order. + var lengthbytes = new byte[2]; + if (stream.Read(lengthbytes, 0, 2) != 2) { - // Length of the header including the length bytes. - // This value is a 16-bit unsigned integer - // in big endian byte-order. - byte[] lengthbytes = new byte[2]; - if (stream.Read(lengthbytes, 0, 2) != 2) - throw new NotValidJPEGFileException(); - long length = (long)BitConverterEx.BigEndian.ToUInt16(lengthbytes, 0); - - // Read section header. - header = new byte[length - 2]; - int bytestoread = header.Length; - while (bytestoread > 0) - { - int count = Math.Min(bytestoread, 4 * 1024); - int bytesread = stream.Read(header, header.Length - bytestoread, count); - if (bytesread == 0) - throw new NotValidJPEGFileException(); - bytestoread -= bytesread; - } + throw new NotValidJPEGFileException(); } - // Start of Scan (SOS) sections and RST sections are immediately - // followed by entropy coded data. For that, we need to read until - // the next section marker once we reach a SOS or RST. - byte[] entropydata = new byte[0]; - if (marker == JPEGMarker.SOS || (marker >= JPEGMarker.RST0 && marker <= JPEGMarker.RST7)) + long length = BitConverterEx.BigEndian.ToUInt16(lengthbytes, 0); + + // Read section header. + header = new byte[length - 2]; + var bytestoread = header.Length; + while (bytestoread > 0) { - long position = stream.Position; - - // Search for the next section marker - while (true) + var count = Math.Min(bytestoread, 4 * 1024); + var bytesread = stream.Read(header, header.Length - bytestoread, count); + if (bytesread == 0) { - // Search for an 0xFF indicating start of a marker - int nextbyte = 0; - do - { - nextbyte = stream.ReadByte(); - if (nextbyte == -1) - throw new NotValidJPEGFileException(); - } while ((byte)nextbyte != 0xFF); + throw new NotValidJPEGFileException(); + } - // Skip filler bytes (0xFF) - do - { - nextbyte = stream.ReadByte(); - if (nextbyte == -1) - throw new NotValidJPEGFileException(); - } while ((byte)nextbyte == 0xFF); + bytestoread -= bytesread; + } + } - // Looks like a section marker. The next byte must not be 0x00. - if ((byte)nextbyte != 0x00) - { - // We reached a section marker. Calculate the - // length of the entropy coded data. - stream.Seek(-2, SeekOrigin.Current); - long edlength = stream.Position - position; - stream.Seek(-edlength, SeekOrigin.Current); + // Start of Scan (SOS) sections and RST sections are immediately + // followed by entropy coded data. For that, we need to read until + // the next section marker once we reach a SOS or RST. + var entropydata = new byte[0]; + if (marker == JPEGMarker.SOS || (marker >= JPEGMarker.RST0 && marker <= JPEGMarker.RST7)) + { + var position = stream.Position; - // Read entropy coded data - entropydata = new byte[edlength]; - int bytestoread = entropydata.Length; - while (bytestoread > 0) + // Search for the next section marker + while (true) + { + // Search for an 0xFF indicating start of a marker + var nextbyte = 0; + do + { + nextbyte = stream.ReadByte(); + if (nextbyte == -1) + { + throw new NotValidJPEGFileException(); + } + } + while ((byte)nextbyte != 0xFF); + + // Skip filler bytes (0xFF) + do + { + nextbyte = stream.ReadByte(); + if (nextbyte == -1) + { + throw new NotValidJPEGFileException(); + } + } + while ((byte)nextbyte == 0xFF); + + // Looks like a section marker. The next byte must not be 0x00. + if ((byte)nextbyte != 0x00) + { + // We reached a section marker. Calculate the + // length of the entropy coded data. + stream.Seek(-2, SeekOrigin.Current); + var edlength = stream.Position - position; + stream.Seek(-edlength, SeekOrigin.Current); + + // Read entropy coded data + entropydata = new byte[edlength]; + var bytestoread = entropydata.Length; + while (bytestoread > 0) + { + var count = Math.Min(bytestoread, 4 * 1024); + var bytesread = stream.Read(entropydata, entropydata.Length - bytestoread, count); + if (bytesread == 0) { - int count = Math.Min(bytestoread, 4 * 1024); - int bytesread = stream.Read(entropydata, entropydata.Length - bytestoread, count); - if (bytesread == 0) - throw new NotValidJPEGFileException(); - bytestoread -= bytesread; + throw new NotValidJPEGFileException(); } - break; + bytestoread -= bytesread; } + + break; } } + } - // Store section. - JPEGSection section = new JPEGSection(marker, header, entropydata); - Sections.Add(section); + // Store section. + var section = new JPEGSection(marker, header, entropydata); + Sections.Add(section); - // Some propriety formats store data past the EOI marker - if (marker == JPEGMarker.EOI) + // Some propriety formats store data past the EOI marker + if (marker == JPEGMarker.EOI) + { + var bytestoread = (int)(stream.Length - stream.Position); + TrailingData = new byte[bytestoread]; + while (bytestoread > 0) { - int bytestoread = (int)(stream.Length - stream.Position); - TrailingData = new byte[bytestoread]; - while (bytestoread > 0) + var count = Math.Min(bytestoread, 4 * 1024); + var bytesread = stream.Read(TrailingData, TrailingData.Length - bytestoread, count); + if (bytesread == 0) { - int count = (int)Math.Min(bytestoread, 4 * 1024); - int bytesread = stream.Read(TrailingData, TrailingData.Length - bytestoread, count); - if (bytesread == 0) - throw new NotValidJPEGFileException(); - bytestoread -= bytesread; + throw new NotValidJPEGFileException(); } + + bytestoread -= bytesread; } } - - // Read metadata sections - ReadJFIFAPP0(); - ReadJFXXAPP0(); - ReadExifAPP1(); - - // Process the maker note - makerNoteProcessed = false; } - #endregion - #region Instance Methods - /// - /// Saves the JPEG/Exif image to the given stream. - /// - /// The path to the JPEG/Exif file. - /// Determines whether the maker note offset of - /// the original file will be preserved. - public void Save(Stream stream, bool preserveMakerNote) + // Read metadata sections + ReadJFIFAPP0(); + ReadJFXXAPP0(); + ReadExifAPP1(); + + // Process the maker note + _makerNoteProcessed = false; + } + + #endregion + + #region Member Variables + + private JPEGSection? _jfifApp0; + private JPEGSection? _jfxxApp0; + private JPEGSection? _exifApp1; + private uint _makerNoteOffset; + private long _exifIfdFieldOffset; + private long _gpsIfdFieldOffset; + private long _interopIfdFieldOffset; + private long _firstIfdFieldOffset; + private long _thumbOffsetLocation; + private long _thumbSizeLocation; + private uint _thumbOffsetValue; + private uint _thumbSizeValue; + private readonly bool _makerNoteProcessed; + + #endregion + + #region Properties + + /// + /// Gets or sets the byte-order of the Exif properties. + /// + public BitConverterEx.ByteOrder ByteOrder { get; set; } + + /// + /// Gets or sets the sections contained in the . + /// + public List Sections { get; } + + /// + /// Gets or sets non-standard trailing data following the End of Image (EOI) marker. + /// + public byte[] TrailingData { get; } + + #endregion + + #region Instance Methods + + /// + /// Saves the JPEG/Exif image to the given stream. + /// + /// The stream of the JPEG/Exif file. + /// + /// Determines whether the maker note offset of + /// the original file will be preserved. + /// + public void Save(Stream stream, bool preserveMakerNote) + { + WriteJFIFApp0(); + WriteJFXXApp0(); + WriteExifApp1(preserveMakerNote); + + // Write sections + foreach (JPEGSection section in Sections) { - WriteJFIFApp0(); - WriteJFXXApp0(); - WriteExifApp1(preserveMakerNote); - - // Write sections - foreach (JPEGSection section in Sections) + // Section header (including length bytes and section marker) + // must not exceed 64 kB. + if (section.Header.Length + 2 + 2 > 64 * 1024) { - // Section header (including length bytes and section marker) - // must not exceed 64 kB. - if (section.Header.Length + 2 + 2 > 64 * 1024) - throw new SectionExceeds64KBException(); + throw new SectionExceeds64KBException(); + } - // APP sections must have a header. - // Otherwise skip the entire section. - if (section.Marker >= JPEGMarker.APP0 && section.Marker <= JPEGMarker.APP15 && section.Header.Length == 0) - continue; + // APP sections must have a header. + // Otherwise skip the entire section. + if (section.Marker >= JPEGMarker.APP0 && section.Marker <= JPEGMarker.APP15 && section.Header.Length == 0) + { + continue; + } - // Write section marker - stream.Write(new byte[] { 0xFF, (byte)section.Marker }, 0, 2); + // Write section marker + stream.Write(new byte[] { 0xFF, (byte)section.Marker }, 0, 2); - // SOI, EOI and RST markers do not contain any header - if (section.Marker != JPEGMarker.SOI && section.Marker != JPEGMarker.EOI && !(section.Marker >= JPEGMarker.RST0 && section.Marker <= JPEGMarker.RST7)) + // SOI, EOI and RST markers do not contain any header + if (section.Marker != JPEGMarker.SOI && section.Marker != JPEGMarker.EOI && + !(section.Marker >= JPEGMarker.RST0 && section.Marker <= JPEGMarker.RST7)) + { + // Header length including the length field itself + stream.Write(BitConverterEx.BigEndian.GetBytes((ushort)(section.Header.Length + 2)), 0, 2); + + // Write section header + if (section.Header.Length != 0) { - // Header length including the length field itself - stream.Write(BitConverterEx.BigEndian.GetBytes((ushort)(section.Header.Length + 2)), 0, 2); - - // Write section header - if (section.Header.Length != 0) - stream.Write(section.Header, 0, section.Header.Length); + stream.Write(section.Header, 0, section.Header.Length); } - - // Write entropy coded data - if (section.EntropyData.Length != 0) - stream.Write(section.EntropyData, 0, section.EntropyData.Length); } - // Write trailing data, if any - if (TrailingData.Length != 0) - stream.Write(TrailingData, 0, TrailingData.Length); - } - - /// - /// Saves the JPEG/Exif image with the given filename. - /// - /// The path to the JPEG/Exif file. - /// Determines whether the maker note offset of - /// the original file will be preserved. - public void Save(string filename, bool preserveMakerNote) - { - using (FileStream stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + // Write entropy coded data + if (section.EntropyData.Length != 0) { - Save(stream, preserveMakerNote); + stream.Write(section.EntropyData, 0, section.EntropyData.Length); } } - /// - /// Saves the JPEG/Exif image with the given filename. - /// - /// The path to the JPEG/Exif file. - public override void Save(string filename) + // Write trailing data, if any + if (TrailingData.Length != 0) { - Save(filename, true); + stream.Write(TrailingData, 0, TrailingData.Length); + } + } + + /// + /// Saves the JPEG/Exif image with the given filename. + /// + /// The path to the JPEG/Exif file. + /// + /// Determines whether the maker note offset of + /// the original file will be preserved. + /// + public void Save(string filename, bool preserveMakerNote) + { + using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + { + Save(stream, preserveMakerNote); + } + } + + /// + /// Saves the JPEG/Exif image with the given filename. + /// + /// The path to the JPEG/Exif file. + public override void Save(string filename) => Save(filename, true); + + /// + /// Saves the JPEG/Exif image to the given stream. + /// + /// The stream of the JPEG/Exif file. + public override void Save(Stream stream) => Save(stream, true); + + #endregion + + #region Private Helper Methods + + /// + /// Reads the APP0 section containing JFIF metadata. + /// + private void ReadJFIFAPP0() + { + // Find the APP0 section containing JFIF metadata + _jfifApp0 = Sections.Find(a => a.Marker == JPEGMarker.APP0 && + a.Header.Length >= 5 && + Encoding.ASCII.GetString(a.Header, 0, 5) == "JFIF\0"); + + // If there is no APP0 section, return. + if (_jfifApp0 == null) + { + return; } - /// - /// Saves the JPEG/Exif image to the given stream. - /// - /// The path to the JPEG/Exif file. - public override void Save(Stream stream) + var header = _jfifApp0.Header; + BitConverterEx jfifConv = BitConverterEx.BigEndian; + + // Version + var version = jfifConv.ToUInt16(header, 5); + Properties.Add(new JFIFVersion(ExifTag.JFIFVersion, version)); + + // Units + var unit = header[7]; + Properties.Add(new ExifEnumProperty(ExifTag.JFIFUnits, (JFIFDensityUnit)unit)); + + // X and Y densities + var xdensity = jfifConv.ToUInt16(header, 8); + Properties.Add(new ExifUShort(ExifTag.XDensity, xdensity)); + var ydensity = jfifConv.ToUInt16(header, 10); + Properties.Add(new ExifUShort(ExifTag.YDensity, ydensity)); + + // Thumbnails pixel count + var xthumbnail = header[12]; + Properties.Add(new ExifByte(ExifTag.JFIFXThumbnail, xthumbnail)); + var ythumbnail = header[13]; + Properties.Add(new ExifByte(ExifTag.JFIFYThumbnail, ythumbnail)); + + // Read JFIF thumbnail + var n = xthumbnail * ythumbnail; + var jfifThumbnail = new byte[n]; + Array.Copy(header, 14, jfifThumbnail, 0, n); + Properties.Add(new JFIFThumbnailProperty(ExifTag.JFIFThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.JPEG, jfifThumbnail))); + } + + /// + /// Replaces the contents of the APP0 section with the JFIF properties. + /// + private bool WriteJFIFApp0() + { + // Which IFD sections do we have? + var ifdjfef = new List(); + foreach (ExifProperty prop in Properties) { - Save(stream, true); + if (prop.IFD == IFD.JFIF) + { + ifdjfef.Add(prop); + } } - #endregion - - #region Private Helper Methods - /// - /// Reads the APP0 section containing JFIF metadata. - /// - private void ReadJFIFAPP0() + if (ifdjfef.Count == 0) { - // Find the APP0 section containing JFIF metadata - jfifApp0 = Sections.Find(a => (a.Marker == JPEGMarker.APP0) && - a.Header.Length >= 5 && - (Encoding.ASCII.GetString(a.Header, 0, 5) == "JFIF\0")); + // Nothing to write + return false; + } - // If there is no APP0 section, return. - if (jfifApp0 == null) - return; + // Create a memory stream to write the APP0 section to + var ms = new MemoryStream(); - byte[] header = jfifApp0.Header; - BitConverterEx jfifConv = BitConverterEx.BigEndian; + // JFIF identifier + ms.Write(Encoding.ASCII.GetBytes("JFIF\0"), 0, 5); - // Version - ushort version = jfifConv.ToUInt16(header, 5); - Properties.Add(new JFIFVersion(ExifTag.JFIFVersion, version)); + // Write tags + foreach (ExifProperty prop in ifdjfef) + { + ExifInterOperability interop = prop.Interoperability; + var data = interop.Data; + if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) + { + Array.Reverse(data); + } - // Units - byte unit = header[7]; - Properties.Add(new ExifEnumProperty(ExifTag.JFIFUnits, (JFIFDensityUnit)unit)); + ms.Write(data, 0, data.Length); + } - // X and Y densities - ushort xdensity = jfifConv.ToUInt16(header, 8); - Properties.Add(new ExifUShort(ExifTag.XDensity, xdensity)); - ushort ydensity = jfifConv.ToUInt16(header, 10); - Properties.Add(new ExifUShort(ExifTag.YDensity, ydensity)); + ms.Close(); + // Return APP0 header + if (_jfifApp0 is not null) + { + _jfifApp0.Header = ms.ToArray(); + return true; + } + + return false; + } + + /// + /// Reads the APP0 section containing JFIF extension metadata. + /// + private void ReadJFXXAPP0() + { + // Find the APP0 section containing JFIF metadata + _jfxxApp0 = Sections.Find(a => a.Marker == JPEGMarker.APP0 && + a.Header.Length >= 5 && + Encoding.ASCII.GetString(a.Header, 0, 5) == "JFXX\0"); + + // If there is no APP0 section, return. + if (_jfxxApp0 == null) + { + return; + } + + var header = _jfxxApp0.Header; + + // Version + var version = (JFIFExtension)header[5]; + Properties.Add(new ExifEnumProperty(ExifTag.JFXXExtensionCode, version)); + + // Read thumbnail + if (version == JFIFExtension.ThumbnailJPEG) + { + var data = new byte[header.Length - 6]; + Array.Copy(header, 6, data, 0, data.Length); + Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.JPEG, data))); + } + else if (version == JFIFExtension.Thumbnail24BitRGB) + { // Thumbnails pixel count - byte xthumbnail = header[12]; - Properties.Add(new ExifByte(ExifTag.JFIFXThumbnail, xthumbnail)); - byte ythumbnail = header[13]; - Properties.Add(new ExifByte(ExifTag.JFIFYThumbnail, ythumbnail)); - - // Read JFIF thumbnail - int n = xthumbnail * ythumbnail; - byte[] jfifThumbnail = new byte[n]; - Array.Copy(header, 14, jfifThumbnail, 0, n); - Properties.Add(new JFIFThumbnailProperty(ExifTag.JFIFThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.JPEG, jfifThumbnail))); + var xthumbnail = header[6]; + Properties.Add(new ExifByte(ExifTag.JFXXXThumbnail, xthumbnail)); + var ythumbnail = header[7]; + Properties.Add(new ExifByte(ExifTag.JFXXYThumbnail, ythumbnail)); + var data = new byte[3 * xthumbnail * ythumbnail]; + Array.Copy(header, 8, data, 0, data.Length); + Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.BMP24Bit, data))); } - /// - /// Replaces the contents of the APP0 section with the JFIF properties. - /// - private bool WriteJFIFApp0() + else if (version == JFIFExtension.ThumbnailPaletteRGB) { - // Which IFD sections do we have? - List ifdjfef = new List(); - foreach (ExifProperty prop in Properties) + // Thumbnails pixel count + var xthumbnail = header[6]; + Properties.Add(new ExifByte(ExifTag.JFXXXThumbnail, xthumbnail)); + var ythumbnail = header[7]; + Properties.Add(new ExifByte(ExifTag.JFXXYThumbnail, ythumbnail)); + var palette = new byte[768]; + Array.Copy(header, 8, palette, 0, palette.Length); + var data = new byte[xthumbnail * ythumbnail]; + Array.Copy(header, 8 + 768, data, 0, data.Length); + Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(palette, data))); + } + } + + /// + /// Replaces the contents of the APP0 section with the JFIF extension properties. + /// + private bool WriteJFXXApp0() + { + // Which IFD sections do we have? + var ifdjfef = new List(); + foreach (ExifProperty prop in Properties) + { + if (prop.IFD == IFD.JFXX) { - if (prop.IFD == IFD.JFIF) - ifdjfef.Add(prop); + ifdjfef.Add(prop); + } + } + + if (ifdjfef.Count == 0) + { + // Nothing to write + return false; + } + + // Create a memory stream to write the APP0 section to + var ms = new MemoryStream(); + + // JFIF identifier + ms.Write(Encoding.ASCII.GetBytes("JFXX\0"), 0, 5); + + // Write tags + foreach (ExifProperty prop in ifdjfef) + { + ExifInterOperability interop = prop.Interoperability; + var data = interop.Data; + if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) + { + Array.Reverse(data); } - if (ifdjfef.Count == 0) - { - // Nothing to write - return false; - } + ms.Write(data, 0, data.Length); + } + ms.Close(); - // Create a memory stream to write the APP0 section to - MemoryStream ms = new MemoryStream(); - - // JFIF identifier - ms.Write(Encoding.ASCII.GetBytes("JFIF\0"), 0, 5); - - // Write tags - foreach (ExifProperty prop in ifdjfef) - { - ExifInterOperability interop = prop.Interoperability; - byte[] data = interop.Data; - if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) - Array.Reverse(data); - ms.Write(data, 0, data.Length); - } - - ms.Close(); - + if (_jfxxApp0 is not null) + { // Return APP0 header - if (jfifApp0 is not null) - { - jfifApp0.Header = ms.ToArray(); - return true; - } - - return false; + _jfxxApp0.Header = ms.ToArray(); + return true; } - /// - /// Reads the APP0 section containing JFIF extension metadata. - /// - private void ReadJFXXAPP0() + return false; + } + + /// + /// Reads the APP1 section containing Exif metadata. + /// + private void ReadExifAPP1() + { + // Find the APP1 section containing Exif metadata + _exifApp1 = Sections.Find(a => a.Marker == JPEGMarker.APP1 && + a.Header.Length >= 6 && + Encoding.ASCII.GetString(a.Header, 0, 6) == "Exif\0\0"); + + // If there is no APP1 section, add a new one after the last APP0 section (if any). + if (_exifApp1 == null) { - // Find the APP0 section containing JFIF metadata - jfxxApp0 = Sections.Find(a => (a.Marker == JPEGMarker.APP0) && - a.Header.Length >= 5 && - (Encoding.ASCII.GetString(a.Header, 0, 5) == "JFXX\0")); - - // If there is no APP0 section, return. - if (jfxxApp0 == null) - return; - - byte[] header = jfxxApp0.Header; - - // Version - JFIFExtension version = (JFIFExtension)header[5]; - Properties.Add(new ExifEnumProperty(ExifTag.JFXXExtensionCode, version)); - - // Read thumbnail - if (version == JFIFExtension.ThumbnailJPEG) + var insertionIndex = Sections.FindLastIndex(a => a.Marker == JPEGMarker.APP0); + if (insertionIndex == -1) { - byte[] data = new byte[header.Length - 6]; - Array.Copy(header, 6, data, 0, data.Length); - Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.JPEG, data))); - } - else if (version == JFIFExtension.Thumbnail24BitRGB) - { - // Thumbnails pixel count - byte xthumbnail = header[6]; - Properties.Add(new ExifByte(ExifTag.JFXXXThumbnail, xthumbnail)); - byte ythumbnail = header[7]; - Properties.Add(new ExifByte(ExifTag.JFXXYThumbnail, ythumbnail)); - byte[] data = new byte[3 * xthumbnail * ythumbnail]; - Array.Copy(header, 8, data, 0, data.Length); - Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(JFIFThumbnail.ImageFormat.BMP24Bit, data))); - } - else if (version == JFIFExtension.ThumbnailPaletteRGB) - { - // Thumbnails pixel count - byte xthumbnail = header[6]; - Properties.Add(new ExifByte(ExifTag.JFXXXThumbnail, xthumbnail)); - byte ythumbnail = header[7]; - Properties.Add(new ExifByte(ExifTag.JFXXYThumbnail, ythumbnail)); - byte[] palette = new byte[768]; - Array.Copy(header, 8, palette, 0, palette.Length); - byte[] data = new byte[xthumbnail * ythumbnail]; - Array.Copy(header, 8 + 768, data, 0, data.Length); - Properties.Add(new JFIFThumbnailProperty(ExifTag.JFXXThumbnail, new JFIFThumbnail(palette, data))); - } - } - /// - /// Replaces the contents of the APP0 section with the JFIF extension properties. - /// - private bool WriteJFXXApp0() - { - // Which IFD sections do we have? - List ifdjfef = new List(); - foreach (ExifProperty prop in Properties) - { - if (prop.IFD == IFD.JFXX) - ifdjfef.Add(prop); + insertionIndex = 0; } - if (ifdjfef.Count == 0) + insertionIndex++; + _exifApp1 = new JPEGSection(JPEGMarker.APP1); + Sections.Insert(insertionIndex, _exifApp1); + if (BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian) { - // Nothing to write - return false; - } - - // Create a memory stream to write the APP0 section to - MemoryStream ms = new MemoryStream(); - - // JFIF identifier - ms.Write(Encoding.ASCII.GetBytes("JFXX\0"), 0, 5); - - // Write tags - foreach (ExifProperty prop in ifdjfef) - { - ExifInterOperability interop = prop.Interoperability; - byte[] data = interop.Data; - if (BitConverterEx.SystemByteOrder != BitConverterEx.ByteOrder.BigEndian && interop.TypeID == 3) - Array.Reverse(data); - ms.Write(data, 0, data.Length); - } - - ms.Close(); - - if (jfxxApp0 is not null) - { - // Return APP0 header - jfxxApp0.Header = ms.ToArray(); - return true; - } - - return false; - } - - /// - /// Reads the APP1 section containing Exif metadata. - /// - private void ReadExifAPP1() - { - // Find the APP1 section containing Exif metadata - exifApp1 = Sections.Find(a => (a.Marker == JPEGMarker.APP1) && - a.Header.Length >= 6 && - (Encoding.ASCII.GetString(a.Header, 0, 6) == "Exif\0\0")); - - // If there is no APP1 section, add a new one after the last APP0 section (if any). - if (exifApp1 == null) - { - int insertionIndex = Sections.FindLastIndex(a => a.Marker == JPEGMarker.APP0); - if (insertionIndex == -1) insertionIndex = 0; - insertionIndex++; - exifApp1 = new JPEGSection(JPEGMarker.APP1); - Sections.Insert(insertionIndex, exifApp1); - if (BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian) - ByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else - ByteOrder = BitConverterEx.ByteOrder.BigEndian; - return; - } - - byte[] header = exifApp1.Header; - SortedList ifdqueue = new SortedList(); - makerNoteOffset = 0; - - // TIFF header - int tiffoffset = 6; - if (header[tiffoffset] == 0x49 && header[tiffoffset + 1] == 0x49) ByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else if (header[tiffoffset] == 0x4D && header[tiffoffset + 1] == 0x4D) + } + else + { ByteOrder = BitConverterEx.ByteOrder.BigEndian; - else - throw new NotValidExifFileException(); - - // TIFF header may have a different byte order - BitConverterEx.ByteOrder tiffByteOrder = ByteOrder; - if (BitConverterEx.LittleEndian.ToUInt16(header, tiffoffset + 2) == 42) - tiffByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else if (BitConverterEx.BigEndian.ToUInt16(header, tiffoffset + 2) == 42) - tiffByteOrder = BitConverterEx.ByteOrder.BigEndian; - else - throw new NotValidExifFileException(); - - // Offset to 0th IFD - int ifd0offset = (int)BitConverterEx.ToUInt32(header, tiffoffset + 4, tiffByteOrder, BitConverterEx.SystemByteOrder); - ifdqueue.Add(ifd0offset, IFD.Zeroth); - - BitConverterEx conv = new BitConverterEx(ByteOrder, BitConverterEx.SystemByteOrder); - int thumboffset = -1; - int thumblength = 0; - int thumbtype = -1; - // Read IFDs - while (ifdqueue.Count != 0) - { - int ifdoffset = tiffoffset + ifdqueue.Keys[0]; - IFD currentifd = ifdqueue.Values[0]; - ifdqueue.RemoveAt(0); - - // Field count - ushort fieldcount = conv.ToUInt16(header, ifdoffset); - for (short i = 0; i < fieldcount; i++) - { - // Read field info - int fieldoffset = ifdoffset + 2 + 12 * i; - ushort tag = conv.ToUInt16(header, fieldoffset); - ushort type = conv.ToUInt16(header, fieldoffset + 2); - uint count = conv.ToUInt32(header, fieldoffset + 4); - byte[] value = new byte[4]; - Array.Copy(header, fieldoffset + 8, value, 0, 4); - - // Fields containing offsets to other IFDs - if (currentifd == IFD.Zeroth && tag == 0x8769) - { - int exififdpointer = (int)conv.ToUInt32(value, 0); - ifdqueue.Add(exififdpointer, IFD.EXIF); - } - else if (currentifd == IFD.Zeroth && tag == 0x8825) - { - int gpsifdpointer = (int)conv.ToUInt32(value, 0); - ifdqueue.Add(gpsifdpointer, IFD.GPS); - } - else if (currentifd == IFD.EXIF && tag == 0xa005) - { - int interopifdpointer = (int)conv.ToUInt32(value, 0); - ifdqueue.Add(interopifdpointer, IFD.Interop); - } - - // Save the offset to maker note data - if (currentifd == IFD.EXIF && tag == 37500) - makerNoteOffset = conv.ToUInt32(value, 0); - - // Calculate the bytes we need to read - uint baselength = 0; - if (type == 1 || type == 2 || type == 7) - baselength = 1; - else if (type == 3) - baselength = 2; - else if (type == 4 || type == 9) - baselength = 4; - else if (type == 5 || type == 10) - baselength = 8; - uint totallength = count * baselength; - - // If field value does not fit in 4 bytes - // the value field is an offset to the actual - // field value - int fieldposition = 0; - if (totallength > 4) - { - fieldposition = tiffoffset + (int)conv.ToUInt32(value, 0); - value = new byte[totallength]; - Array.Copy(header, fieldposition, value, 0, totallength); - } - - // Compressed thumbnail data - if (currentifd == IFD.First && tag == 0x201) - { - thumbtype = 0; - thumboffset = (int)conv.ToUInt32(value, 0); - } - else if (currentifd == IFD.First && tag == 0x202) - thumblength = (int)conv.ToUInt32(value, 0); - - // Uncompressed thumbnail data - if (currentifd == IFD.First && tag == 0x111) - { - thumbtype = 1; - // Offset to first strip - if (type == 3) - thumboffset = (int)conv.ToUInt16(value, 0); - else - thumboffset = (int)conv.ToUInt32(value, 0); - } - else if (currentifd == IFD.First && tag == 0x117) - { - thumblength = 0; - for (int j = 0; j < count; j++) - { - if (type == 3) - thumblength += (int)conv.ToUInt16(value, 0); - else - thumblength += (int)conv.ToUInt32(value, 0); - } - } - - // Create the exif property from the interop data - ExifProperty prop = ExifPropertyFactory.Get(tag, type, count, value, ByteOrder, currentifd, Encoding); - Properties.Add(prop); - } - - // 1st IFD pointer - int firstifdpointer = (int)conv.ToUInt32(header, ifdoffset + 2 + 12 * fieldcount); - if (firstifdpointer != 0) - ifdqueue.Add(firstifdpointer, IFD.First); - // Read thumbnail - if (thumboffset != -1 && thumblength != 0 && Thumbnail == null) - { - if (thumbtype == 0) - { - using (MemoryStream ts = new MemoryStream(header, tiffoffset + thumboffset, thumblength)) - { - Thumbnail = ImageFile.FromStream(ts); - } - } - } } + + return; } - /// - /// Replaces the contents of the APP1 section with the Exif properties. - /// - private bool WriteExifApp1(bool preserveMakerNote) + var header = _exifApp1.Header; + var ifdqueue = new SortedList(); + _makerNoteOffset = 0; + + // TIFF header + var tiffoffset = 6; + if (header[tiffoffset] == 0x49 && header[tiffoffset + 1] == 0x49) { - // Zero out IFD field offsets. We will fill those as we write the IFD sections - exifIFDFieldOffset = 0; - gpsIFDFieldOffset = 0; - interopIFDFieldOffset = 0; - firstIFDFieldOffset = 0; - // We also do not know the location of the embedded thumbnail yet - thumbOffsetLocation = 0; - thumbOffsetValue = 0; - thumbSizeLocation = 0; - thumbSizeValue = 0; - // Write thumbnail tags if they are missing, remove otherwise - if (Thumbnail == null) - { - Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormat); - Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormatLength); - } - else - { - if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormat)) - Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormat, 0)); - if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormatLength)) - Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormatLength, 0)); - } - - // Which IFD sections do we have? - Dictionary ifdzeroth = new Dictionary(); - Dictionary ifdexif = new Dictionary(); - Dictionary ifdgps = new Dictionary(); - Dictionary ifdinterop = new Dictionary(); - Dictionary ifdfirst = new Dictionary(); - - foreach (ExifProperty prop in Properties) - { - switch (prop.IFD) - { - case IFD.Zeroth: - ifdzeroth.Add(prop.Tag, prop); - break; - case IFD.EXIF: - ifdexif.Add(prop.Tag, prop); - break; - case IFD.GPS: - ifdgps.Add(prop.Tag, prop); - break; - case IFD.Interop: - ifdinterop.Add(prop.Tag, prop); - break; - case IFD.First: - ifdfirst.Add(prop.Tag, prop); - break; - } - } - - // Add IFD pointers if they are missing - // We will write the pointer values later on - if (ifdexif.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) - ifdzeroth.Add(ExifTag.EXIFIFDPointer, new ExifUInt(ExifTag.EXIFIFDPointer, 0)); - if (ifdgps.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) - ifdzeroth.Add(ExifTag.GPSIFDPointer, new ExifUInt(ExifTag.GPSIFDPointer, 0)); - if (ifdinterop.Count != 0 && !ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) - ifdexif.Add(ExifTag.InteroperabilityIFDPointer, new ExifUInt(ExifTag.InteroperabilityIFDPointer, 0)); - - // Remove IFD pointers if IFD sections are missing - if (ifdexif.Count == 0 && ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) - ifdzeroth.Remove(ExifTag.EXIFIFDPointer); - if (ifdgps.Count == 0 && ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) - ifdzeroth.Remove(ExifTag.GPSIFDPointer); - if (ifdinterop.Count == 0 && ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) - ifdexif.Remove(ExifTag.InteroperabilityIFDPointer); - - if (ifdzeroth.Count == 0 && ifdgps.Count == 0 && ifdinterop.Count == 0 && ifdfirst.Count == 0 && Thumbnail == null) - { - // Nothing to write - return false; - } - - // We will need these BitConverters to write byte-ordered data - BitConverterEx bceExif = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); - - // Create a memory stream to write the APP1 section to - MemoryStream ms = new MemoryStream(); - - // Exif identifier - ms.Write(Encoding.ASCII.GetBytes("Exif\0\0"), 0, 6); - - // TIFF header - // Byte order - long tiffoffset = ms.Position; - ms.Write((ByteOrder == BitConverterEx.ByteOrder.LittleEndian ? new byte[] { 0x49, 0x49 } : new byte[] { 0x4D, 0x4D }), 0, 2); - // TIFF ID - ms.Write(bceExif.GetBytes((ushort)42), 0, 2); - // Offset to 0th IFD - ms.Write(bceExif.GetBytes((uint)8), 0, 4); - - // Write IFDs - WriteIFD(ms, ifdzeroth, IFD.Zeroth, tiffoffset, preserveMakerNote); - uint exififdrelativeoffset = (uint)(ms.Position - tiffoffset); - WriteIFD(ms, ifdexif, IFD.EXIF, tiffoffset, preserveMakerNote); - uint gpsifdrelativeoffset = (uint)(ms.Position - tiffoffset); - WriteIFD(ms, ifdgps, IFD.GPS, tiffoffset, preserveMakerNote); - uint interopifdrelativeoffset = (uint)(ms.Position - tiffoffset); - WriteIFD(ms, ifdinterop, IFD.Interop, tiffoffset, preserveMakerNote); - uint firstifdrelativeoffset = (uint)(ms.Position - tiffoffset); - WriteIFD(ms, ifdfirst, IFD.First, tiffoffset, preserveMakerNote); - - // Now that we now the location of IFDs we can go back and write IFD offsets - if (exifIFDFieldOffset != 0) - { - ms.Seek(exifIFDFieldOffset, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(exififdrelativeoffset), 0, 4); - } - if (gpsIFDFieldOffset != 0) - { - ms.Seek(gpsIFDFieldOffset, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(gpsifdrelativeoffset), 0, 4); - } - if (interopIFDFieldOffset != 0) - { - ms.Seek(interopIFDFieldOffset, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(interopifdrelativeoffset), 0, 4); - } - if (firstIFDFieldOffset != 0) - { - ms.Seek(firstIFDFieldOffset, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(firstifdrelativeoffset), 0, 4); - } - // We can write thumbnail location now - if (thumbOffsetLocation != 0) - { - ms.Seek(thumbOffsetLocation, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(thumbOffsetValue), 0, 4); - } - if (thumbSizeLocation != 0) - { - ms.Seek(thumbSizeLocation, SeekOrigin.Begin); - ms.Write(bceExif.GetBytes(thumbSizeValue), 0, 4); - } - - ms.Close(); - - if (exifApp1 is not null) - { - // Return APP1 header - exifApp1.Header = ms.ToArray(); - return true; - } - - return false; + ByteOrder = BitConverterEx.ByteOrder.LittleEndian; + } + else if (header[tiffoffset] == 0x4D && header[tiffoffset + 1] == 0x4D) + { + ByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidExifFileException(); } - private void WriteIFD(MemoryStream stream, Dictionary ifd, IFD ifdtype, long tiffoffset, bool preserveMakerNote) + // TIFF header may have a different byte order + BitConverterEx.ByteOrder tiffByteOrder = ByteOrder; + if (BitConverterEx.LittleEndian.ToUInt16(header, tiffoffset + 2) == 42) { - BitConverterEx conv = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); + tiffByteOrder = BitConverterEx.ByteOrder.LittleEndian; + } + else if (BitConverterEx.BigEndian.ToUInt16(header, tiffoffset + 2) == 42) + { + tiffByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidExifFileException(); + } - // Create a queue of fields to write - Queue fieldqueue = new Queue(); - foreach (ExifProperty prop in ifd.Values) - if (prop.Tag != ExifTag.MakerNote) - fieldqueue.Enqueue(prop); - // Push the maker note data to the end - if (ifd.ContainsKey(ExifTag.MakerNote)) - fieldqueue.Enqueue(ifd[ExifTag.MakerNote]); + // Offset to 0th IFD + var ifd0offset = (int)BitConverterEx.ToUInt32(header, tiffoffset + 4, tiffByteOrder, BitConverterEx.SystemByteOrder); + ifdqueue.Add(ifd0offset, IFD.Zeroth); - // Offset to start of field data from start of TIFF header - uint dataoffset = (uint)(2 + ifd.Count * 12 + 4 + stream.Position - tiffoffset); - uint currentdataoffset = dataoffset; - long absolutedataoffset = stream.Position + (2 + ifd.Count * 12 + 4); + var conv = new BitConverterEx(ByteOrder, BitConverterEx.SystemByteOrder); + var thumboffset = -1; + var thumblength = 0; + var thumbtype = -1; + + // Read IFDs + while (ifdqueue.Count != 0) + { + var ifdoffset = tiffoffset + ifdqueue.Keys[0]; + IFD currentifd = ifdqueue.Values[0]; + ifdqueue.RemoveAt(0); - bool makernotewritten = false; // Field count - stream.Write(conv.GetBytes((ushort)ifd.Count), 0, 2); - // Fields - while (fieldqueue.Count != 0) + var fieldcount = conv.ToUInt16(header, ifdoffset); + for (short i = 0; i < fieldcount; i++) { - ExifProperty field = fieldqueue.Dequeue(); - ExifInterOperability interop = field.Interoperability; - - uint fillerbytecount = 0; - - // Try to preserve the makernote data offset - if (!makernotewritten && - !makerNoteProcessed && - makerNoteOffset != 0 && - ifdtype == IFD.EXIF && - field.Tag != ExifTag.MakerNote && - interop.Data.Length > 4 && - currentdataoffset + interop.Data.Length > makerNoteOffset && - ifd.ContainsKey(ExifTag.MakerNote)) - { - // Delay writing this field until we write the creator's note data - fieldqueue.Enqueue(field); - continue; - } - else if (field.Tag == ExifTag.MakerNote) - { - makernotewritten = true; - // We may need to write filler bytes to preserve maker note offset - if (preserveMakerNote && !makerNoteProcessed && (makerNoteOffset > currentdataoffset)) - fillerbytecount = makerNoteOffset - currentdataoffset; - else - fillerbytecount = 0; - } - - // Tag - stream.Write(conv.GetBytes(interop.TagID), 0, 2); - // Type - stream.Write(conv.GetBytes(interop.TypeID), 0, 2); - // Count - stream.Write(conv.GetBytes(interop.Count), 0, 4); - // Field data - byte[] data = interop.Data; - if (ByteOrder != BitConverterEx.SystemByteOrder && - (interop.TypeID == 3 || interop.TypeID == 4 || interop.TypeID == 9 || - interop.TypeID == 5 || interop.TypeID == 10)) - { - int vlen = 4; - if (interop.TypeID == 3) vlen = 2; - int n = data.Length / vlen; - - for (int i = 0; i < n; i++) - Array.Reverse(data, i * vlen, vlen); - } + // Read field info + var fieldoffset = ifdoffset + 2 + (12 * i); + var tag = conv.ToUInt16(header, fieldoffset); + var type = conv.ToUInt16(header, fieldoffset + 2); + var count = conv.ToUInt32(header, fieldoffset + 4); + var value = new byte[4]; + Array.Copy(header, fieldoffset + 8, value, 0, 4); // Fields containing offsets to other IFDs - // Just store their offsets, we will write the values later on when we know the lengths of IFDs - if (ifdtype == IFD.Zeroth && interop.TagID == 0x8769) - exifIFDFieldOffset = stream.Position; - else if (ifdtype == IFD.Zeroth && interop.TagID == 0x8825) - gpsIFDFieldOffset = stream.Position; - else if (ifdtype == IFD.EXIF && interop.TagID == 0xa005) - interopIFDFieldOffset = stream.Position; - else if (ifdtype == IFD.First && interop.TagID == 0x201) - thumbOffsetLocation = stream.Position; - else if (ifdtype == IFD.First && interop.TagID == 0x202) - thumbSizeLocation = stream.Position; + if (currentifd == IFD.Zeroth && tag == 0x8769) + { + var exififdpointer = (int)conv.ToUInt32(value, 0); + ifdqueue.Add(exififdpointer, IFD.EXIF); + } + else if (currentifd == IFD.Zeroth && tag == 0x8825) + { + var gpsifdpointer = (int)conv.ToUInt32(value, 0); + ifdqueue.Add(gpsifdpointer, IFD.GPS); + } + else if (currentifd == IFD.EXIF && tag == 0xa005) + { + var interopifdpointer = (int)conv.ToUInt32(value, 0); + ifdqueue.Add(interopifdpointer, IFD.Interop); + } - // Write 4 byte field value or field data - if (data.Length <= 4) + // Save the offset to maker note data + if (currentifd == IFD.EXIF && tag == 37500) { - stream.Write(data, 0, data.Length); - for (int i = data.Length; i < 4; i++) - stream.WriteByte(0); + _makerNoteOffset = conv.ToUInt32(value, 0); } - else + + // Calculate the bytes we need to read + uint baselength = 0; + if (type == 1 || type == 2 || type == 7) { - // Pointer to data area relative to TIFF header - stream.Write(conv.GetBytes(currentdataoffset + fillerbytecount), 0, 4); - // Actual data - long currentoffset = stream.Position; - stream.Seek(absolutedataoffset, SeekOrigin.Begin); - // Write filler bytes - for (int i = 0; i < fillerbytecount; i++) - stream.WriteByte(0xFF); - stream.Write(data, 0, data.Length); - stream.Seek(currentoffset, SeekOrigin.Begin); - // Increment pointers - currentdataoffset += fillerbytecount + (uint)data.Length; - absolutedataoffset += fillerbytecount + data.Length; + baselength = 1; } + else if (type == 3) + { + baselength = 2; + } + else if (type == 4 || type == 9) + { + baselength = 4; + } + else if (type == 5 || type == 10) + { + baselength = 8; + } + + var totallength = count * baselength; + + // If field value does not fit in 4 bytes + // the value field is an offset to the actual + // field value + var fieldposition = 0; + if (totallength > 4) + { + fieldposition = tiffoffset + (int)conv.ToUInt32(value, 0); + value = new byte[totallength]; + Array.Copy(header, fieldposition, value, 0, totallength); + } + + // Compressed thumbnail data + if (currentifd == IFD.First && tag == 0x201) + { + thumbtype = 0; + thumboffset = (int)conv.ToUInt32(value, 0); + } + else if (currentifd == IFD.First && tag == 0x202) + { + thumblength = (int)conv.ToUInt32(value, 0); + } + + // Uncompressed thumbnail data + if (currentifd == IFD.First && tag == 0x111) + { + thumbtype = 1; + + // Offset to first strip + if (type == 3) + { + thumboffset = conv.ToUInt16(value, 0); + } + else + { + thumboffset = (int)conv.ToUInt32(value, 0); + } + } + else if (currentifd == IFD.First && tag == 0x117) + { + thumblength = 0; + for (var j = 0; j < count; j++) + { + if (type == 3) + { + thumblength += conv.ToUInt16(value, 0); + } + else + { + thumblength += (int)conv.ToUInt32(value, 0); + } + } + } + + // Create the exif property from the interop data + ExifProperty prop = ExifPropertyFactory.Get(tag, type, count, value, ByteOrder, currentifd, Encoding); + Properties.Add(prop); } - // Offset to 1st IFD - // We will write zeros for now. This will be filled after we write all IFDs - if (ifdtype == IFD.Zeroth) - firstIFDFieldOffset = stream.Position; - stream.Write(new byte[] { 0, 0, 0, 0 }, 0, 4); - // Seek to end of IFD - stream.Seek(absolutedataoffset, SeekOrigin.Begin); - - // Write thumbnail data - if (ifdtype == IFD.First) + // 1st IFD pointer + var firstifdpointer = (int)conv.ToUInt32(header, ifdoffset + 2 + (12 * fieldcount)); + if (firstifdpointer != 0) { - if (Thumbnail != null) + ifdqueue.Add(firstifdpointer, IFD.First); + } + + // Read thumbnail + if (thumboffset != -1 && thumblength != 0 && Thumbnail == null) + { + if (thumbtype == 0) { - MemoryStream ts = new MemoryStream(); - Thumbnail.Save(ts); - ts.Close(); - byte[] thumb = ts.ToArray(); - thumbOffsetValue = (uint)(stream.Position - tiffoffset); - thumbSizeValue = (uint)thumb.Length; - stream.Write(thumb, 0, thumb.Length); - ts.Dispose(); - } - else - { - thumbOffsetValue = 0; - thumbSizeValue = 0; + using (var ts = new MemoryStream(header, tiffoffset + thumboffset, thumblength)) + { + Thumbnail = FromStream(ts); + } } } } - #endregion } + + /// + /// Replaces the contents of the APP1 section with the Exif properties. + /// + private bool WriteExifApp1(bool preserveMakerNote) + { + // Zero out IFD field offsets. We will fill those as we write the IFD sections + _exifIfdFieldOffset = 0; + _gpsIfdFieldOffset = 0; + _interopIfdFieldOffset = 0; + _firstIfdFieldOffset = 0; + + // We also do not know the location of the embedded thumbnail yet + _thumbOffsetLocation = 0; + _thumbOffsetValue = 0; + _thumbSizeLocation = 0; + _thumbSizeValue = 0; + + // Write thumbnail tags if they are missing, remove otherwise + if (Thumbnail == null) + { + Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormat); + Properties.Remove(ExifTag.ThumbnailJPEGInterchangeFormatLength); + } + else + { + if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormat)) + { + Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormat, 0)); + } + + if (!Properties.ContainsKey(ExifTag.ThumbnailJPEGInterchangeFormatLength)) + { + Properties.Add(new ExifUInt(ExifTag.ThumbnailJPEGInterchangeFormatLength, 0)); + } + } + + // Which IFD sections do we have? + var ifdzeroth = new Dictionary(); + var ifdexif = new Dictionary(); + var ifdgps = new Dictionary(); + var ifdinterop = new Dictionary(); + var ifdfirst = new Dictionary(); + + foreach (ExifProperty prop in Properties) + { + switch (prop.IFD) + { + case IFD.Zeroth: + ifdzeroth.Add(prop.Tag, prop); + break; + case IFD.EXIF: + ifdexif.Add(prop.Tag, prop); + break; + case IFD.GPS: + ifdgps.Add(prop.Tag, prop); + break; + case IFD.Interop: + ifdinterop.Add(prop.Tag, prop); + break; + case IFD.First: + ifdfirst.Add(prop.Tag, prop); + break; + } + } + + // Add IFD pointers if they are missing + // We will write the pointer values later on + if (ifdexif.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) + { + ifdzeroth.Add(ExifTag.EXIFIFDPointer, new ExifUInt(ExifTag.EXIFIFDPointer, 0)); + } + + if (ifdgps.Count != 0 && !ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) + { + ifdzeroth.Add(ExifTag.GPSIFDPointer, new ExifUInt(ExifTag.GPSIFDPointer, 0)); + } + + if (ifdinterop.Count != 0 && !ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) + { + ifdexif.Add(ExifTag.InteroperabilityIFDPointer, new ExifUInt(ExifTag.InteroperabilityIFDPointer, 0)); + } + + // Remove IFD pointers if IFD sections are missing + if (ifdexif.Count == 0 && ifdzeroth.ContainsKey(ExifTag.EXIFIFDPointer)) + { + ifdzeroth.Remove(ExifTag.EXIFIFDPointer); + } + + if (ifdgps.Count == 0 && ifdzeroth.ContainsKey(ExifTag.GPSIFDPointer)) + { + ifdzeroth.Remove(ExifTag.GPSIFDPointer); + } + + if (ifdinterop.Count == 0 && ifdexif.ContainsKey(ExifTag.InteroperabilityIFDPointer)) + { + ifdexif.Remove(ExifTag.InteroperabilityIFDPointer); + } + + if (ifdzeroth.Count == 0 && ifdgps.Count == 0 && ifdinterop.Count == 0 && ifdfirst.Count == 0 && + Thumbnail == null) + { + // Nothing to write + return false; + } + + // We will need these BitConverters to write byte-ordered data + var bceExif = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); + + // Create a memory stream to write the APP1 section to + var ms = new MemoryStream(); + + // Exif identifier + ms.Write(Encoding.ASCII.GetBytes("Exif\0\0"), 0, 6); + + // TIFF header + // Byte order + var tiffoffset = ms.Position; + ms.Write(ByteOrder == BitConverterEx.ByteOrder.LittleEndian ? new byte[] { 0x49, 0x49 } : new byte[] { 0x4D, 0x4D }, 0, 2); + + // TIFF ID + ms.Write(bceExif.GetBytes((ushort)42), 0, 2); + + // Offset to 0th IFD + ms.Write(bceExif.GetBytes((uint)8), 0, 4); + + // Write IFDs + WriteIFD(ms, ifdzeroth, IFD.Zeroth, tiffoffset, preserveMakerNote); + var exififdrelativeoffset = (uint)(ms.Position - tiffoffset); + WriteIFD(ms, ifdexif, IFD.EXIF, tiffoffset, preserveMakerNote); + var gpsifdrelativeoffset = (uint)(ms.Position - tiffoffset); + WriteIFD(ms, ifdgps, IFD.GPS, tiffoffset, preserveMakerNote); + var interopifdrelativeoffset = (uint)(ms.Position - tiffoffset); + WriteIFD(ms, ifdinterop, IFD.Interop, tiffoffset, preserveMakerNote); + var firstifdrelativeoffset = (uint)(ms.Position - tiffoffset); + WriteIFD(ms, ifdfirst, IFD.First, tiffoffset, preserveMakerNote); + + // Now that we now the location of IFDs we can go back and write IFD offsets + if (_exifIfdFieldOffset != 0) + { + ms.Seek(_exifIfdFieldOffset, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(exififdrelativeoffset), 0, 4); + } + + if (_gpsIfdFieldOffset != 0) + { + ms.Seek(_gpsIfdFieldOffset, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(gpsifdrelativeoffset), 0, 4); + } + + if (_interopIfdFieldOffset != 0) + { + ms.Seek(_interopIfdFieldOffset, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(interopifdrelativeoffset), 0, 4); + } + + if (_firstIfdFieldOffset != 0) + { + ms.Seek(_firstIfdFieldOffset, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(firstifdrelativeoffset), 0, 4); + } + + // We can write thumbnail location now + if (_thumbOffsetLocation != 0) + { + ms.Seek(_thumbOffsetLocation, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(_thumbOffsetValue), 0, 4); + } + + if (_thumbSizeLocation != 0) + { + ms.Seek(_thumbSizeLocation, SeekOrigin.Begin); + ms.Write(bceExif.GetBytes(_thumbSizeValue), 0, 4); + } + + ms.Close(); + + if (_exifApp1 is not null) + { + // Return APP1 header + _exifApp1.Header = ms.ToArray(); + return true; + } + + return false; + } + + private void WriteIFD(MemoryStream stream, Dictionary ifd, IFD ifdtype, long tiffoffset, bool preserveMakerNote) + { + var conv = new BitConverterEx(BitConverterEx.SystemByteOrder, ByteOrder); + + // Create a queue of fields to write + var fieldqueue = new Queue(); + foreach (ExifProperty prop in ifd.Values) + { + if (prop.Tag != ExifTag.MakerNote) + { + fieldqueue.Enqueue(prop); + } + } + + // Push the maker note data to the end + if (ifd.ContainsKey(ExifTag.MakerNote)) + { + fieldqueue.Enqueue(ifd[ExifTag.MakerNote]); + } + + // Offset to start of field data from start of TIFF header + var dataoffset = (uint)(2 + (ifd.Count * 12) + 4 + stream.Position - tiffoffset); + var currentdataoffset = dataoffset; + var absolutedataoffset = stream.Position + (2 + (ifd.Count * 12) + 4); + + var makernotewritten = false; + + // Field count + stream.Write(conv.GetBytes((ushort)ifd.Count), 0, 2); + + // Fields + while (fieldqueue.Count != 0) + { + ExifProperty field = fieldqueue.Dequeue(); + ExifInterOperability interop = field.Interoperability; + + uint fillerbytecount = 0; + + // Try to preserve the makernote data offset + if (!makernotewritten && + !_makerNoteProcessed && + _makerNoteOffset != 0 && + ifdtype == IFD.EXIF && + field.Tag != ExifTag.MakerNote && + interop.Data.Length > 4 && + currentdataoffset + interop.Data.Length > _makerNoteOffset && + ifd.ContainsKey(ExifTag.MakerNote)) + { + // Delay writing this field until we write the creator's note data + fieldqueue.Enqueue(field); + continue; + } + + if (field.Tag == ExifTag.MakerNote) + { + makernotewritten = true; + + // We may need to write filler bytes to preserve maker note offset + if (preserveMakerNote && !_makerNoteProcessed && _makerNoteOffset > currentdataoffset) + { + fillerbytecount = _makerNoteOffset - currentdataoffset; + } + else + { + fillerbytecount = 0; + } + } + + // Tag + stream.Write(conv.GetBytes(interop.TagID), 0, 2); + + // Type + stream.Write(conv.GetBytes(interop.TypeID), 0, 2); + + // Count + stream.Write(conv.GetBytes(interop.Count), 0, 4); + + // Field data + var data = interop.Data; + if (ByteOrder != BitConverterEx.SystemByteOrder && + (interop.TypeID == 3 || interop.TypeID == 4 || interop.TypeID == 9 || + interop.TypeID == 5 || interop.TypeID == 10)) + { + var vlen = 4; + if (interop.TypeID == 3) + { + vlen = 2; + } + + var n = data.Length / vlen; + + for (var i = 0; i < n; i++) + { + Array.Reverse(data, i * vlen, vlen); + } + } + + // Fields containing offsets to other IFDs + // Just store their offsets, we will write the values later on when we know the lengths of IFDs + if (ifdtype == IFD.Zeroth && interop.TagID == 0x8769) + { + _exifIfdFieldOffset = stream.Position; + } + else if (ifdtype == IFD.Zeroth && interop.TagID == 0x8825) + { + _gpsIfdFieldOffset = stream.Position; + } + else if (ifdtype == IFD.EXIF && interop.TagID == 0xa005) + { + _interopIfdFieldOffset = stream.Position; + } + else if (ifdtype == IFD.First && interop.TagID == 0x201) + { + _thumbOffsetLocation = stream.Position; + } + else if (ifdtype == IFD.First && interop.TagID == 0x202) + { + _thumbSizeLocation = stream.Position; + } + + // Write 4 byte field value or field data + if (data.Length <= 4) + { + stream.Write(data, 0, data.Length); + for (var i = data.Length; i < 4; i++) + { + stream.WriteByte(0); + } + } + else + { + // Pointer to data area relative to TIFF header + stream.Write(conv.GetBytes(currentdataoffset + fillerbytecount), 0, 4); + + // Actual data + var currentoffset = stream.Position; + stream.Seek(absolutedataoffset, SeekOrigin.Begin); + + // Write filler bytes + for (var i = 0; i < fillerbytecount; i++) + { + stream.WriteByte(0xFF); + } + + stream.Write(data, 0, data.Length); + stream.Seek(currentoffset, SeekOrigin.Begin); + + // Increment pointers + currentdataoffset += fillerbytecount + (uint)data.Length; + absolutedataoffset += fillerbytecount + data.Length; + } + } + + // Offset to 1st IFD + // We will write zeros for now. This will be filled after we write all IFDs + if (ifdtype == IFD.Zeroth) + { + _firstIfdFieldOffset = stream.Position; + } + + stream.Write(new byte[] { 0, 0, 0, 0 }, 0, 4); + + // Seek to end of IFD + stream.Seek(absolutedataoffset, SeekOrigin.Begin); + + // Write thumbnail data + if (ifdtype == IFD.First) + { + if (Thumbnail != null) + { + var ts = new MemoryStream(); + Thumbnail.Save(ts); + ts.Close(); + var thumb = ts.ToArray(); + _thumbOffsetValue = (uint)(stream.Position - tiffoffset); + _thumbSizeValue = (uint)thumb.Length; + stream.Write(thumb, 0, thumb.Length); + ts.Dispose(); + } + else + { + _thumbOffsetValue = 0; + _thumbSizeValue = 0; + } + } + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/JPEGMarker.cs b/src/Umbraco.Core/Media/Exif/JPEGMarker.cs index a7a3b4a9b1..3912d87e82 100644 --- a/src/Umbraco.Core/Media/Exif/JPEGMarker.cs +++ b/src/Umbraco.Core/Media/Exif/JPEGMarker.cs @@ -1,85 +1,95 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents a JPEG marker byte. +/// +internal enum JPEGMarker : byte { - /// - /// Represents a JPEG marker byte. - /// - internal enum JPEGMarker : byte - { - // Start Of Frame markers, non-differential, Huffman coding - SOF0 = 0xc0, - SOF1 = 0xc1, - SOF2 = 0xc2, - SOF3 = 0xc3, - // Start Of Frame markers, differential, Huffman coding - SOF5 = 0xc5, - SOF6 = 0xc6, - SOF7 = 0xc7, - // Start Of Frame markers, non-differential, arithmetic coding - JPG = 0xc8, - SOF9 = 0xc9, - SOF10 = 0xca, - SOF11 = 0xcb, - // Start Of Frame markers, differential, arithmetic coding - SOF13 = 0xcd, - SOF14 = 0xce, - SOF15 = 0xcf, - // Huffman table specification - DHT = 0xc4, - // Arithmetic coding conditioning specification - DAC = 0xcc, - // Restart interval termination - RST0 = 0xd0, - RST1 = 0xd1, - RST2 = 0xd2, - RST3 = 0xd3, - RST4 = 0xd4, - RST5 = 0xd5, - RST6 = 0xd6, - RST7 = 0xd7, - // Other markers - SOI = 0xd8, - EOI = 0xd9, - SOS = 0xda, - DQT = 0xdb, - DNL = 0xdc, - DRI = 0xdd, - DHP = 0xde, - EXP = 0xdf, - // application segments - APP0 = 0xe0, - APP1 = 0xe1, - APP2 = 0xe2, - APP3 = 0xe3, - APP4 = 0xe4, - APP5 = 0xe5, - APP6 = 0xe6, - APP7 = 0xe7, - APP8 = 0xe8, - APP9 = 0xe9, - APP10 = 0xea, - APP11 = 0xeb, - APP12 = 0xec, - APP13 = 0xed, - APP14 = 0xee, - APP15 = 0xef, - // JPEG extensions - JPG0 = 0xf0, - JPG1 = 0xf1, - JPG2 = 0xf2, - JPG3 = 0xf3, - JPG4 = 0xf4, - JPG5 = 0xf5, - JPG6 = 0xf6, - JPG7 = 0xf7, - JPG8 = 0xf8, - JPG9 = 0xf9, - JPG10 = 0xfa, - JPG11 = 0xfb, - JP1G2 = 0xfc, - JPG13 = 0xfd, - // Comment - COM = 0xfe, - // Temporary - TEM = 0x01, - } + // Start Of Frame markers, non-differential, Huffman coding + SOF0 = 0xc0, + SOF1 = 0xc1, + SOF2 = 0xc2, + SOF3 = 0xc3, + + // Start Of Frame markers, differential, Huffman coding + SOF5 = 0xc5, + SOF6 = 0xc6, + SOF7 = 0xc7, + + // Start Of Frame markers, non-differential, arithmetic coding + JPG = 0xc8, + SOF9 = 0xc9, + SOF10 = 0xca, + SOF11 = 0xcb, + + // Start Of Frame markers, differential, arithmetic coding + SOF13 = 0xcd, + SOF14 = 0xce, + SOF15 = 0xcf, + + // Huffman table specification + DHT = 0xc4, + + // Arithmetic coding conditioning specification + DAC = 0xcc, + + // Restart interval termination + RST0 = 0xd0, + RST1 = 0xd1, + RST2 = 0xd2, + RST3 = 0xd3, + RST4 = 0xd4, + RST5 = 0xd5, + RST6 = 0xd6, + RST7 = 0xd7, + + // Other markers + SOI = 0xd8, + EOI = 0xd9, + SOS = 0xda, + DQT = 0xdb, + DNL = 0xdc, + DRI = 0xdd, + DHP = 0xde, + EXP = 0xdf, + + // application segments + APP0 = 0xe0, + APP1 = 0xe1, + APP2 = 0xe2, + APP3 = 0xe3, + APP4 = 0xe4, + APP5 = 0xe5, + APP6 = 0xe6, + APP7 = 0xe7, + APP8 = 0xe8, + APP9 = 0xe9, + APP10 = 0xea, + APP11 = 0xeb, + APP12 = 0xec, + APP13 = 0xed, + APP14 = 0xee, + APP15 = 0xef, + + // JPEG extensions + JPG0 = 0xf0, + JPG1 = 0xf1, + JPG2 = 0xf2, + JPG3 = 0xf3, + JPG4 = 0xf4, + JPG5 = 0xf5, + JPG6 = 0xf6, + JPG7 = 0xf7, + JPG8 = 0xf8, + JPG9 = 0xf9, + JPG10 = 0xfa, + JPG11 = 0xfb, + JP1G2 = 0xfc, + JPG13 = 0xfd, + + // Comment + COM = 0xfe, + + // Temporary + TEM = 0x01, } diff --git a/src/Umbraco.Core/Media/Exif/JPEGSection.cs b/src/Umbraco.Core/Media/Exif/JPEGSection.cs index 07dd488384..787b04b056 100644 --- a/src/Umbraco.Core/Media/Exif/JPEGSection.cs +++ b/src/Umbraco.Core/Media/Exif/JPEGSection.cs @@ -1,63 +1,66 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the memory view of a JPEG section. +/// A JPEG section is the data between markers of the JPEG file. +/// +internal class JPEGSection { + #region Instance Methods + /// - /// Represents the memory view of a JPEG section. - /// A JPEG section is the data between markers of the JPEG file. + /// Returns a string representation of the current section. /// - internal class JPEGSection + /// A System.String that represents the current section. + public override string ToString() => string.Format("{0} => Header: {1} bytes, Entropy Data: {2} bytes", Marker, Header.Length, EntropyData.Length); + + #endregion + + #region Properties + + /// + /// The marker byte representing the section. + /// + public JPEGMarker Marker { get; } + + /// + /// Section header as a byte array. This is different from the header + /// definition in JPEG specification in that it does not include the + /// two byte section length. + /// + public byte[] Header { get; set; } + + /// + /// For the SOS and RST markers, this contains the entropy coded data. + /// + public byte[] EntropyData { get; set; } + + #endregion + + #region Constructors + + /// + /// Constructs a JPEGSection represented by the marker byte and containing + /// the given data. + /// + /// The marker byte representing the section. + /// Section data. + /// Entropy coded data. + public JPEGSection(JPEGMarker marker, byte[] data, byte[] entropydata) { - #region Properties - /// - /// The marker byte representing the section. - /// - public JPEGMarker Marker { get; private set; } - /// - /// Section header as a byte array. This is different from the header - /// definition in JPEG specification in that it does not include the - /// two byte section length. - /// - public byte[] Header { get; set; } - /// - /// For the SOS and RST markers, this contains the entropy coded data. - /// - public byte[] EntropyData { get; set; } - #endregion - - #region Constructors - /// - /// Constructs a JPEGSection represented by the marker byte and containing - /// the given data. - /// - /// The marker byte representing the section. - /// Section data. - /// Entropy coded data. - public JPEGSection(JPEGMarker marker, byte[] data, byte[] entropydata) - { - Marker = marker; - Header = data; - EntropyData = entropydata; - } - - /// - /// Constructs a JPEGSection represented by the marker byte. - /// - /// The marker byte representing the section. - public JPEGSection(JPEGMarker marker) - : this(marker, new byte[0], new byte[0]) - { - - } - #endregion - - #region Instance Methods - /// - /// Returns a string representation of the current section. - /// - /// A System.String that represents the current section. - public override string ToString() - { - return string.Format("{0} => Header: {1} bytes, Entropy Data: {2} bytes", Marker, Header.Length, EntropyData.Length); - } - #endregion + Marker = marker; + Header = data; + EntropyData = entropydata; } + + /// + /// Constructs a JPEGSection represented by the marker byte. + /// + /// The marker byte representing the section. + public JPEGSection(JPEGMarker marker) + : this(marker, new byte[0], new byte[0]) + { + } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/MathEx.cs b/src/Umbraco.Core/Media/Exif/MathEx.cs index d49ccf924f..fbf5f2dbde 100644 --- a/src/Umbraco.Core/Media/Exif/MathEx.cs +++ b/src/Umbraco.Core/Media/Exif/MathEx.cs @@ -1,1370 +1,1329 @@ -using System; -using System.Globalization; +using System.Globalization; using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Contains extended Math functions. +/// +internal static class MathEx { /// - /// Contains extended Math functions. + /// Returns the greatest common divisor of two numbers. /// - internal static class MathEx + /// First number. + /// Second number. + public static uint GCD(uint a, uint b) { - /// - /// Returns the greatest common divisor of two numbers. - /// - /// First number. - /// Second number. - public static uint GCD(uint a, uint b) + while (b != 0) { - while (b != 0) - { - uint rem = a % b; - a = b; - b = rem; - } - - return a; + var rem = a % b; + a = b; + b = rem; } - /// - /// Returns the greatest common divisor of two numbers. - /// - /// First number. - /// Second number. - public static ulong GCD(ulong a, ulong b) - { - while (b != 0) - { - ulong rem = a % b; - a = b; - b = rem; - } + return a; + } - return a; + /// + /// Returns the greatest common divisor of two numbers. + /// + /// First number. + /// Second number. + public static ulong GCD(ulong a, ulong b) + { + while (b != 0) + { + var rem = a % b; + a = b; + b = rem; } - /// - /// Represents a generic rational number represented by 32-bit signed numerator and denominator. - /// - public struct Fraction32 : IComparable, IFormattable, IComparable, IEquatable - { - #region Constants - private const uint MaximumIterations = 10000000; - #endregion + return a; + } - #region Member Variables - private bool mIsNegative; - private int mNumerator; - private int mDenominator; - private double mError; - #endregion + /// + /// Represents a generic rational number represented by 32-bit signed numerator and denominator. + /// + public struct Fraction32 : IComparable, IFormattable, IComparable, IEquatable + { + #region Constants - #region Properties - /// - /// Gets or sets the numerator. - /// - public int Numerator - { - get - { - return (mIsNegative ? -1 : 1) * mNumerator; - } - set - { - if (value < 0) - { - mIsNegative = true; - mNumerator = -1 * value; - } - else - { - mIsNegative = false; - mNumerator = value; - } - Reduce(ref mNumerator, ref mDenominator); - } - } - /// - /// Gets or sets the denominator. - /// - public int Denominator - { - get - { - return ((int)mDenominator); - } - set - { - mDenominator = System.Math.Abs(value); - Reduce(ref mNumerator, ref mDenominator); - } - } + private const uint MaximumIterations = 10000000; - /// - /// Gets the error term. - /// - public double Error - { - get - { - return mError; - } - } + #endregion - /// - /// Gets or sets a value determining id the fraction is a negative value. - /// - public bool IsNegative - { - get - { - return mIsNegative; - } - set - { - mIsNegative = value; - } - } - #endregion + #region Member Variables - #region Predefined Values - public static readonly Fraction32 NaN = new Fraction32(0, 0); - public static readonly Fraction32 NegativeInfinity = new Fraction32(-1, 0); - public static readonly Fraction32 PositiveInfinity = new Fraction32(1, 0); - #endregion + private int mNumerator; + private int mDenominator; - #region Static Methods - /// - /// Returns a value indicating whether the specified number evaluates to a value - /// that is not a number. - /// - /// A fraction. - /// true if f evaluates to Fraction.NaN; otherwise, false. - public static bool IsNan(Fraction32 f) - { - return (f.Numerator == 0 && f.Denominator == 0); - } - /// - /// Returns a value indicating whether the specified number evaluates to negative - /// infinity. - /// - /// A fraction. - /// true if f evaluates to Fraction.NegativeInfinity; otherwise, false. - public static bool IsNegativeInfinity(Fraction32 f) - { - return (f.Numerator < 0 && f.Denominator == 0); - } - /// - /// Returns a value indicating whether the specified number evaluates to positive - /// infinity. - /// - /// A fraction. - /// true if f evaluates to Fraction.PositiveInfinity; otherwise, false. - public static bool IsPositiveInfinity(Fraction32 f) - { - return (f.Numerator > 0 && f.Denominator == 0); - } - /// - /// Returns a value indicating whether the specified number evaluates to negative - /// or positive infinity. - /// - /// A fraction. - /// true if f evaluates to Fraction.NegativeInfinity or Fraction.PositiveInfinity; otherwise, false. - public static bool IsInfinity(Fraction32 f) - { - return (f.Denominator == 0); - } - /// - /// Returns the multiplicative inverse of a given value. - /// - /// A fraction. - /// Multiplicative inverse of f. - public static Fraction32 Inverse(Fraction32 f) - { - return new Fraction32(f.Denominator, f.Numerator); - } + #endregion - /// - /// Converts the string representation of a fraction to a fraction object. - /// - /// A string formatted as numerator/denominator - /// A fraction object converted from s. - /// s is null - /// s is not in the correct format - /// - /// s represents a number less than System.UInt32.MinValue or greater than - /// System.UInt32.MaxValue. - /// - public static Fraction32 Parse(string s) - { - return FromString(s); - } - - /// - /// Converts the string representation of a fraction to a fraction object. - /// A return value indicates whether the conversion succeeded. - /// - /// A string formatted as numerator/denominator - /// true if s was converted successfully; otherwise, false. - public static bool TryParse(string s, out Fraction32 f) - { - try - { - f = Parse(s); - return true; - } - catch - { - f = new Fraction32(); - return false; - } - } - #endregion - - #region Operators - #region Arithmetic Operators - // Multiplication - public static Fraction32 operator *(Fraction32 f, int n) - { - return new Fraction32(f.Numerator * n, f.Denominator * System.Math.Abs(n)); - } - public static Fraction32 operator *(int n, Fraction32 f) - { - return f * n; - } - public static Fraction32 operator *(Fraction32 f, float n) - { - return new Fraction32(((float)f) * n); - } - public static Fraction32 operator *(float n, Fraction32 f) - { - return f * n; - } - public static Fraction32 operator *(Fraction32 f, double n) - { - return new Fraction32(((double)f) * n); - } - public static Fraction32 operator *(double n, Fraction32 f) - { - return f * n; - } - public static Fraction32 operator *(Fraction32 f1, Fraction32 f2) - { - return new Fraction32(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); - } - // Division - public static Fraction32 operator /(Fraction32 f, int n) - { - return new Fraction32(f.Numerator / n, f.Denominator / System.Math.Abs(n)); - } - public static Fraction32 operator /(Fraction32 f, float n) - { - return new Fraction32(((float)f) / n); - } - public static Fraction32 operator /(Fraction32 f, double n) - { - return new Fraction32(((double)f) / n); - } - public static Fraction32 operator /(Fraction32 f1, Fraction32 f2) - { - return f1 * Inverse(f2); - } - // Addition - public static Fraction32 operator +(Fraction32 f, int n) - { - return f + new Fraction32(n, 1); - } - public static Fraction32 operator +(int n, Fraction32 f) - { - return f + n; - } - public static Fraction32 operator +(Fraction32 f, float n) - { - return new Fraction32(((float)f) + n); - } - public static Fraction32 operator +(float n, Fraction32 f) - { - return f + n; - } - public static Fraction32 operator +(Fraction32 f, double n) - { - return new Fraction32(((double)f) + n); - } - public static Fraction32 operator +(double n, Fraction32 f) - { - return f + n; - } - public static Fraction32 operator +(Fraction32 f1, Fraction32 f2) - { - int n1 = f1.Numerator, d1 = f1.Denominator; - int n2 = f2.Numerator, d2 = f2.Denominator; - - return new Fraction32(n1 * d2 + n2 * d1, d1 * d2); - } - // Subtraction - public static Fraction32 operator -(Fraction32 f, int n) - { - return f - new Fraction32(n, 1); - } - public static Fraction32 operator -(int n, Fraction32 f) - { - return new Fraction32(n, 1) - f; - } - public static Fraction32 operator -(Fraction32 f, float n) - { - return new Fraction32(((float)f) - n); - } - public static Fraction32 operator -(float n, Fraction32 f) - { - return new Fraction32(n) - f; - } - public static Fraction32 operator -(Fraction32 f, double n) - { - return new Fraction32(((double)f) - n); - } - public static Fraction32 operator -(double n, Fraction32 f) - { - return new Fraction32(n) - f; - } - public static Fraction32 operator -(Fraction32 f1, Fraction32 f2) - { - int n1 = f1.Numerator, d1 = f1.Denominator; - int n2 = f2.Numerator, d2 = f2.Denominator; - - return new Fraction32(n1 * d2 - n2 * d1, d1 * d2); - } - // Increment - public static Fraction32 operator ++(Fraction32 f) - { - return f + new Fraction32(1, 1); - } - // Decrement - public static Fraction32 operator --(Fraction32 f) - { - return f - new Fraction32(1, 1); - } - #endregion - #region Casts To Integral Types - public static explicit operator int(Fraction32 f) - { - return f.Numerator / f.Denominator; - } - public static explicit operator float(Fraction32 f) - { - return ((float)f.Numerator) / ((float)f.Denominator); - } - public static explicit operator double(Fraction32 f) - { - return ((double)f.Numerator) / ((double)f.Denominator); - } - #endregion - #region Comparison Operators - public static bool operator ==(Fraction32 f1, Fraction32 f2) - { - return (f1.Numerator == f2.Numerator) && (f1.Denominator == f2.Denominator); - } - public static bool operator !=(Fraction32 f1, Fraction32 f2) - { - return (f1.Numerator != f2.Numerator) || (f1.Denominator != f2.Denominator); - } - public static bool operator <(Fraction32 f1, Fraction32 f2) - { - return (f1.Numerator * f2.Denominator) < (f2.Numerator * f1.Denominator); - } - public static bool operator >(Fraction32 f1, Fraction32 f2) - { - return (f1.Numerator * f2.Denominator) > (f2.Numerator * f1.Denominator); - } - #endregion - #endregion - - #region Constructors - private Fraction32(int numerator, int denominator, double error) - { - mIsNegative = false; - if (numerator < 0) - { - numerator = -numerator; - mIsNegative = !mIsNegative; - } - if (denominator < 0) - { - denominator = -denominator; - mIsNegative = !mIsNegative; - } - - mNumerator = numerator; - mDenominator = denominator; - mError = error; - - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } - - public Fraction32(int numerator, int denominator) - : this(numerator, denominator, 0) - { - - } - - public Fraction32(int numerator) - : this(numerator, (int)1) - { - - } - - public Fraction32(Fraction32 f) - : this(f.Numerator, f.Denominator, f.Error) - { - - } - - public Fraction32(float value) - : this((double)value) - { - - } - - public Fraction32(double value) - : this(FromDouble(value)) - { - - } - - public Fraction32(string s) - : this(FromString(s)) - { - - } - #endregion - - #region Instance Methods - /// - /// Sets the value of this instance to the fraction represented - /// by the given numerator and denominator. - /// - /// The new numerator. - /// The new denominator. - public void Set(int numerator, int denominator) - { - mIsNegative = false; - if (numerator < 0) - { - mIsNegative = !mIsNegative; - numerator = -numerator; - } - if (denominator < 0) - { - mIsNegative = !mIsNegative; - denominator = -denominator; - } - - mNumerator = numerator; - mDenominator = denominator; - - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } - - /// - /// Indicates whether this instance and a specified object are equal value-wise. - /// - /// Another object to compare to. - /// true if obj and this instance are the same type and represent - /// the same value; otherwise, false. - public override bool Equals(object? obj) - { - if (obj == null) - return false; - - if (obj is Fraction32) - return Equals((Fraction32)obj); - else - return false; - } - - /// - /// Indicates whether this instance and a specified object are equal value-wise. - /// - /// Another fraction object to compare to. - /// true if obj and this instance represent the same value; - /// otherwise, false. - public bool Equals(Fraction32 obj) - { - return (mIsNegative == obj.IsNegative) && (mNumerator == obj.Numerator) && (mDenominator == obj.Denominator); - } - - /// - /// Returns the hash code for this instance. - /// - /// A 32-bit signed integer that is the hash code for this instance. - public override int GetHashCode() - { - return mDenominator ^ ((mIsNegative ? -1 : 1) * mNumerator); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A numeric format string. - /// - /// An System.IFormatProvider that supplies culture-specific - /// formatting information. - /// - /// - /// The string representation of the value of this instance as - /// specified by format and provider. - /// - /// - /// format is invalid or not supported. - /// - public string ToString(string? format, IFormatProvider? formatProvider) - { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString(format, formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format, formatProvider)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A numeric format string. - /// - /// The string representation of the value of this instance as - /// specified by format. - /// - /// - /// format is invalid or not supported. - /// - public string ToString(string format) - { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString(format)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// - /// An System.IFormatProvider that supplies culture-specific - /// formatting information. - /// - /// - /// The string representation of the value of this instance as - /// specified by provider. - /// - public string ToString(IFormatProvider formatProvider) - { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString(formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(formatProvider)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A string formatted as numerator/denominator. - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append(((mIsNegative ? -1 : 1) * mNumerator).ToString()); - sb.Append('/'); - sb.Append(mDenominator.ToString()); - return sb.ToString(); - } - - /// - /// Compares this instance to a specified object and returns an indication of - /// their relative values. - /// - /// An object to compare, or null. - /// - /// A signed number indicating the relative values of this instance and value. - /// Less than zero: This instance is less than obj. - /// Zero: This instance is equal to obj. - /// Greater than zero: This instance is greater than obj or obj is null. - /// - /// obj is not a Fraction. - public int CompareTo(object? obj) - { - if (!(obj is Fraction32)) - throw new ArgumentException("obj must be of type Fraction", "obj"); - - return CompareTo((Fraction32)obj); - } - - /// - /// Compares this instance to a specified object and returns an indication of - /// their relative values. - /// - /// An fraction to compare with this instance. - /// - /// A signed number indicating the relative values of this instance and value. - /// Less than zero: This instance is less than obj. - /// Zero: This instance is equal to obj. - /// Greater than zero: This instance is greater than obj or obj is null. - /// - public int CompareTo(Fraction32 obj) - { - if (this < obj) - return -1; - else if (this > obj) - return 1; - return 0; - } - #endregion - - #region Private Helper Methods - /// - /// Converts the given floating-point number to its rational representation. - /// - /// The floating-point number to be converted. - /// The rational representation of value. - private static Fraction32 FromDouble(double value) - { - if (double.IsNaN(value)) - return Fraction32.NaN; - else if (double.IsNegativeInfinity(value)) - return Fraction32.NegativeInfinity; - else if (double.IsPositiveInfinity(value)) - return Fraction32.PositiveInfinity; - - bool isneg = (value < 0); - if (isneg) value = -value; - - double f = value; - double forg = f; - int lnum = 0; - int lden = 1; - int num = 1; - int den = 0; - double lasterr = 1.0; - int a = 0; - int currIteration = 0; - while (true) - { - if (++currIteration > MaximumIterations) break; - - a = (int)Math.Floor(f); - f = f - (double)a; - if (Math.Abs(f) < double.Epsilon) - break; - f = 1.0 / f; - if (double.IsInfinity(f)) - break; - int cnum = num * a + lnum; - int cden = den * a + lden; - if (Math.Abs((double)cnum / (double)cden - forg) < double.Epsilon) - break; - double err = ((double)cnum / (double)cden - (double)num / (double)den) / ((double)num / (double)den); - // Are we converging? - if (err >= lasterr) - break; - lasterr = err; - lnum = num; - lden = den; - num = cnum; - den = cden; - } - - if (den > 0) - lasterr = value - ((double)num / (double)den); - else - lasterr = double.PositiveInfinity; - - return new Fraction32((isneg ? -1 : 1) * num, den, lasterr); - } - - /// Converts the string representation of a fraction to a Fraction type. - /// The input string formatted as numerator/denominator. - /// s is null. - /// s is not formatted as numerator/denominator. - /// - /// s represents numbers less than System.Int32.MinValue or greater than - /// System.Int32.MaxValue. - /// - private static Fraction32 FromString(string s) - { - if (s == null) - throw new ArgumentNullException("s"); - - string[] sa = s.Split(Constants.CharArrays.ForwardSlash); - int numerator = 1; - int denominator = 1; - - if (sa.Length == 1) - { - // Try to parse as int - if (int.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) - { - denominator = 1; - } - else - { - // Parse as double - double dval = double.Parse(sa[0]); - return FromDouble(dval); - } - } - else if (sa.Length == 2) - { - numerator = int.Parse(sa[0], CultureInfo.InvariantCulture); - denominator = int.Parse(sa[1], CultureInfo.InvariantCulture); - } - else - throw new FormatException("The input string must be formatted as n/d where n and d are integers"); - - return new Fraction32(numerator, denominator); - } - - /// - /// Reduces the given numerator and denominator by dividing with their - /// greatest common divisor. - /// - /// numerator to be reduced. - /// denominator to be reduced. - private static void Reduce(ref int numerator, ref int denominator) - { - uint gcd = MathEx.GCD((uint)numerator, (uint)denominator); - if (gcd == 0) gcd = 1; - numerator = numerator / (int)gcd; - denominator = denominator / (int)gcd; - } - #endregion - } + #region Properties /// - /// Represents a generic rational number represented by 32-bit unsigned numerator and denominator. + /// Gets or sets the numerator. /// - public struct UFraction32 : IComparable, IFormattable, IComparable, IEquatable + public int Numerator { - #region Constants - private const uint MaximumIterations = 10000000; - #endregion - - #region Member Variables - private uint mNumerator; - private uint mDenominator; - private double mError; - #endregion - - #region Properties - /// - /// Gets or sets the numerator. - /// - public uint Numerator - { - get - { - return mNumerator; - } - set - { - mNumerator = value; - Reduce(ref mNumerator, ref mDenominator); - } - } - /// - /// Gets or sets the denominator. - /// - public uint Denominator - { - get - { - return mDenominator; - } - set - { - mDenominator = value; - Reduce(ref mNumerator, ref mDenominator); - } - } - - - /// - /// Gets the error term. - /// - public double Error - { - get - { - return mError; - } - } - #endregion - - #region Predefined Values - public static readonly UFraction32 NaN = new UFraction32(0, 0); - public static readonly UFraction32 Infinity = new UFraction32(1, 0); - #endregion - - #region Static Methods - /// - /// Returns a value indicating whether the specified number evaluates to a value - /// that is not a number. - /// - /// A fraction. - /// true if f evaluates to Fraction.NaN; otherwise, false. - public static bool IsNan(UFraction32 f) - { - return (f.Numerator == 0 && f.Denominator == 0); - } - /// - /// Returns a value indicating whether the specified number evaluates to infinity. - /// - /// A fraction. - /// true if f evaluates to Fraction.Infinity; otherwise, false. - public static bool IsInfinity(UFraction32 f) - { - return (f.Denominator == 0); - } - - /// - /// Converts the string representation of a fraction to a fraction object. - /// - /// A string formatted as numerator/denominator - /// A fraction object converted from s. - /// s is null - /// s is not in the correct format - /// - /// s represents a number less than System.UInt32.MinValue or greater than - /// System.UInt32.MaxValue. - /// - public static UFraction32 Parse(string s) - { - return FromString(s); - } - - /// - /// Converts the string representation of a fraction to a fraction object. - /// A return value indicates whether the conversion succeeded. - /// - /// A string formatted as numerator/denominator - /// true if s was converted successfully; otherwise, false. - public static bool TryParse(string s, out UFraction32 f) - { - try - { - f = Parse(s); - return true; - } - catch - { - f = new UFraction32(); - return false; - } - } - #endregion - - #region Operators - #region Arithmetic Operators - // Multiplication - public static UFraction32 operator *(UFraction32 f, uint n) - { - return new UFraction32(f.Numerator * n, f.Denominator * n); - } - public static UFraction32 operator *(uint n, UFraction32 f) - { - return f * n; - } - public static UFraction32 operator *(UFraction32 f, float n) - { - return new UFraction32(((float)f) * n); - } - public static UFraction32 operator *(float n, UFraction32 f) - { - return f * n; - } - public static UFraction32 operator *(UFraction32 f, double n) - { - return new UFraction32(((double)f) * n); - } - public static UFraction32 operator *(double n, UFraction32 f) - { - return f * n; - } - public static UFraction32 operator *(UFraction32 f1, UFraction32 f2) - { - return new UFraction32(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); - } - // Division - public static UFraction32 operator /(UFraction32 f, uint n) - { - return new UFraction32(f.Numerator / n, f.Denominator / n); - } - public static UFraction32 operator /(UFraction32 f, float n) - { - return new UFraction32(((float)f) / n); - } - public static UFraction32 operator /(UFraction32 f, double n) - { - return new UFraction32(((double)f) / n); - } - public static UFraction32 operator /(UFraction32 f1, UFraction32 f2) - { - return f1 * Inverse(f2); - } - // Addition - public static UFraction32 operator +(UFraction32 f, uint n) - { - return f + new UFraction32(n, 1); - } - public static UFraction32 operator +(uint n, UFraction32 f) - { - return f + n; - } - public static UFraction32 operator +(UFraction32 f, float n) - { - return new UFraction32(((float)f) + n); - } - public static UFraction32 operator +(float n, UFraction32 f) - { - return f + n; - } - public static UFraction32 operator +(UFraction32 f, double n) - { - return new UFraction32(((double)f) + n); - } - public static UFraction32 operator +(double n, UFraction32 f) - { - return f + n; - } - public static UFraction32 operator +(UFraction32 f1, UFraction32 f2) - { - uint n1 = f1.Numerator, d1 = f1.Denominator; - uint n2 = f2.Numerator, d2 = f2.Denominator; - - return new UFraction32(n1 * d2 + n2 * d1, d1 * d2); - } - // Subtraction - public static UFraction32 operator -(UFraction32 f, uint n) - { - return f - new UFraction32(n, 1); - } - public static UFraction32 operator -(uint n, UFraction32 f) - { - return new UFraction32(n, 1) - f; - } - public static UFraction32 operator -(UFraction32 f, float n) - { - return new UFraction32(((float)f) - n); - } - public static UFraction32 operator -(float n, UFraction32 f) - { - return new UFraction32(n) - f; - } - public static UFraction32 operator -(UFraction32 f, double n) - { - return new UFraction32(((double)f) - n); - } - public static UFraction32 operator -(double n, UFraction32 f) - { - return new UFraction32(n) - f; - } - public static UFraction32 operator -(UFraction32 f1, UFraction32 f2) - { - uint n1 = f1.Numerator, d1 = f1.Denominator; - uint n2 = f2.Numerator, d2 = f2.Denominator; - - return new UFraction32(n1 * d2 - n2 * d1, d1 * d2); - } - // Increment - public static UFraction32 operator ++(UFraction32 f) - { - return f + new UFraction32(1, 1); - } - // Decrement - public static UFraction32 operator --(UFraction32 f) - { - return f - new UFraction32(1, 1); - } - #endregion - #region Casts To Integral Types - public static explicit operator uint(UFraction32 f) - { - return ((uint)f.Numerator) / ((uint)f.Denominator); - } - public static explicit operator float(UFraction32 f) - { - return ((float)f.Numerator) / ((float)f.Denominator); - } - public static explicit operator double(UFraction32 f) - { - return ((double)f.Numerator) / ((double)f.Denominator); - } - #endregion - #region Comparison Operators - public static bool operator ==(UFraction32 f1, UFraction32 f2) - { - return (f1.Numerator == f2.Numerator) && (f1.Denominator == f2.Denominator); - } - public static bool operator !=(UFraction32 f1, UFraction32 f2) - { - return (f1.Numerator != f2.Numerator) || (f1.Denominator != f2.Denominator); - } - public static bool operator <(UFraction32 f1, UFraction32 f2) - { - return (f1.Numerator * f2.Denominator) < (f2.Numerator * f1.Denominator); - } - public static bool operator >(UFraction32 f1, UFraction32 f2) - { - return (f1.Numerator * f2.Denominator) > (f2.Numerator * f1.Denominator); - } - #endregion - #endregion - - #region Constructors - public UFraction32(uint numerator, uint denominator, double error) - { - mNumerator = numerator; - mDenominator = denominator; - mError = error; - - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } - - public UFraction32(uint numerator, uint denominator) - : this(numerator, denominator, 0) - { - - } - - public UFraction32(uint numerator) - : this(numerator, (uint)1) - { - - } - - public UFraction32(UFraction32 f) - : this(f.Numerator, f.Denominator, f.Error) - { - - } - - public UFraction32(float value) - : this((double)value) - { - - } - - public UFraction32(double value) - : this(FromDouble(value)) - { - - } - - public UFraction32(string s) - : this(FromString(s)) - { - - } - #endregion - - #region Instance Methods - /// - /// Sets the value of this instance to the fraction represented - /// by the given numerator and denominator. - /// - /// The new numerator. - /// The new denominator. - public void Set(uint numerator, uint denominator) - { - mNumerator = numerator; - mDenominator = denominator; - - if (mDenominator != 0) - Reduce(ref mNumerator, ref mDenominator); - } - - /// - /// Returns the multiplicative inverse of a given value. - /// - /// A fraction. - /// Multiplicative inverse of f. - public static UFraction32 Inverse(UFraction32 f) - { - return new UFraction32(f.Denominator, f.Numerator); - } - - /// - /// Indicates whether this instance and a specified object are equal value-wise. - /// - /// Another object to compare to. - /// true if obj and this instance are the same type and represent - /// the same value; otherwise, false. - public override bool Equals(object? obj) - { - if (obj == null) - return false; - - if (obj is UFraction32) - return Equals((UFraction32)obj); - else - return false; - } - - /// - /// Indicates whether this instance and a specified object are equal value-wise. - /// - /// Another fraction object to compare to. - /// true if obj and this instance represent the same value; - /// otherwise, false. - public bool Equals(UFraction32 obj) - { - return (mNumerator == obj.Numerator) && (mDenominator == obj.Denominator); - } - - /// - /// Returns the hash code for this instance. - /// - /// A 32-bit signed integer that is the hash code for this instance. - public override int GetHashCode() - { - return ((int)mDenominator) ^ ((int)mNumerator); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A numeric format string. - /// - /// An System.IFormatProvider that supplies culture-specific - /// formatting information. - /// - /// - /// The string representation of the value of this instance as - /// specified by format and provider. - /// - /// - /// format is invalid or not supported. - /// - public string ToString(string? format, IFormatProvider? formatProvider) - { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString(format, formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format, formatProvider)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A numeric format string. - /// - /// The string representation of the value of this instance as - /// specified by format. - /// - /// - /// format is invalid or not supported. - /// - public string ToString(string format) - { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString(format)); - sb.Append('/'); - sb.Append(mDenominator.ToString(format)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// - /// An System.IFormatProvider that supplies culture-specific - /// formatting information. - /// - /// - /// The string representation of the value of this instance as - /// specified by provider. - /// - public string ToString(IFormatProvider formatProvider) - { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString(formatProvider)); - sb.Append('/'); - sb.Append(mDenominator.ToString(formatProvider)); - return sb.ToString(); - } - - /// - /// Returns a string representation of the fraction. - /// - /// A string formatted as numerator/denominator. - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append(mNumerator.ToString()); - sb.Append('/'); - sb.Append(mDenominator.ToString()); - return sb.ToString(); - } - - /// - /// Compares this instance to a specified object and returns an indication of - /// their relative values. - /// - /// An object to compare, or null. - /// - /// A signed number indicating the relative values of this instance and value. - /// Less than zero: This instance is less than obj. - /// Zero: This instance is equal to obj. - /// Greater than zero: This instance is greater than obj or obj is null. - /// - /// obj is not a Fraction. - public int CompareTo(object? obj) - { - if (!(obj is UFraction32)) - throw new ArgumentException("obj must be of type UFraction32", "obj"); - - return CompareTo((UFraction32)obj); - } - - /// - /// Compares this instance to a specified object and returns an indication of - /// their relative values. - /// - /// An fraction to compare with this instance. - /// - /// A signed number indicating the relative values of this instance and value. - /// Less than zero: This instance is less than obj. - /// Zero: This instance is equal to obj. - /// Greater than zero: This instance is greater than obj or obj is null. - /// - public int CompareTo(UFraction32 obj) - { - if (this < obj) - return -1; - else if (this > obj) - return 1; - return 0; - } - #endregion - - #region Private Helper Methods - /// - /// Converts the given floating-point number to its rational representation. - /// - /// The floating-point number to be converted. - /// The rational representation of value. - private static UFraction32 FromDouble(double value) + get => (IsNegative ? -1 : 1) * mNumerator; + set { if (value < 0) - throw new ArgumentException("value cannot be negative.", "value"); - - if (double.IsNaN(value)) - return UFraction32.NaN; - else if (double.IsInfinity(value)) - return UFraction32.Infinity; - - double f = value; - double forg = f; - uint lnum = 0; - uint lden = 1; - uint num = 1; - uint den = 0; - double lasterr = 1.0; - uint a = 0; - int currIteration = 0; - while (true) { - if (++currIteration > MaximumIterations) break; - - a = (uint)Math.Floor(f); - f = f - (double)a; - if (Math.Abs(f) < double.Epsilon) - break; - f = 1.0 / f; - if (double.IsInfinity(f)) - break; - uint cnum = num * a + lnum; - uint cden = den * a + lden; - if (Math.Abs((double)cnum / (double)cden - forg) < double.Epsilon) - break; - double err = ((double)cnum / (double)cden - (double)num / (double)den) / ((double)num / (double)den); - // Are we converging? - if (err >= lasterr) - break; - lasterr = err; - lnum = num; - lden = den; - num = cnum; - den = cden; - } - uint fnum = num * a + lnum; - uint fden = den * a + lden; - - if (fden > 0) - lasterr = value - ((double)fnum / (double)fden); - else - lasterr = double.PositiveInfinity; - - return new UFraction32(fnum, fden, lasterr); - } - - /// Converts the string representation of a fraction to a Fraction type. - /// The input string formatted as numerator/denominator. - /// s is null. - /// s is not formatted as numerator/denominator. - /// - /// s represents numbers less than System.UInt32.MinValue or greater than - /// System.UInt32.MaxValue. - /// - private static UFraction32 FromString(string s) - { - if (s == null) - throw new ArgumentNullException("s"); - - string[] sa = s.Split(Constants.CharArrays.ForwardSlash); - uint numerator = 1; - uint denominator = 1; - - if (sa.Length == 1) - { - // Try to parse as uint - if (uint.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) - { - denominator = 1; - } - else - { - // Parse as double - double dval = double.Parse(sa[0]); - return FromDouble(dval); - } - } - else if (sa.Length == 2) - { - numerator = uint.Parse(sa[0], CultureInfo.InvariantCulture); - denominator = uint.Parse(sa[1], CultureInfo.InvariantCulture); + IsNegative = true; + mNumerator = -1 * value; } else - throw new FormatException("The input string must be formatted as n/d where n and d are integers"); + { + IsNegative = false; + mNumerator = value; + } - return new UFraction32(numerator, denominator); + Reduce(ref mNumerator, ref mDenominator); } - - /// - /// Reduces the given numerator and denominator by dividing with their - /// greatest common divisor. - /// - /// numerator to be reduced. - /// denominator to be reduced. - private static void Reduce(ref uint numerator, ref uint denominator) - { - uint gcd = MathEx.GCD(numerator, denominator); - numerator = numerator / gcd; - denominator = denominator / gcd; - } - #endregion } + + /// + /// Gets or sets the denominator. + /// + public int Denominator + { + get => mDenominator; + set + { + mDenominator = Math.Abs(value); + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Gets the error term. + /// + public double Error { get; } + + /// + /// Gets or sets a value determining id the fraction is a negative value. + /// + public bool IsNegative { get; set; } + + #endregion + + #region Predefined Values + + public static readonly Fraction32 NaN = new(0, 0); + public static readonly Fraction32 NegativeInfinity = new(-1, 0); + public static readonly Fraction32 PositiveInfinity = new(1, 0); + + #endregion + + #region Static Methods + + /// + /// Returns a value indicating whether the specified number evaluates to a value + /// that is not a number. + /// + /// A fraction. + /// true if f evaluates to Fraction.NaN; otherwise, false. + public static bool IsNan(Fraction32 f) => f.Numerator == 0 && f.Denominator == 0; + + /// + /// Returns a value indicating whether the specified number evaluates to negative + /// infinity. + /// + /// A fraction. + /// true if f evaluates to Fraction.NegativeInfinity; otherwise, false. + public static bool IsNegativeInfinity(Fraction32 f) => f.Numerator < 0 && f.Denominator == 0; + + /// + /// Returns a value indicating whether the specified number evaluates to positive + /// infinity. + /// + /// A fraction. + /// true if f evaluates to Fraction.PositiveInfinity; otherwise, false. + public static bool IsPositiveInfinity(Fraction32 f) => f.Numerator > 0 && f.Denominator == 0; + + /// + /// Returns a value indicating whether the specified number evaluates to negative + /// or positive infinity. + /// + /// A fraction. + /// true if f evaluates to Fraction.NegativeInfinity or Fraction.PositiveInfinity; otherwise, false. + public static bool IsInfinity(Fraction32 f) => f.Denominator == 0; + + /// + /// Returns the multiplicative inverse of a given value. + /// + /// A fraction. + /// Multiplicative inverse of f. + public static Fraction32 Inverse(Fraction32 f) => new(f.Denominator, f.Numerator); + + /// + /// Converts the string representation of a fraction to a fraction object. + /// + /// A string formatted as numerator/denominator + /// A fraction object converted from s. + /// s is null + /// s is not in the correct format + /// + /// s represents a number less than System.UInt32.MinValue or greater than + /// System.UInt32.MaxValue. + /// + public static Fraction32 Parse(string s) => FromString(s); + + /// + /// Converts the string representation of a fraction to a fraction object. + /// A return value indicates whether the conversion succeeded. + /// + /// A string formatted as numerator/denominator + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string s, out Fraction32 f) + { + try + { + f = Parse(s); + return true; + } + catch + { + f = new Fraction32(); + return false; + } + } + + #endregion + + #region Operators + + #region Arithmetic Operators + + // Multiplication + public static Fraction32 operator *(Fraction32 f, int n) => new(f.Numerator * n, f.Denominator * Math.Abs(n)); + + public static Fraction32 operator *(int n, Fraction32 f) => f * n; + + public static Fraction32 operator *(Fraction32 f, float n) => new((float)f * n); + + public static Fraction32 operator *(float n, Fraction32 f) => f * n; + + public static Fraction32 operator *(Fraction32 f, double n) => new((double)f * n); + + public static Fraction32 operator *(double n, Fraction32 f) => f * n; + + public static Fraction32 operator *(Fraction32 f1, Fraction32 f2) => + new(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); + + // Division + public static Fraction32 operator /(Fraction32 f, int n) => new(f.Numerator / n, f.Denominator / Math.Abs(n)); + + public static Fraction32 operator /(Fraction32 f, float n) => new((float)f / n); + + public static Fraction32 operator /(Fraction32 f, double n) => new((double)f / n); + + public static Fraction32 operator /(Fraction32 f1, Fraction32 f2) => f1 * Inverse(f2); + + // Addition + public static Fraction32 operator +(Fraction32 f, int n) => f + new Fraction32(n, 1); + + public static Fraction32 operator +(int n, Fraction32 f) => f + n; + + public static Fraction32 operator +(Fraction32 f, float n) => new((float)f + n); + + public static Fraction32 operator +(float n, Fraction32 f) => f + n; + + public static Fraction32 operator +(Fraction32 f, double n) => new((double)f + n); + + public static Fraction32 operator +(double n, Fraction32 f) => f + n; + + public static Fraction32 operator +(Fraction32 f1, Fraction32 f2) + { + int n1 = f1.Numerator, d1 = f1.Denominator; + int n2 = f2.Numerator, d2 = f2.Denominator; + + return new Fraction32((n1 * d2) + (n2 * d1), d1 * d2); + } + + // Subtraction + public static Fraction32 operator -(Fraction32 f, int n) => f - new Fraction32(n, 1); + + public static Fraction32 operator -(int n, Fraction32 f) => new Fraction32(n, 1) - f; + + public static Fraction32 operator -(Fraction32 f, float n) => new((float)f - n); + + public static Fraction32 operator -(float n, Fraction32 f) => new Fraction32(n) - f; + + public static Fraction32 operator -(Fraction32 f, double n) => new((double)f - n); + + public static Fraction32 operator -(double n, Fraction32 f) => new Fraction32(n) - f; + + public static Fraction32 operator -(Fraction32 f1, Fraction32 f2) + { + int n1 = f1.Numerator, d1 = f1.Denominator; + int n2 = f2.Numerator, d2 = f2.Denominator; + + return new Fraction32((n1 * d2) - (n2 * d1), d1 * d2); + } + + // Increment + public static Fraction32 operator ++(Fraction32 f) => f + new Fraction32(1, 1); + + // Decrement + public static Fraction32 operator --(Fraction32 f) => f - new Fraction32(1, 1); + + #endregion + + #region Casts To Integral Types + + public static explicit operator int(Fraction32 f) => f.Numerator / f.Denominator; + + public static explicit operator float(Fraction32 f) => f.Numerator / (float)f.Denominator; + + public static explicit operator double(Fraction32 f) => f.Numerator / (double)f.Denominator; + + #endregion + + #region Comparison Operators + + public static bool operator ==(Fraction32 f1, Fraction32 f2) => + f1.Numerator == f2.Numerator && f1.Denominator == f2.Denominator; + + public static bool operator !=(Fraction32 f1, Fraction32 f2) => + f1.Numerator != f2.Numerator || f1.Denominator != f2.Denominator; + + public static bool operator <(Fraction32 f1, Fraction32 f2) => + f1.Numerator * f2.Denominator < f2.Numerator * f1.Denominator; + + public static bool operator >(Fraction32 f1, Fraction32 f2) => + f1.Numerator * f2.Denominator > f2.Numerator * f1.Denominator; + + #endregion + + #endregion + + #region Constructors + + private Fraction32(int numerator, int denominator, double error) + { + IsNegative = false; + if (numerator < 0) + { + numerator = -numerator; + IsNegative = !IsNegative; + } + + if (denominator < 0) + { + denominator = -denominator; + IsNegative = !IsNegative; + } + + mNumerator = numerator; + mDenominator = denominator; + Error = error; + + if (mDenominator != 0) + { + Reduce(ref mNumerator, ref mDenominator); + } + } + + public Fraction32(int numerator, int denominator) + : this(numerator, denominator, 0) + { + } + + public Fraction32(int numerator) + : this(numerator, 1) + { + } + + public Fraction32(Fraction32 f) + : this(f.Numerator, f.Denominator, f.Error) + { + } + + public Fraction32(float value) + : this((double)value) + { + } + + public Fraction32(double value) + : this(FromDouble(value)) + { + } + + public Fraction32(string s) + : this(FromString(s)) + { + } + + #endregion + + #region Instance Methods + + /// + /// Sets the value of this instance to the fraction represented + /// by the given numerator and denominator. + /// + /// The new numerator. + /// The new denominator. + public void Set(int numerator, int denominator) + { + IsNegative = false; + if (numerator < 0) + { + IsNegative = !IsNegative; + numerator = -numerator; + } + + if (denominator < 0) + { + IsNegative = !IsNegative; + denominator = -denominator; + } + + mNumerator = numerator; + mDenominator = denominator; + + if (mDenominator != 0) + { + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Indicates whether this instance and a specified object are equal value-wise. + /// + /// Another object to compare to. + /// + /// true if obj and this instance are the same type and represent + /// the same value; otherwise, false. + /// + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is Fraction32) + { + return Equals((Fraction32)obj); + } + + return false; + } + + /// + /// Indicates whether this instance and a specified object are equal value-wise. + /// + /// Another fraction object to compare to. + /// + /// true if obj and this instance represent the same value; + /// otherwise, false. + /// + public bool Equals(Fraction32 obj) => IsNegative == obj.IsNegative && mNumerator == obj.Numerator && + mDenominator == obj.Denominator; + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer that is the hash code for this instance. + public override int GetHashCode() => mDenominator ^ ((IsNegative ? -1 : 1) * mNumerator); + + /// + /// Returns a string representation of the fraction. + /// + /// A numeric format string. + /// + /// An System.IFormatProvider that supplies culture-specific + /// formatting information. + /// + /// + /// The string representation of the value of this instance as + /// specified by format and provider. + /// + /// + /// format is invalid or not supported. + /// + public string ToString(string? format, IFormatProvider? formatProvider) + { + var sb = new StringBuilder(); + sb.Append(((IsNegative ? -1 : 1) * mNumerator).ToString(format, formatProvider)); + sb.Append('/'); + sb.Append(mDenominator.ToString(format, formatProvider)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// A numeric format string. + /// + /// The string representation of the value of this instance as + /// specified by format. + /// + /// + /// format is invalid or not supported. + /// + public string ToString(string format) + { + var sb = new StringBuilder(); + sb.Append(((IsNegative ? -1 : 1) * mNumerator).ToString(format)); + sb.Append('/'); + sb.Append(mDenominator.ToString(format)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// + /// An System.IFormatProvider that supplies culture-specific + /// formatting information. + /// + /// + /// The string representation of the value of this instance as + /// specified by provider. + /// + public string ToString(IFormatProvider formatProvider) + { + var sb = new StringBuilder(); + sb.Append(((IsNegative ? -1 : 1) * mNumerator).ToString(formatProvider)); + sb.Append('/'); + sb.Append(mDenominator.ToString(formatProvider)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// A string formatted as numerator/denominator. + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(((IsNegative ? -1 : 1) * mNumerator).ToString()); + sb.Append('/'); + sb.Append(mDenominator.ToString()); + return sb.ToString(); + } + + /// + /// Compares this instance to a specified object and returns an indication of + /// their relative values. + /// + /// An object to compare, or null. + /// + /// A signed number indicating the relative values of this instance and value. + /// Less than zero: This instance is less than obj. + /// Zero: This instance is equal to obj. + /// Greater than zero: This instance is greater than obj or obj is null. + /// + /// obj is not a Fraction. + public int CompareTo(object? obj) + { + if (!(obj is Fraction32)) + { + throw new ArgumentException("obj must be of type Fraction", "obj"); + } + + return CompareTo((Fraction32)obj); + } + + /// + /// Compares this instance to a specified object and returns an indication of + /// their relative values. + /// + /// An fraction to compare with this instance. + /// + /// A signed number indicating the relative values of this instance and value. + /// Less than zero: This instance is less than obj. + /// Zero: This instance is equal to obj. + /// Greater than zero: This instance is greater than obj or obj is null. + /// + public int CompareTo(Fraction32 obj) + { + if (this < obj) + { + return -1; + } + + if (this > obj) + { + return 1; + } + + return 0; + } + + #endregion + + #region Private Helper Methods + + /// + /// Converts the given floating-point number to its rational representation. + /// + /// The floating-point number to be converted. + /// The rational representation of value. + private static Fraction32 FromDouble(double value) + { + if (double.IsNaN(value)) + { + return NaN; + } + + if (double.IsNegativeInfinity(value)) + { + return NegativeInfinity; + } + + if (double.IsPositiveInfinity(value)) + { + return PositiveInfinity; + } + + var isneg = value < 0; + if (isneg) + { + value = -value; + } + + var f = value; + var forg = f; + var lnum = 0; + var lden = 1; + var num = 1; + var den = 0; + var lasterr = 1.0; + var a = 0; + var currIteration = 0; + while (true) + { + if (++currIteration > MaximumIterations) + { + break; + } + + a = (int)Math.Floor(f); + f = f - a; + if (Math.Abs(f) < double.Epsilon) + { + break; + } + + f = 1.0 / f; + if (double.IsInfinity(f)) + { + break; + } + + var cnum = (num * a) + lnum; + var cden = (den * a) + lden; + if (Math.Abs((cnum / (double)cden) - forg) < double.Epsilon) + { + break; + } + + var err = ((cnum / (double)cden) - (num / (double)den)) / (num / (double)den); + + // Are we converging? + if (err >= lasterr) + { + break; + } + + lasterr = err; + lnum = num; + lden = den; + num = cnum; + den = cden; + } + + if (den > 0) + { + lasterr = value - (num / (double)den); + } + else + { + lasterr = double.PositiveInfinity; + } + + return new Fraction32((isneg ? -1 : 1) * num, den, lasterr); + } + + /// Converts the string representation of a fraction to a Fraction type. + /// The input string formatted as numerator/denominator. + /// s is null. + /// s is not formatted as numerator/denominator. + /// + /// s represents numbers less than System.Int32.MinValue or greater than + /// System.Int32.MaxValue. + /// + private static Fraction32 FromString(string s) + { + if (s == null) + { + throw new ArgumentNullException("s"); + } + + var sa = s.Split(Constants.CharArrays.ForwardSlash); + var numerator = 1; + var denominator = 1; + + if (sa.Length == 1) + { + // Try to parse as int + if (int.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) + { + denominator = 1; + } + else + { + // Parse as double + var dval = double.Parse(sa[0]); + return FromDouble(dval); + } + } + else if (sa.Length == 2) + { + numerator = int.Parse(sa[0], CultureInfo.InvariantCulture); + denominator = int.Parse(sa[1], CultureInfo.InvariantCulture); + } + else + { + throw new FormatException("The input string must be formatted as n/d where n and d are integers"); + } + + return new Fraction32(numerator, denominator); + } + + /// + /// Reduces the given numerator and denominator by dividing with their + /// greatest common divisor. + /// + /// numerator to be reduced. + /// denominator to be reduced. + private static void Reduce(ref int numerator, ref int denominator) + { + var gcd = GCD((uint)numerator, (uint)denominator); + if (gcd == 0) + { + gcd = 1; + } + + numerator = numerator / (int)gcd; + denominator = denominator / (int)gcd; + } + + #endregion + } + + /// + /// Represents a generic rational number represented by 32-bit unsigned numerator and denominator. + /// + public struct UFraction32 : IComparable, IFormattable, IComparable, IEquatable + { + #region Constants + + private const uint MaximumIterations = 10000000; + + #endregion + + #region Member Variables + + private uint mNumerator; + private uint mDenominator; + + #endregion + + #region Properties + + /// + /// Gets or sets the numerator. + /// + public uint Numerator + { + get => mNumerator; + set + { + mNumerator = value; + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Gets or sets the denominator. + /// + public uint Denominator + { + get => mDenominator; + set + { + mDenominator = value; + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Gets the error term. + /// + public double Error { get; } + + #endregion + + #region Predefined Values + + public static readonly UFraction32 NaN = new(0, 0); + public static readonly UFraction32 Infinity = new(1, 0); + + #endregion + + #region Static Methods + + /// + /// Returns a value indicating whether the specified number evaluates to a value + /// that is not a number. + /// + /// A fraction. + /// true if f evaluates to Fraction.NaN; otherwise, false. + public static bool IsNan(UFraction32 f) => f.Numerator == 0 && f.Denominator == 0; + + /// + /// Returns a value indicating whether the specified number evaluates to infinity. + /// + /// A fraction. + /// true if f evaluates to Fraction.Infinity; otherwise, false. + public static bool IsInfinity(UFraction32 f) => f.Denominator == 0; + + /// + /// Converts the string representation of a fraction to a fraction object. + /// + /// A string formatted as numerator/denominator + /// A fraction object converted from s. + /// s is null + /// s is not in the correct format + /// + /// s represents a number less than System.UInt32.MinValue or greater than + /// System.UInt32.MaxValue. + /// + public static UFraction32 Parse(string s) => FromString(s); + + /// + /// Converts the string representation of a fraction to a fraction object. + /// A return value indicates whether the conversion succeeded. + /// + /// A string formatted as numerator/denominator + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string s, out UFraction32 f) + { + try + { + f = Parse(s); + return true; + } + catch + { + f = new UFraction32(); + return false; + } + } + + #endregion + + #region Operators + + #region Arithmetic Operators + + // Multiplication + public static UFraction32 operator *(UFraction32 f, uint n) => new(f.Numerator * n, f.Denominator * n); + + public static UFraction32 operator *(uint n, UFraction32 f) => f * n; + + public static UFraction32 operator *(UFraction32 f, float n) => new((float)f * n); + + public static UFraction32 operator *(float n, UFraction32 f) => f * n; + + public static UFraction32 operator *(UFraction32 f, double n) => new((double)f * n); + + public static UFraction32 operator *(double n, UFraction32 f) => f * n; + + public static UFraction32 operator *(UFraction32 f1, UFraction32 f2) => + new(f1.Numerator * f2.Numerator, f1.Denominator * f2.Denominator); + + // Division + public static UFraction32 operator /(UFraction32 f, uint n) => new(f.Numerator / n, f.Denominator / n); + + public static UFraction32 operator /(UFraction32 f, float n) => new((float)f / n); + + public static UFraction32 operator /(UFraction32 f, double n) => new((double)f / n); + + public static UFraction32 operator /(UFraction32 f1, UFraction32 f2) => f1 * Inverse(f2); + + // Addition + public static UFraction32 operator +(UFraction32 f, uint n) => f + new UFraction32(n, 1); + + public static UFraction32 operator +(uint n, UFraction32 f) => f + n; + + public static UFraction32 operator +(UFraction32 f, float n) => new((float)f + n); + + public static UFraction32 operator +(float n, UFraction32 f) => f + n; + + public static UFraction32 operator +(UFraction32 f, double n) => new((double)f + n); + + public static UFraction32 operator +(double n, UFraction32 f) => f + n; + + public static UFraction32 operator +(UFraction32 f1, UFraction32 f2) + { + uint n1 = f1.Numerator, d1 = f1.Denominator; + uint n2 = f2.Numerator, d2 = f2.Denominator; + + return new UFraction32((n1 * d2) + (n2 * d1), d1 * d2); + } + + // Subtraction + public static UFraction32 operator -(UFraction32 f, uint n) => f - new UFraction32(n, 1); + + public static UFraction32 operator -(uint n, UFraction32 f) => new UFraction32(n, 1) - f; + + public static UFraction32 operator -(UFraction32 f, float n) => new((float)f - n); + + public static UFraction32 operator -(float n, UFraction32 f) => new UFraction32(n) - f; + + public static UFraction32 operator -(UFraction32 f, double n) => new((double)f - n); + + public static UFraction32 operator -(double n, UFraction32 f) => new UFraction32(n) - f; + + public static UFraction32 operator -(UFraction32 f1, UFraction32 f2) + { + uint n1 = f1.Numerator, d1 = f1.Denominator; + uint n2 = f2.Numerator, d2 = f2.Denominator; + + return new UFraction32((n1 * d2) - (n2 * d1), d1 * d2); + } + + // Increment + public static UFraction32 operator ++(UFraction32 f) => f + new UFraction32(1, 1); + + // Decrement + public static UFraction32 operator --(UFraction32 f) => f - new UFraction32(1, 1); + + #endregion + + #region Casts To Integral Types + + public static explicit operator uint(UFraction32 f) => f.Numerator / f.Denominator; + + public static explicit operator float(UFraction32 f) => f.Numerator / (float)f.Denominator; + public static explicit operator double(UFraction32 f) => f.Numerator / (double)f.Denominator; + + #endregion + + #region Comparison Operators + + public static bool operator ==(UFraction32 f1, UFraction32 f2) => + f1.Numerator == f2.Numerator && f1.Denominator == f2.Denominator; + + public static bool operator !=(UFraction32 f1, UFraction32 f2) => + f1.Numerator != f2.Numerator || f1.Denominator != f2.Denominator; + + public static bool operator <(UFraction32 f1, UFraction32 f2) => + f1.Numerator * f2.Denominator < f2.Numerator * f1.Denominator; + + public static bool operator >(UFraction32 f1, UFraction32 f2) => + f1.Numerator * f2.Denominator > f2.Numerator * f1.Denominator; + + #endregion + + #endregion + + #region Constructors + + public UFraction32(uint numerator, uint denominator, double error) + { + mNumerator = numerator; + mDenominator = denominator; + Error = error; + + if (mDenominator != 0) + { + Reduce(ref mNumerator, ref mDenominator); + } + } + + public UFraction32(uint numerator, uint denominator) + : this(numerator, denominator, 0) + { + } + + public UFraction32(uint numerator) + : this(numerator, 1) + { + } + + public UFraction32(UFraction32 f) + : this(f.Numerator, f.Denominator, f.Error) + { + } + + public UFraction32(float value) + : this((double)value) + { + } + + public UFraction32(double value) + : this(FromDouble(value)) + { + } + + public UFraction32(string s) + : this(FromString(s)) + { + } + + #endregion + + #region Instance Methods + + /// + /// Sets the value of this instance to the fraction represented + /// by the given numerator and denominator. + /// + /// The new numerator. + /// The new denominator. + public void Set(uint numerator, uint denominator) + { + mNumerator = numerator; + mDenominator = denominator; + + if (mDenominator != 0) + { + Reduce(ref mNumerator, ref mDenominator); + } + } + + /// + /// Returns the multiplicative inverse of a given value. + /// + /// A fraction. + /// Multiplicative inverse of f. + public static UFraction32 Inverse(UFraction32 f) => new(f.Denominator, f.Numerator); + + /// + /// Indicates whether this instance and a specified object are equal value-wise. + /// + /// Another object to compare to. + /// + /// true if obj and this instance are the same type and represent + /// the same value; otherwise, false. + /// + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is UFraction32) + { + return Equals((UFraction32)obj); + } + + return false; + } + + /// + /// Indicates whether this instance and a specified object are equal value-wise. + /// + /// Another fraction object to compare to. + /// + /// true if obj and this instance represent the same value; + /// otherwise, false. + /// + public bool Equals(UFraction32 obj) => mNumerator == obj.Numerator && mDenominator == obj.Denominator; + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer that is the hash code for this instance. + public override int GetHashCode() => (int)mDenominator ^ (int)mNumerator; + + /// + /// Returns a string representation of the fraction. + /// + /// A numeric format string. + /// + /// An System.IFormatProvider that supplies culture-specific + /// formatting information. + /// + /// + /// The string representation of the value of this instance as + /// specified by format and provider. + /// + /// + /// format is invalid or not supported. + /// + public string ToString(string? format, IFormatProvider? formatProvider) + { + var sb = new StringBuilder(); + sb.Append(mNumerator.ToString(format, formatProvider)); + sb.Append('/'); + sb.Append(mDenominator.ToString(format, formatProvider)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// A numeric format string. + /// + /// The string representation of the value of this instance as + /// specified by format. + /// + /// + /// format is invalid or not supported. + /// + public string ToString(string format) + { + var sb = new StringBuilder(); + sb.Append(mNumerator.ToString(format)); + sb.Append('/'); + sb.Append(mDenominator.ToString(format)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// + /// An System.IFormatProvider that supplies culture-specific + /// formatting information. + /// + /// + /// The string representation of the value of this instance as + /// specified by provider. + /// + public string ToString(IFormatProvider formatProvider) + { + var sb = new StringBuilder(); + sb.Append(mNumerator.ToString(formatProvider)); + sb.Append('/'); + sb.Append(mDenominator.ToString(formatProvider)); + return sb.ToString(); + } + + /// + /// Returns a string representation of the fraction. + /// + /// A string formatted as numerator/denominator. + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(mNumerator.ToString()); + sb.Append('/'); + sb.Append(mDenominator.ToString()); + return sb.ToString(); + } + + /// + /// Compares this instance to a specified object and returns an indication of + /// their relative values. + /// + /// An object to compare, or null. + /// + /// A signed number indicating the relative values of this instance and value. + /// Less than zero: This instance is less than obj. + /// Zero: This instance is equal to obj. + /// Greater than zero: This instance is greater than obj or obj is null. + /// + /// obj is not a Fraction. + public int CompareTo(object? obj) + { + if (!(obj is UFraction32)) + { + throw new ArgumentException("obj must be of type UFraction32", "obj"); + } + + return CompareTo((UFraction32)obj); + } + + /// + /// Compares this instance to a specified object and returns an indication of + /// their relative values. + /// + /// An fraction to compare with this instance. + /// + /// A signed number indicating the relative values of this instance and value. + /// Less than zero: This instance is less than obj. + /// Zero: This instance is equal to obj. + /// Greater than zero: This instance is greater than obj or obj is null. + /// + public int CompareTo(UFraction32 obj) + { + if (this < obj) + { + return -1; + } + + if (this > obj) + { + return 1; + } + + return 0; + } + + #endregion + + #region Private Helper Methods + + /// + /// Converts the given floating-point number to its rational representation. + /// + /// The floating-point number to be converted. + /// The rational representation of value. + private static UFraction32 FromDouble(double value) + { + if (value < 0) + { + throw new ArgumentException("value cannot be negative.", "value"); + } + + if (double.IsNaN(value)) + { + return NaN; + } + + if (double.IsInfinity(value)) + { + return Infinity; + } + + var f = value; + var forg = f; + uint lnum = 0; + uint lden = 1; + uint num = 1; + uint den = 0; + var lasterr = 1.0; + uint a = 0; + var currIteration = 0; + while (true) + { + if (++currIteration > MaximumIterations) + { + break; + } + + a = (uint)Math.Floor(f); + f = f - a; + if (Math.Abs(f) < double.Epsilon) + { + break; + } + + f = 1.0 / f; + if (double.IsInfinity(f)) + { + break; + } + + var cnum = (num * a) + lnum; + var cden = (den * a) + lden; + if (Math.Abs((cnum / (double)cden) - forg) < double.Epsilon) + { + break; + } + + var err = ((cnum / (double)cden) - (num / (double)den)) / (num / (double)den); + + // Are we converging? + if (err >= lasterr) + { + break; + } + + lasterr = err; + lnum = num; + lden = den; + num = cnum; + den = cden; + } + + var fnum = (num * a) + lnum; + var fden = (den * a) + lden; + + if (fden > 0) + { + lasterr = value - (fnum / (double)fden); + } + else + { + lasterr = double.PositiveInfinity; + } + + return new UFraction32(fnum, fden, lasterr); + } + + /// Converts the string representation of a fraction to a Fraction type. + /// The input string formatted as numerator/denominator. + /// s is null. + /// s is not formatted as numerator/denominator. + /// + /// s represents numbers less than System.UInt32.MinValue or greater than + /// System.UInt32.MaxValue. + /// + private static UFraction32 FromString(string s) + { + if (s == null) + { + throw new ArgumentNullException("s"); + } + + var sa = s.Split(Constants.CharArrays.ForwardSlash); + uint numerator = 1; + uint denominator = 1; + + if (sa.Length == 1) + { + // Try to parse as uint + if (uint.TryParse(sa[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out numerator)) + { + denominator = 1; + } + else + { + // Parse as double + var dval = double.Parse(sa[0]); + return FromDouble(dval); + } + } + else if (sa.Length == 2) + { + numerator = uint.Parse(sa[0], CultureInfo.InvariantCulture); + denominator = uint.Parse(sa[1], CultureInfo.InvariantCulture); + } + else + { + throw new FormatException("The input string must be formatted as n/d where n and d are integers"); + } + + return new UFraction32(numerator, denominator); + } + + /// + /// Reduces the given numerator and denominator by dividing with their + /// greatest common divisor. + /// + /// numerator to be reduced. + /// denominator to be reduced. + private static void Reduce(ref uint numerator, ref uint denominator) + { + var gcd = GCD(numerator, denominator); + numerator = numerator / gcd; + denominator = denominator / gcd; + } + + #endregion } } diff --git a/src/Umbraco.Core/Media/Exif/SvgFile.cs b/src/Umbraco.Core/Media/Exif/SvgFile.cs index b83aebe1fb..08326e634c 100644 --- a/src/Umbraco.Core/Media/Exif/SvgFile.cs +++ b/src/Umbraco.Core/Media/Exif/SvgFile.cs @@ -1,32 +1,31 @@ -using System.Globalization; -using System.IO; -using System.Linq; +using System.Globalization; using System.Xml.Linq; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +internal class SvgFile : ImageFile { - internal class SvgFile : ImageFile + public SvgFile(Stream fileStream) { - public SvgFile(Stream fileStream) - { - fileStream.Position = 0; + fileStream.Position = 0; - var document = XDocument.Load(fileStream); //if it throws an exception the ugly try catch in MediaFileSystem will catch it + var document = + XDocument.Load(fileStream); // if it throws an exception the ugly try catch in MediaFileSystem will catch it - var width = document.Root?.Attributes().Where(x => x.Name == "width").Select(x => x.Value).FirstOrDefault(); - var height = document.Root?.Attributes().Where(x => x.Name == "height").Select(x => x.Value).FirstOrDefault(); + var width = document.Root?.Attributes().Where(x => x.Name == "width").Select(x => x.Value).FirstOrDefault(); + var height = document.Root?.Attributes().Where(x => x.Name == "height").Select(x => x.Value).FirstOrDefault(); - Properties.Add(new ExifSInt(ExifTag.PixelYDimension, - height == null ? Constants.Conventions.Media.DefaultSize : int.Parse(height, CultureInfo.InvariantCulture))); - Properties.Add(new ExifSInt(ExifTag.PixelXDimension, - width == null ? Constants.Conventions.Media.DefaultSize : int.Parse(width, CultureInfo.InvariantCulture))); + Properties.Add(new ExifSInt( + ExifTag.PixelYDimension, + height == null ? Constants.Conventions.Media.DefaultSize : int.Parse(height, CultureInfo.InvariantCulture))); + Properties.Add(new ExifSInt( + ExifTag.PixelXDimension, + width == null ? Constants.Conventions.Media.DefaultSize : int.Parse(width, CultureInfo.InvariantCulture))); - Format = ImageFileFormat.SVG; - } - - public override void Save(Stream stream) - { - } + Format = ImageFileFormat.SVG; + } + public override void Save(Stream stream) + { } } diff --git a/src/Umbraco.Core/Media/Exif/TIFFFile.cs b/src/Umbraco.Core/Media/Exif/TIFFFile.cs index 8841e8337b..2ae27c46dc 100644 --- a/src/Umbraco.Core/Media/Exif/TIFFFile.cs +++ b/src/Umbraco.Core/Media/Exif/TIFFFile.cs @@ -1,166 +1,186 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Text; -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents the binary view of a TIFF file. +/// +internal class TIFFFile : ImageFile { + #region Constructor + /// - /// Represents the binary view of a TIFF file. + /// Initializes a new instance of the class from the + /// specified data stream. /// - internal class TIFFFile : ImageFile + /// A that contains image data. + /// The encoding to be used for text metadata when the source encoding is unknown. + protected internal TIFFFile(Stream stream, Encoding encoding) { - #region Properties - /// - /// Gets the TIFF header. - /// - public TIFFHeader TIFFHeader { get; private set; } - /// - /// Gets the image file directories. - /// - public List IFDs { get; private set; } - #endregion + Format = ImageFileFormat.TIFF; + IFDs = new List(); + Encoding = encoding; - #region Constructor - /// - /// Initializes a new instance of the class from the - /// specified data stream. - /// - /// A that contains image data. - /// The encoding to be used for text metadata when the source encoding is unknown. - protected internal TIFFFile(Stream stream, System.Text.Encoding encoding) + // Read the entire stream + var data = Utility.GetStreamBytes(stream); + + // Read the TIFF header + TIFFHeader = TIFFHeader.FromBytes(data, 0); + var nextIFDOffset = TIFFHeader.IFDOffset; + if (nextIFDOffset == 0) { - Format = ImageFileFormat.TIFF; - IFDs = new List(); - Encoding = encoding; - - // Read the entire stream - byte[] data = Utility.GetStreamBytes(stream); - - // Read the TIFF header - TIFFHeader = TIFFHeader.FromBytes(data, 0); - uint nextIFDOffset = TIFFHeader.IFDOffset; - if (nextIFDOffset == 0) - throw new NotValidTIFFileException("The first IFD offset is zero."); - - // Read IFDs in order - while (nextIFDOffset != 0) - { - ImageFileDirectory ifd = ImageFileDirectory.FromBytes(data, nextIFDOffset, TIFFHeader.ByteOrder); - nextIFDOffset = ifd.NextIFDOffset; - IFDs.Add(ifd); - } - - // Process IFDs - // TODO: Add support for multiple frames - foreach (ImageFileDirectoryEntry field in IFDs[0].Fields) - { - Properties.Add(ExifPropertyFactory.Get(field.Tag, field.Type, field.Count, field.Data, BitConverterEx.SystemByteOrder, IFD.Zeroth, Encoding)); - } - } - #endregion - - #region Instance Methods - /// - /// Saves the to the given stream. - /// - /// The data stream used to save the image. - public override void Save(Stream stream) - { - BitConverterEx conv = BitConverterEx.SystemEndian; - - // Write TIFF header - uint ifdoffset = 8; - // Byte order - stream.Write((BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian ? new byte[] { 0x49, 0x49 } : new byte[] { 0x4D, 0x4D }), 0, 2); - // TIFF ID - stream.Write(conv.GetBytes((ushort)42), 0, 2); - // Offset to 0th IFD, will be corrected below - stream.Write(conv.GetBytes(ifdoffset), 0, 4); - - // Write IFD sections - for (int i = 0; i < IFDs.Count; i++) - { - ImageFileDirectory ifd = IFDs[i]; - - // Save the location of IFD offset - long ifdLocation = stream.Position - 4; - - // Write strips first - byte[] stripOffsets = new byte[4 * ifd.Strips.Count]; - byte[] stripLengths = new byte[4 * ifd.Strips.Count]; - uint stripOffset = ifdoffset; - for (int j = 0; j < ifd.Strips.Count; j++) - { - byte[] stripData = ifd.Strips[j].Data; - byte[] oBytes = BitConverter.GetBytes(stripOffset); - byte[] lBytes = BitConverter.GetBytes((uint)stripData.Length); - Array.Copy(oBytes, 0, stripOffsets, 4 * j, 4); - Array.Copy(lBytes, 0, stripLengths, 4 * j, 4); - stream.Write(stripData, 0, stripData.Length); - stripOffset += (uint)stripData.Length; - } - - // Remove old strip tags - for (int j = ifd.Fields.Count - 1; j > 0; j--) - { - ushort tag = ifd.Fields[j].Tag; - if (tag == 273 || tag == 279) - ifd.Fields.RemoveAt(j); - } - // Write new strip tags - ifd.Fields.Add(new ImageFileDirectoryEntry(273, 4, (uint)ifd.Strips.Count, stripOffsets)); - ifd.Fields.Add(new ImageFileDirectoryEntry(279, 4, (uint)ifd.Strips.Count, stripLengths)); - - // Write fields after strips - ifdoffset = stripOffset; - - // Correct IFD offset - long currentLocation = stream.Position; - stream.Seek(ifdLocation, SeekOrigin.Begin); - stream.Write(conv.GetBytes(ifdoffset), 0, 4); - stream.Seek(currentLocation, SeekOrigin.Begin); - - // Offset to field data - uint dataOffset = ifdoffset + 2 + (uint)ifd.Fields.Count * 12 + 4; - - // Field count - stream.Write(conv.GetBytes((ushort)ifd.Fields.Count), 0, 2); - - // Fields - foreach (ImageFileDirectoryEntry field in ifd.Fields) - { - // Tag - stream.Write(conv.GetBytes(field.Tag), 0, 2); - // Type - stream.Write(conv.GetBytes(field.Type), 0, 2); - // Count - stream.Write(conv.GetBytes(field.Count), 0, 4); - - // Field data - byte[] data = field.Data; - if (data.Length <= 4) - { - stream.Write(data, 0, data.Length); - for (int j = data.Length; j < 4; j++) - stream.WriteByte(0); - } - else - { - stream.Write(conv.GetBytes(dataOffset), 0, 4); - long currentOffset = stream.Position; - stream.Seek(dataOffset, SeekOrigin.Begin); - stream.Write(data, 0, data.Length); - dataOffset += (uint)data.Length; - stream.Seek(currentOffset, SeekOrigin.Begin); - } - } - - // Offset to next IFD - ifdoffset = dataOffset; - stream.Write(conv.GetBytes(i == IFDs.Count - 1 ? 0 : ifdoffset), 0, 4); - } + throw new NotValidTIFFileException("The first IFD offset is zero."); } - #endregion + // Read IFDs in order + while (nextIFDOffset != 0) + { + var ifd = ImageFileDirectory.FromBytes(data, nextIFDOffset, TIFFHeader.ByteOrder); + nextIFDOffset = ifd.NextIFDOffset; + IFDs.Add(ifd); + } + + // Process IFDs + // TODO: Add support for multiple frames + foreach (ImageFileDirectoryEntry field in IFDs[0].Fields) + { + Properties.Add(ExifPropertyFactory.Get(field.Tag, field.Type, field.Count, field.Data, BitConverterEx.SystemByteOrder, IFD.Zeroth, Encoding)); + } } + + #endregion + + #region Properties + + /// + /// Gets the TIFF header. + /// + public TIFFHeader TIFFHeader { get; } + + #endregion + + #region Instance Methods + + /// + /// Saves the to the given stream. + /// + /// The data stream used to save the image. + public override void Save(Stream stream) + { + BitConverterEx conv = BitConverterEx.SystemEndian; + + // Write TIFF header + uint ifdoffset = 8; + + // Byte order + stream.Write( + BitConverterEx.SystemByteOrder == BitConverterEx.ByteOrder.LittleEndian + ? new byte[] { 0x49, 0x49 } + : new byte[] { 0x4D, 0x4D }, + 0, + 2); + + // TIFF ID + stream.Write(conv.GetBytes((ushort)42), 0, 2); + + // Offset to 0th IFD, will be corrected below + stream.Write(conv.GetBytes(ifdoffset), 0, 4); + + // Write IFD sections + for (var i = 0; i < IFDs.Count; i++) + { + ImageFileDirectory ifd = IFDs[i]; + + // Save the location of IFD offset + var ifdLocation = stream.Position - 4; + + // Write strips first + var stripOffsets = new byte[4 * ifd.Strips.Count]; + var stripLengths = new byte[4 * ifd.Strips.Count]; + var stripOffset = ifdoffset; + for (var j = 0; j < ifd.Strips.Count; j++) + { + var stripData = ifd.Strips[j].Data; + var oBytes = BitConverter.GetBytes(stripOffset); + var lBytes = BitConverter.GetBytes((uint)stripData.Length); + Array.Copy(oBytes, 0, stripOffsets, 4 * j, 4); + Array.Copy(lBytes, 0, stripLengths, 4 * j, 4); + stream.Write(stripData, 0, stripData.Length); + stripOffset += (uint)stripData.Length; + } + + // Remove old strip tags + for (var j = ifd.Fields.Count - 1; j > 0; j--) + { + var tag = ifd.Fields[j].Tag; + if (tag == 273 || tag == 279) + { + ifd.Fields.RemoveAt(j); + } + } + + // Write new strip tags + ifd.Fields.Add(new ImageFileDirectoryEntry(273, 4, (uint)ifd.Strips.Count, stripOffsets)); + ifd.Fields.Add(new ImageFileDirectoryEntry(279, 4, (uint)ifd.Strips.Count, stripLengths)); + + // Write fields after strips + ifdoffset = stripOffset; + + // Correct IFD offset + var currentLocation = stream.Position; + stream.Seek(ifdLocation, SeekOrigin.Begin); + stream.Write(conv.GetBytes(ifdoffset), 0, 4); + stream.Seek(currentLocation, SeekOrigin.Begin); + + // Offset to field data + var dataOffset = ifdoffset + 2 + ((uint)ifd.Fields.Count * 12) + 4; + + // Field count + stream.Write(conv.GetBytes((ushort)ifd.Fields.Count), 0, 2); + + // Fields + foreach (ImageFileDirectoryEntry field in ifd.Fields) + { + // Tag + stream.Write(conv.GetBytes(field.Tag), 0, 2); + + // Type + stream.Write(conv.GetBytes(field.Type), 0, 2); + + // Count + stream.Write(conv.GetBytes(field.Count), 0, 4); + + // Field data + var data = field.Data; + if (data.Length <= 4) + { + stream.Write(data, 0, data.Length); + for (var j = data.Length; j < 4; j++) + { + stream.WriteByte(0); + } + } + else + { + stream.Write(conv.GetBytes(dataOffset), 0, 4); + var currentOffset = stream.Position; + stream.Seek(dataOffset, SeekOrigin.Begin); + stream.Write(data, 0, data.Length); + dataOffset += (uint)data.Length; + stream.Seek(currentOffset, SeekOrigin.Begin); + } + } + + // Offset to next IFD + ifdoffset = dataOffset; + stream.Write(conv.GetBytes(i == IFDs.Count - 1 ? 0 : ifdoffset), 0, 4); + } + } + + /// + /// Gets the image file directories. + /// + public List IFDs { get; } + + #endregion } diff --git a/src/Umbraco.Core/Media/Exif/TIFFHeader.cs b/src/Umbraco.Core/Media/Exif/TIFFHeader.cs index ac7c503d0c..54a79d90b4 100644 --- a/src/Umbraco.Core/Media/Exif/TIFFHeader.cs +++ b/src/Umbraco.Core/Media/Exif/TIFFHeader.cs @@ -1,78 +1,98 @@ -namespace Umbraco.Cms.Core.Media.Exif +namespace Umbraco.Cms.Core.Media.Exif; + +/// +/// Represents a TIFF Header. +/// +internal struct TIFFHeader { /// - /// Represents a TIFF Header. + /// The byte order of the image file. /// - internal struct TIFFHeader + public BitConverterEx.ByteOrder ByteOrder; + + /// + /// TIFF ID. This value should always be 42. + /// + public byte ID; + + /// + /// The offset to the first IFD section from the + /// start of the TIFF header. + /// + public uint IFDOffset; + + /// + /// The byte order of the TIFF header itself. + /// + public BitConverterEx.ByteOrder TIFFHeaderByteOrder; + + /// + /// Initializes a new instance of the struct. + /// + /// The byte order. + /// The TIFF ID. This value should always be 42. + /// + /// The offset to the first IFD section from the + /// start of the TIFF header. + /// + /// The byte order of the TIFF header itself. + public TIFFHeader(BitConverterEx.ByteOrder byteOrder, byte id, uint ifdOffset, BitConverterEx.ByteOrder headerByteOrder) { - /// - /// The byte order of the image file. - /// - public BitConverterEx.ByteOrder ByteOrder; - /// - /// TIFF ID. This value should always be 42. - /// - public byte ID; - /// - /// The offset to the first IFD section from the - /// start of the TIFF header. - /// - public uint IFDOffset; - /// - /// The byte order of the TIFF header itself. - /// - public BitConverterEx.ByteOrder TIFFHeaderByteOrder; - - /// - /// Initializes a new instance of the struct. - /// - /// The byte order. - /// The TIFF ID. This value should always be 42. - /// The offset to the first IFD section from the - /// start of the TIFF header. - /// The byte order of the TIFF header itself. - public TIFFHeader(BitConverterEx.ByteOrder byteOrder, byte id, uint ifdOffset, BitConverterEx.ByteOrder headerByteOrder) + if (id != 42) { - if (id != 42) - throw new NotValidTIFFHeader(); - - ByteOrder = byteOrder; - ID = id; - IFDOffset = ifdOffset; - TIFFHeaderByteOrder = headerByteOrder; + throw new NotValidTIFFHeader(); } - /// - /// Returns a initialized from the given byte data. - /// - /// The data. - /// The offset into . - /// A initialized from the given byte data. - public static TIFFHeader FromBytes(byte[] data, int offset) + ByteOrder = byteOrder; + ID = id; + IFDOffset = ifdOffset; + TIFFHeaderByteOrder = headerByteOrder; + } + + /// + /// Returns a initialized from the given byte data. + /// + /// The data. + /// The offset into . + /// A initialized from the given byte data. + public static TIFFHeader FromBytes(byte[] data, int offset) + { + var header = default(TIFFHeader); + + // TIFF header + if (data[offset] == 0x49 && data[offset + 1] == 0x49) { - TIFFHeader header = new TIFFHeader(); - - // TIFF header - if (data[offset] == 0x49 && data[offset + 1] == 0x49) - header.ByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else if (data[offset] == 0x4D && data[offset + 1] == 0x4D) - header.ByteOrder = BitConverterEx.ByteOrder.BigEndian; - else - throw new NotValidTIFFHeader(); - - // TIFF header may have a different byte order - if (BitConverterEx.LittleEndian.ToUInt16(data, offset + 2) == 42) - header.TIFFHeaderByteOrder = BitConverterEx.ByteOrder.LittleEndian; - else if (BitConverterEx.BigEndian.ToUInt16(data, offset + 2) == 42) - header.TIFFHeaderByteOrder = BitConverterEx.ByteOrder.BigEndian; - else - throw new NotValidTIFFHeader(); - header.ID = 42; - - // IFD offset - header.IFDOffset = BitConverterEx.ToUInt32(data, offset + 4, header.TIFFHeaderByteOrder, BitConverterEx.SystemByteOrder); - - return header; + header.ByteOrder = BitConverterEx.ByteOrder.LittleEndian; } + else if (data[offset] == 0x4D && data[offset + 1] == 0x4D) + { + header.ByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidTIFFHeader(); + } + + // TIFF header may have a different byte order + if (BitConverterEx.LittleEndian.ToUInt16(data, offset + 2) == 42) + { + header.TIFFHeaderByteOrder = BitConverterEx.ByteOrder.LittleEndian; + } + else if (BitConverterEx.BigEndian.ToUInt16(data, offset + 2) == 42) + { + header.TIFFHeaderByteOrder = BitConverterEx.ByteOrder.BigEndian; + } + else + { + throw new NotValidTIFFHeader(); + } + + header.ID = 42; + + // IFD offset + header.IFDOffset = + BitConverterEx.ToUInt32(data, offset + 4, header.TIFFHeaderByteOrder, BitConverterEx.SystemByteOrder); + + return header; } } diff --git a/src/Umbraco.Core/Media/Exif/TIFFStrip.cs b/src/Umbraco.Core/Media/Exif/TIFFStrip.cs index 9930961e20..8bf91abde6 100644 --- a/src/Umbraco.Core/Media/Exif/TIFFStrip.cs +++ b/src/Umbraco.Core/Media/Exif/TIFFStrip.cs @@ -1,27 +1,24 @@ -using System; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Represents a strip of compressed image data in a TIFF file. +/// +internal class TIFFStrip { /// - /// Represents a strip of compressed image data in a TIFF file. + /// Initializes a new instance of the class. /// - internal class TIFFStrip + /// The byte array to copy strip from. + /// The offset to the beginning of strip. + /// The length of strip. + public TIFFStrip(byte[] data, uint offset, uint length) { - /// - /// Compressed image data contained in this strip. - /// - public byte[] Data { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// The byte array to copy strip from. - /// The offset to the beginning of strip. - /// The length of strip. - public TIFFStrip(byte[] data, uint offset, uint length) - { - Data = new byte[length]; - Array.Copy(data, offset, Data, 0, length); - } + Data = new byte[length]; + Array.Copy(data, offset, Data, 0, length); } + + /// + /// Compressed image data contained in this strip. + /// + public byte[] Data { get; } } diff --git a/src/Umbraco.Core/Media/Exif/Utility.cs b/src/Umbraco.Core/Media/Exif/Utility.cs index 033b97ecc7..1ce1b1cdc7 100644 --- a/src/Umbraco.Core/Media/Exif/Utility.cs +++ b/src/Umbraco.Core/Media/Exif/Utility.cs @@ -1,30 +1,29 @@ -using System.IO; +namespace Umbraco.Cms.Core.Media.Exif; -namespace Umbraco.Cms.Core.Media.Exif +/// +/// Contains utility functions. +/// +internal class Utility { /// - /// Contains utility functions. + /// Reads the entire stream and returns its contents as a byte array. /// - internal class Utility + /// The to read. + /// Contents of the as a byte array. + public static byte[] GetStreamBytes(Stream stream) { - /// - /// Reads the entire stream and returns its contents as a byte array. - /// - /// The to read. - /// Contents of the as a byte array. - public static byte[] GetStreamBytes(Stream stream) + using (var mem = new MemoryStream()) { - using (MemoryStream mem = new MemoryStream()) + stream.Seek(0, SeekOrigin.Begin); + + var b = new byte[32768]; + int r; + while ((r = stream.Read(b, 0, b.Length)) > 0) { - stream.Seek(0, SeekOrigin.Begin); - - byte[] b = new byte[32768]; - int r; - while ((r = stream.Read(b, 0, b.Length)) > 0) - mem.Write(b, 0, r); - - return mem.ToArray(); + mem.Write(b, 0, r); } + + return mem.ToArray(); } } } diff --git a/src/Umbraco.Core/Media/IEmbedProvider.cs b/src/Umbraco.Core/Media/IEmbedProvider.cs index e7937904bd..6760243ce6 100644 --- a/src/Umbraco.Core/Media/IEmbedProvider.cs +++ b/src/Umbraco.Core/Media/IEmbedProvider.cs @@ -1,25 +1,22 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Media; -namespace Umbraco.Cms.Core.Media +public interface IEmbedProvider { - public interface IEmbedProvider - { - /// - /// The OEmbed API Endpoint - /// - string ApiEndpoint { get; } + /// + /// The OEmbed API Endpoint + /// + string ApiEndpoint { get; } - /// - /// A string array of Regex patterns to match against the pasted OEmbed URL - /// - string[] UrlSchemeRegex { get; } + /// + /// A string array of Regex patterns to match against the pasted OEmbed URL + /// + string[] UrlSchemeRegex { get; } - /// - /// A collection of querystring request parameters to append to the API URL - /// - /// ?key=value&key2=value2 - Dictionary RequestParams { get; } + /// + /// A collection of querystring request parameters to append to the API URL + /// + /// ?key=value&key2=value2 + Dictionary RequestParams { get; } - string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); - } + string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); } diff --git a/src/Umbraco.Core/Media/IImageDimensionExtractor.cs b/src/Umbraco.Core/Media/IImageDimensionExtractor.cs index 2eaf632e54..67f11415d3 100644 --- a/src/Umbraco.Core/Media/IImageDimensionExtractor.cs +++ b/src/Umbraco.Core/Media/IImageDimensionExtractor.cs @@ -1,10 +1,8 @@ using System.Drawing; -using System.IO; -namespace Umbraco.Cms.Core.Media +namespace Umbraco.Cms.Core.Media; + +public interface IImageDimensionExtractor { - public interface IImageDimensionExtractor - { - public Size? GetDimensions(Stream? stream); - } + public Size? GetDimensions(Stream? stream); } diff --git a/src/Umbraco.Core/Media/IImageUrlGenerator.cs b/src/Umbraco.Core/Media/IImageUrlGenerator.cs index 25bb1ac899..d8fdf72005 100644 --- a/src/Umbraco.Core/Media/IImageUrlGenerator.cs +++ b/src/Umbraco.Core/Media/IImageUrlGenerator.cs @@ -1,28 +1,26 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Media +namespace Umbraco.Cms.Core.Media; + +/// +/// Exposes a method that generates an image URL based on the specified options. +/// +public interface IImageUrlGenerator { /// - /// Exposes a method that generates an image URL based on the specified options. + /// Gets the supported image file types/extensions. /// - public interface IImageUrlGenerator - { - /// - /// Gets the supported image file types/extensions. - /// - /// - /// The supported image file types/extensions. - /// - IEnumerable SupportedImageFileTypes { get; } + /// + /// The supported image file types/extensions. + /// + IEnumerable SupportedImageFileTypes { get; } - /// - /// Gets the image URL based on the specified . - /// - /// The image URL generation options. - /// - /// The generated image URL. - /// - string? GetImageUrl(ImageUrlGenerationOptions options); - } + /// + /// Gets the image URL based on the specified . + /// + /// The image URL generation options. + /// + /// The generated image URL. + /// + string? GetImageUrl(ImageUrlGenerationOptions options); } diff --git a/src/Umbraco.Core/Media/ImageUrlGeneratorExtensions.cs b/src/Umbraco.Core/Media/ImageUrlGeneratorExtensions.cs index 9cdc6869f4..ab904f2094 100644 --- a/src/Umbraco.Core/Media/ImageUrlGeneratorExtensions.cs +++ b/src/Umbraco.Core/Media/ImageUrlGeneratorExtensions.cs @@ -1,29 +1,31 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Media; -namespace Umbraco.Extensions -{ - public static class ImageUrlGeneratorExtensions - { - /// - /// Gets a value indicating whether the file extension corresponds to a supported image. - /// - /// The image URL generator implementation that provides detail on which image extensions are supported. - /// The file extension. - /// - /// A value indicating whether the file extension corresponds to an image. - /// - /// imageUrlGenerator - public static bool IsSupportedImageFormat(this IImageUrlGenerator imageUrlGenerator, string extension) - { - if (imageUrlGenerator == null) - { - throw new ArgumentNullException(nameof(imageUrlGenerator)); - } +namespace Umbraco.Extensions; - return string.IsNullOrWhiteSpace(extension) == false && - imageUrlGenerator.SupportedImageFileTypes.InvariantContains(extension.TrimStart(Constants.CharArrays.Period)); +public static class ImageUrlGeneratorExtensions +{ + /// + /// Gets a value indicating whether the file extension corresponds to a supported image. + /// + /// + /// The image URL generator implementation that provides detail on which image extensions + /// are supported. + /// + /// The file extension. + /// + /// A value indicating whether the file extension corresponds to an image. + /// + /// imageUrlGenerator + public static bool IsSupportedImageFormat(this IImageUrlGenerator imageUrlGenerator, string extension) + { + if (imageUrlGenerator == null) + { + throw new ArgumentNullException(nameof(imageUrlGenerator)); } + + return string.IsNullOrWhiteSpace(extension) == false && + imageUrlGenerator.SupportedImageFileTypes.InvariantContains( + extension.TrimStart(Constants.CharArrays.Period)); } } diff --git a/src/Umbraco.Core/Media/OEmbedResult.cs b/src/Umbraco.Core/Media/OEmbedResult.cs index b370efc1ae..3e4834521d 100644 --- a/src/Umbraco.Core/Media/OEmbedResult.cs +++ b/src/Umbraco.Core/Media/OEmbedResult.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Core.Media +namespace Umbraco.Cms.Core.Media; + +public class OEmbedResult { - public class OEmbedResult - { - public OEmbedStatus OEmbedStatus { get; set; } - public bool SupportsDimensions { get; set; } - public string? Markup { get; set; } - } + public OEmbedStatus OEmbedStatus { get; set; } + + public bool SupportsDimensions { get; set; } + + public string? Markup { get; set; } } diff --git a/src/Umbraco.Core/Media/OEmbedStatus.cs b/src/Umbraco.Core/Media/OEmbedStatus.cs index 268fc1cd0d..1903643d5e 100644 --- a/src/Umbraco.Core/Media/OEmbedStatus.cs +++ b/src/Umbraco.Core/Media/OEmbedStatus.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Media +namespace Umbraco.Cms.Core.Media; + +public enum OEmbedStatus { - public enum OEmbedStatus - { - NotSupported, - Error, - Success - } + NotSupported, + Error, + Success, } diff --git a/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs b/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs index 0481323a4a..e89d8e159d 100644 --- a/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs +++ b/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs @@ -1,13 +1,10 @@ -using System.IO; +namespace Umbraco.Cms.Core.Media.TypeDetector; -namespace Umbraco.Cms.Core.Media.TypeDetector +public class JpegDetector : RasterizedTypeDetector { - public class JpegDetector : RasterizedTypeDetector + public static bool IsOfType(Stream fileStream) { - public static bool IsOfType(Stream fileStream) - { - var header = GetFileHeader(fileStream); - return header != null && header[0] == 0xff && header[1] == 0xD8; - } + var header = GetFileHeader(fileStream); + return header != null && header[0] == 0xff && header[1] == 0xD8; } } diff --git a/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs b/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs index 167fbe5e0e..6f4e7a8a86 100644 --- a/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs +++ b/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs @@ -1,20 +1,19 @@ -using System.IO; +namespace Umbraco.Cms.Core.Media.TypeDetector; -namespace Umbraco.Cms.Core.Media.TypeDetector +public abstract class RasterizedTypeDetector { - public abstract class RasterizedTypeDetector + public static byte[]? GetFileHeader(Stream fileStream) { - public static byte[]? GetFileHeader(Stream fileStream) + fileStream.Seek(0, SeekOrigin.Begin); + var header = new byte[8]; + fileStream.Seek(0, SeekOrigin.Begin); + + // Invalid header + if (fileStream.Read(header, 0, header.Length) != header.Length) { - fileStream.Seek(0, SeekOrigin.Begin); - var header = new byte[8]; - fileStream.Seek(0, SeekOrigin.Begin); - - // Invalid header - if (fileStream.Read(header, 0, header.Length) != header.Length) - return null; - - return header; + return null; } + + return header; } } diff --git a/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs b/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs index 81f13b199d..c790806b9b 100644 --- a/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs +++ b/src/Umbraco.Core/Media/TypeDetector/SvgDetector.cs @@ -1,24 +1,22 @@ -using System.IO; using System.Xml.Linq; -namespace Umbraco.Cms.Core.Media.TypeDetector +namespace Umbraco.Cms.Core.Media.TypeDetector; + +public class SvgDetector { - public class SvgDetector + public static bool IsOfType(Stream fileStream) { - public static bool IsOfType(Stream fileStream) + var document = new XDocument(); + + try { - var document = new XDocument(); - - try - { - document = XDocument.Load(fileStream); - } - catch (System.Exception) - { - return false; - } - - return document.Root?.Name.LocalName == "svg"; + document = XDocument.Load(fileStream); } + catch (Exception) + { + return false; + } + + return document.Root?.Name.LocalName == "svg"; } } diff --git a/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs b/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs index 1eda8efe7a..5581c81a62 100644 --- a/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs +++ b/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs @@ -1,24 +1,24 @@ -using System.IO; using System.Text; -namespace Umbraco.Cms.Core.Media.TypeDetector +namespace Umbraco.Cms.Core.Media.TypeDetector; + +public class TIFFDetector { - public class TIFFDetector + public static bool IsOfType(Stream fileStream) { - public static bool IsOfType(Stream fileStream) + var tiffHeader = GetFileHeader(fileStream); + return (tiffHeader != null && tiffHeader == "MM\x00\x2a") || tiffHeader == "II\x2a\x00"; + } + + public static string? GetFileHeader(Stream fileStream) + { + var header = RasterizedTypeDetector.GetFileHeader(fileStream); + if (header == null) { - var tiffHeader = GetFileHeader(fileStream); - return tiffHeader != null && tiffHeader == "MM\x00\x2a" || tiffHeader == "II\x2a\x00"; + return null; } - public static string? GetFileHeader(Stream fileStream) - { - var header = RasterizedTypeDetector.GetFileHeader(fileStream); - if (header == null) - return null; - - var tiffHeader = Encoding.ASCII.GetString(header, 0, 4); - return tiffHeader; - } + var tiffHeader = Encoding.ASCII.GetString(header, 0, 4); + return tiffHeader; } } diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs index 459866a8d9..6a5ffd23d7 100644 --- a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -1,162 +1,218 @@ -using System; using System.Drawing; -using System.IO; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Media -{ - /// - /// Provides methods to manage auto-fill properties for upload fields. - /// - public class UploadAutoFillProperties - { - private readonly MediaFileManager _mediaFileManager; - private readonly ILogger _logger; - private readonly IImageUrlGenerator _imageUrlGenerator; - private readonly IImageDimensionExtractor _imageDimensionExtractor; +namespace Umbraco.Cms.Core.Media; - public UploadAutoFillProperties( - MediaFileManager mediaFileManager, - ILogger logger, - IImageUrlGenerator imageUrlGenerator, - IImageDimensionExtractor imageDimensionExtractor) +/// +/// Provides methods to manage auto-fill properties for upload fields. +/// +public class UploadAutoFillProperties +{ + private readonly IImageDimensionExtractor _imageDimensionExtractor; + private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly ILogger _logger; + private readonly MediaFileManager _mediaFileManager; + + public UploadAutoFillProperties( + MediaFileManager mediaFileManager, + ILogger logger, + IImageUrlGenerator imageUrlGenerator, + IImageDimensionExtractor imageDimensionExtractor) + { + _mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _imageUrlGenerator = imageUrlGenerator ?? throw new ArgumentNullException(nameof(imageUrlGenerator)); + _imageDimensionExtractor = + imageDimensionExtractor ?? throw new ArgumentNullException(nameof(imageDimensionExtractor)); + } + + /// + /// Resets the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + /// Variation language. + /// Variation segment. + public void Reset(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) + { + if (content == null) { - _mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _imageUrlGenerator = imageUrlGenerator ?? throw new ArgumentNullException(nameof(imageUrlGenerator)); - _imageDimensionExtractor = imageDimensionExtractor ?? throw new ArgumentNullException(nameof(imageDimensionExtractor)); + throw new ArgumentNullException(nameof(content)); } - /// - /// Resets the auto-fill properties of a content item, for a specified auto-fill configuration. - /// - /// The content item. - /// The auto-fill configuration. - /// Variation language. - /// Variation segment. - public void Reset(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) + if (autoFillConfig == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + throw new ArgumentNullException(nameof(autoFillConfig)); + } + ResetProperties(content, autoFillConfig, culture, segment); + } + + /// + /// Populates the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + /// The filesystem path to the uploaded file. + /// The parameter is the path relative to the filesystem. + /// Variation language. + /// Variation segment. + public void Populate(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, string? culture, string? segment) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (autoFillConfig == null) + { + throw new ArgumentNullException(nameof(autoFillConfig)); + } + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace()) + { ResetProperties(content, autoFillConfig, culture, segment); } - - /// - /// Populates the auto-fill properties of a content item, for a specified auto-fill configuration. - /// - /// The content item. - /// The auto-fill configuration. - /// The filesystem path to the uploaded file. - /// The parameter is the path relative to the filesystem. - /// Variation language. - /// Variation segment. - public void Populate(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, string? culture, string? segment) + else { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); - - // no file = reset, file = auto-fill - if (filepath.IsNullOrWhiteSpace()) + // it might not exist if the media item has been created programatically but doesn't have a file persisted yet. + if (_mediaFileManager.FileSystem.FileExists(filepath)) { - ResetProperties(content, autoFillConfig, culture, segment); - } - else - { - // it might not exist if the media item has been created programatically but doesn't have a file persisted yet. - if (_mediaFileManager.FileSystem.FileExists(filepath)) + // if anything goes wrong, just reset the properties + try { - // if anything goes wrong, just reset the properties - try + using (Stream filestream = _mediaFileManager.FileSystem.OpenFile(filepath)) { - using (Stream filestream = _mediaFileManager.FileSystem.OpenFile(filepath)) - { - SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not populate upload auto-fill properties for file '{File}'.", filepath); - ResetProperties(content, autoFillConfig, culture, segment); + SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); } } + catch (Exception ex) + { + _logger.LogError(ex, "Could not populate upload auto-fill properties for file '{File}'.", filepath); + ResetProperties(content, autoFillConfig, culture, segment); + } } } + } - /// - /// Populates the auto-fill properties of a content item. - /// - /// The content item. - /// - /// The filesystem-relative filepath, or null to clear properties. - /// The stream containing the file data. - /// Variation language. - /// Variation segment. - public void Populate(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, string culture, string segment) + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// + /// The filesystem-relative filepath, or null to clear properties. + /// The stream containing the file data. + /// Variation language. + /// Variation segment. + public void Populate(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, string culture, string segment) + { + if (content == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); - - // no file = reset, file = auto-fill - if (filepath.IsNullOrWhiteSpace() || filestream == null) - { - ResetProperties(content, autoFillConfig, culture, segment); - } - else - { - SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); - } + throw new ArgumentNullException(nameof(content)); } - private void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream? filestream, string? culture, string? segment) + if (autoFillConfig == null) { - var extension = (Path.GetExtension(filepath) ?? string.Empty).TrimStart(Constants.CharArrays.Period); - - var size = _imageUrlGenerator.IsSupportedImageFormat(extension) - ? _imageDimensionExtractor.GetDimensions(filestream) ?? (Size?)new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize) - : null; - - SetProperties(content, autoFillConfig, size, filestream?.Length, extension, culture, segment); + throw new ArgumentNullException(nameof(autoFillConfig)); } - private static void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, Size? size, long? length, string extension, string? culture, string? segment) + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace() || filestream == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + ResetProperties(content, autoFillConfig, culture, segment); + } + else + { + SetProperties(content, autoFillConfig, filepath, filestream, culture, segment); + } + } - if (!string.IsNullOrWhiteSpace(autoFillConfig.WidthFieldAlias) && content.Properties.Contains(autoFillConfig.WidthFieldAlias)) - content.Properties[autoFillConfig.WidthFieldAlias]!.SetValue(size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty, culture, segment); - - if (!string.IsNullOrWhiteSpace(autoFillConfig.HeightFieldAlias) && content.Properties.Contains(autoFillConfig.HeightFieldAlias)) - content.Properties[autoFillConfig.HeightFieldAlias]!.SetValue(size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty, culture, segment); - - if (!string.IsNullOrWhiteSpace(autoFillConfig.LengthFieldAlias) && content.Properties.Contains(autoFillConfig.LengthFieldAlias)) - content.Properties[autoFillConfig.LengthFieldAlias]!.SetValue(length, culture, segment); - - if (!string.IsNullOrWhiteSpace(autoFillConfig.ExtensionFieldAlias) && content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) - content.Properties[autoFillConfig.ExtensionFieldAlias]!.SetValue(extension, culture, segment); + private static void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, Size? size, long? length, string extension, string? culture, string? segment) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); } - private static void ResetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) + if (autoFillConfig == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + throw new ArgumentNullException(nameof(autoFillConfig)); + } - if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) - content.Properties[autoFillConfig.WidthFieldAlias]?.SetValue(string.Empty, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.WidthFieldAlias) && + content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + { + content.Properties[autoFillConfig.WidthFieldAlias]!.SetValue( + size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty, culture, segment); + } - if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) - content.Properties[autoFillConfig.HeightFieldAlias]?.SetValue(string.Empty, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.HeightFieldAlias) && + content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + { + content.Properties[autoFillConfig.HeightFieldAlias]!.SetValue( + size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty, culture, segment); + } - if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) - content.Properties[autoFillConfig.LengthFieldAlias]?.SetValue(string.Empty, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.LengthFieldAlias) && + content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + { + content.Properties[autoFillConfig.LengthFieldAlias]!.SetValue(length, culture, segment); + } - if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) - content.Properties[autoFillConfig.ExtensionFieldAlias]?.SetValue(string.Empty, culture, segment); + if (!string.IsNullOrWhiteSpace(autoFillConfig.ExtensionFieldAlias) && + content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + { + content.Properties[autoFillConfig.ExtensionFieldAlias]!.SetValue(extension, culture, segment); + } + } + + private void SetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string filepath, Stream? filestream, string? culture, string? segment) + { + var extension = (Path.GetExtension(filepath) ?? string.Empty).TrimStart(Constants.CharArrays.Period); + + Size? size = _imageUrlGenerator.IsSupportedImageFormat(extension) + ? _imageDimensionExtractor.GetDimensions(filestream) ?? + (Size?)new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize) + : null; + + SetProperties(content, autoFillConfig, size, filestream?.Length, extension, culture, segment); + } + + private static void ResetProperties(IContentBase content, ImagingAutoFillUploadField autoFillConfig, string? culture, string? segment) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (autoFillConfig == null) + { + throw new ArgumentNullException(nameof(autoFillConfig)); + } + + if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + { + content.Properties[autoFillConfig.WidthFieldAlias]?.SetValue(string.Empty, culture, segment); + } + + if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + { + content.Properties[autoFillConfig.HeightFieldAlias]?.SetValue(string.Empty, culture, segment); + } + + if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + { + content.Properties[autoFillConfig.LengthFieldAlias]?.SetValue(string.Empty, culture, segment); + } + + if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + { + content.Properties[autoFillConfig.ExtensionFieldAlias]?.SetValue(string.Empty, culture, segment); } } } diff --git a/src/Umbraco.Core/Models/AnchorsModel.cs b/src/Umbraco.Core/Models/AnchorsModel.cs index 466751c82d..90faa01da1 100644 --- a/src/Umbraco.Core/Models/AnchorsModel.cs +++ b/src/Umbraco.Core/Models/AnchorsModel.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class AnchorsModel { - public class AnchorsModel - { - public string? RteContent { get; set; } - } + public string? RteContent { get; set; } } diff --git a/src/Umbraco.Core/Models/AuditEntry.cs b/src/Umbraco.Core/Models/AuditEntry.cs index e0bb52375b..9d1b4dfcef 100644 --- a/src/Umbraco.Core/Models/AuditEntry.cs +++ b/src/Umbraco.Core/Models/AuditEntry.cs @@ -1,78 +1,76 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an audited event. +/// +[Serializable] +[DataContract(IsReference = true)] +public class AuditEntry : EntityBase, IAuditEntry { - /// - /// Represents an audited event. - /// - [Serializable] - [DataContract(IsReference = true)] - public class AuditEntry : EntityBase, IAuditEntry + private string? _affectedDetails; + private int _affectedUserId; + private string? _eventDetails; + private string? _eventType; + private string? _performingDetails; + private string? _performingIp; + private int _performingUserId; + + /// + public int PerformingUserId { - private int _performingUserId; - private string? _performingDetails; - private string? _performingIp; - private int _affectedUserId; - private string? _affectedDetails; - private string? _eventType; - private string? _eventDetails; + get => _performingUserId; + set => SetPropertyValueAndDetectChanges(value, ref _performingUserId, nameof(PerformingUserId)); + } - /// - public int PerformingUserId - { - get => _performingUserId; - set => SetPropertyValueAndDetectChanges(value, ref _performingUserId, nameof(PerformingUserId)); - } + /// + public string? PerformingDetails + { + get => _performingDetails; + set => SetPropertyValueAndDetectChanges(value, ref _performingDetails, nameof(PerformingDetails)); + } - /// - public string? PerformingDetails - { - get => _performingDetails; - set => SetPropertyValueAndDetectChanges(value, ref _performingDetails, nameof(PerformingDetails)); - } + /// + public string? PerformingIp + { + get => _performingIp; + set => SetPropertyValueAndDetectChanges(value, ref _performingIp, nameof(PerformingIp)); + } - /// - public string? PerformingIp - { - get => _performingIp; - set => SetPropertyValueAndDetectChanges(value, ref _performingIp, nameof(PerformingIp)); - } + /// + public DateTime EventDateUtc + { + get => CreateDate; + set => CreateDate = value; + } - /// - public DateTime EventDateUtc - { - get => CreateDate; - set => CreateDate = value; - } + /// + public int AffectedUserId + { + get => _affectedUserId; + set => SetPropertyValueAndDetectChanges(value, ref _affectedUserId, nameof(AffectedUserId)); + } - /// - public int AffectedUserId - { - get => _affectedUserId; - set => SetPropertyValueAndDetectChanges(value, ref _affectedUserId, nameof(AffectedUserId)); - } + /// + public string? AffectedDetails + { + get => _affectedDetails; + set => SetPropertyValueAndDetectChanges(value, ref _affectedDetails, nameof(AffectedDetails)); + } - /// - public string? AffectedDetails - { - get => _affectedDetails; - set => SetPropertyValueAndDetectChanges(value, ref _affectedDetails, nameof(AffectedDetails)); - } + /// + public string? EventType + { + get => _eventType; + set => SetPropertyValueAndDetectChanges(value, ref _eventType, nameof(EventType)); + } - /// - public string? EventType - { - get => _eventType; - set => SetPropertyValueAndDetectChanges(value, ref _eventType, nameof(EventType)); - } - - /// - public string? EventDetails - { - get => _eventDetails; - set => SetPropertyValueAndDetectChanges(value, ref _eventDetails, nameof(EventDetails)); - } + /// + public string? EventDetails + { + get => _eventDetails; + set => SetPropertyValueAndDetectChanges(value, ref _eventDetails, nameof(EventDetails)); } } diff --git a/src/Umbraco.Core/Models/AuditItem.cs b/src/Umbraco.Core/Models/AuditItem.cs index 83ecad0878..bbfca724aa 100644 --- a/src/Umbraco.Core/Models/AuditItem.cs +++ b/src/Umbraco.Core/Models/AuditItem.cs @@ -1,39 +1,38 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public sealed class AuditItem : EntityBase, IAuditItem { - public sealed class AuditItem : EntityBase, IAuditItem + /// + /// Initializes a new instance of the class. + /// + public AuditItem(int objectId, AuditType type, int userId, string? entityType, string? comment = null, string? parameters = null) { - /// - /// Initializes a new instance of the class. - /// - public AuditItem(int objectId, AuditType type, int userId, string? entityType, string? comment = null, string? parameters = null) - { - DisableChangeTracking(); + DisableChangeTracking(); - Id = objectId; - Comment = comment; - AuditType = type; - UserId = userId; - EntityType = entityType; - Parameters = parameters; + Id = objectId; + Comment = comment; + AuditType = type; + UserId = userId; + EntityType = entityType; + Parameters = parameters; - EnableChangeTracking(); - } - - /// - public AuditType AuditType { get; } - - /// - public string? EntityType { get; } - - /// - public int UserId { get; } - - /// - public string? Comment { get; } - - /// - public string? Parameters { get; } + EnableChangeTracking(); } + + /// + public AuditType AuditType { get; } + + /// + public string? EntityType { get; } + + /// + public int UserId { get; } + + /// + public string? Comment { get; } + + /// + public string? Parameters { get; } } diff --git a/src/Umbraco.Core/Models/AuditType.cs b/src/Umbraco.Core/Models/AuditType.cs index b6a36be5ff..6a3e528273 100644 --- a/src/Umbraco.Core/Models/AuditType.cs +++ b/src/Umbraco.Core/Models/AuditType.cs @@ -1,128 +1,127 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines audit types. +/// +public enum AuditType { /// - /// Defines audit types. + /// New node(s) being added. /// - public enum AuditType - { - /// - /// New node(s) being added. - /// - New, + New, - /// - /// Node(s) being saved. - /// - Save, + /// + /// Node(s) being saved. + /// + Save, - /// - /// Variant(s) being saved. - /// - SaveVariant, + /// + /// Variant(s) being saved. + /// + SaveVariant, - /// - /// Node(s) being opened. - /// - Open, + /// + /// Node(s) being opened. + /// + Open, - /// - /// Node(s) being deleted. - /// - Delete, + /// + /// Node(s) being deleted. + /// + Delete, - /// - /// Node(s) being published. - /// - Publish, + /// + /// Node(s) being published. + /// + Publish, - /// - /// Variant(s) being published. - /// - PublishVariant, + /// + /// Variant(s) being published. + /// + PublishVariant, - /// - /// Node(s) being sent to publishing. - /// - SendToPublish, + /// + /// Node(s) being sent to publishing. + /// + SendToPublish, - /// - /// Variant(s) being sent to publishing. - /// - SendToPublishVariant, + /// + /// Variant(s) being sent to publishing. + /// + SendToPublishVariant, - /// - /// Node(s) being unpublished. - /// - Unpublish, + /// + /// Node(s) being unpublished. + /// + Unpublish, - /// - /// Variant(s) being unpublished. - /// - UnpublishVariant, + /// + /// Variant(s) being unpublished. + /// + UnpublishVariant, - /// - /// Node(s) being moved. - /// - Move, + /// + /// Node(s) being moved. + /// + Move, - /// - /// Node(s) being copied. - /// - Copy, + /// + /// Node(s) being copied. + /// + Copy, - /// - /// Node(s) being assigned domains. - /// - AssignDomain, + /// + /// Node(s) being assigned domains. + /// + AssignDomain, - /// - /// Node(s) public access changing. - /// - PublicAccess, + /// + /// Node(s) public access changing. + /// + PublicAccess, - /// - /// Node(s) being sorted. - /// - Sort, + /// + /// Node(s) being sorted. + /// + Sort, - /// - /// Notification(s) being sent to user. - /// - Notify, + /// + /// Notification(s) being sent to user. + /// + Notify, - /// - /// General system audit message. - /// - System, + /// + /// General system audit message. + /// + System, - /// - /// Node's content being rolled back to a previous version. - /// - RollBack, + /// + /// Node's content being rolled back to a previous version. + /// + RollBack, - /// - /// Package being installed. - /// - PackagerInstall, + /// + /// Package being installed. + /// + PackagerInstall, - /// - /// Package being uninstalled. - /// - PackagerUninstall, + /// + /// Package being uninstalled. + /// + PackagerUninstall, - /// - /// Custom audit message. - /// - Custom, + /// + /// Custom audit message. + /// + Custom, - /// - /// Content version preventCleanup set to true - /// - ContentVersionPreventCleanup, + /// + /// Content version preventCleanup set to true + /// + ContentVersionPreventCleanup, - /// - /// Content version preventCleanup set to false - /// - ContentVersionEnableCleanup - } + /// + /// Content version preventCleanup set to false + /// + ContentVersionEnableCleanup, } diff --git a/src/Umbraco.Core/Models/BackOfficeTour.cs b/src/Umbraco.Core/Models/BackOfficeTour.cs index d6a5d8971e..a7a9d3a5c3 100644 --- a/src/Umbraco.Core/Models/BackOfficeTour.cs +++ b/src/Umbraco.Core/Models/BackOfficeTour.cs @@ -1,47 +1,42 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing a tour. +/// +[DataContract(Name = "tour", Namespace = "")] +public class BackOfficeTour { - /// - /// A model representing a tour. - /// - [DataContract(Name = "tour", Namespace = "")] - public class BackOfficeTour - { - public BackOfficeTour() - { - RequiredSections = new List(); - } + public BackOfficeTour() => RequiredSections = new List(); - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; + [DataMember(Name = "alias")] + public string Alias { get; set; } = null!; - [DataMember(Name = "group")] - public string? Group { get; set; } + [DataMember(Name = "group")] + public string? Group { get; set; } - [DataMember(Name = "groupOrder")] - public int GroupOrder { get; set; } + [DataMember(Name = "groupOrder")] + public int GroupOrder { get; set; } - [DataMember(Name = "hidden")] - public bool Hidden { get; set; } + [DataMember(Name = "hidden")] + public bool Hidden { get; set; } - [DataMember(Name = "allowDisable")] - public bool AllowDisable { get; set; } + [DataMember(Name = "allowDisable")] + public bool AllowDisable { get; set; } - [DataMember(Name = "requiredSections")] - public List RequiredSections { get; set; } + [DataMember(Name = "requiredSections")] + public List RequiredSections { get; set; } - [DataMember(Name = "steps")] - public BackOfficeTourStep[]? Steps { get; set; } + [DataMember(Name = "steps")] + public BackOfficeTourStep[]? Steps { get; set; } - [DataMember(Name = "culture")] - public string? Culture { get; set; } + [DataMember(Name = "culture")] + public string? Culture { get; set; } - [DataMember(Name = "contentType")] - public string? ContentType { get; set; } - } + [DataMember(Name = "contentType")] + public string? ContentType { get; set; } } diff --git a/src/Umbraco.Core/Models/BackOfficeTourFile.cs b/src/Umbraco.Core/Models/BackOfficeTourFile.cs index 21b769f94e..bc0a5cea3b 100644 --- a/src/Umbraco.Core/Models/BackOfficeTourFile.cs +++ b/src/Umbraco.Core/Models/BackOfficeTourFile.cs @@ -1,35 +1,30 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing the file used to load a tour. +/// +[DataContract(Name = "tourFile", Namespace = "")] +public class BackOfficeTourFile { + public BackOfficeTourFile() => Tours = new List(); + /// - /// A model representing the file used to load a tour. + /// The file name for the tour /// - [DataContract(Name = "tourFile", Namespace = "")] - public class BackOfficeTourFile - { - public BackOfficeTourFile() - { - Tours = new List(); - } + [DataMember(Name = "fileName")] + public string? FileName { get; set; } - /// - /// The file name for the tour - /// - [DataMember(Name = "fileName")] - public string? FileName { get; set; } + /// + /// The plugin folder that the tour comes from + /// + /// + /// If this is null it means it's a Core tour + /// + [DataMember(Name = "pluginName")] + public string? PluginName { get; set; } - /// - /// The plugin folder that the tour comes from - /// - /// - /// If this is null it means it's a Core tour - /// - [DataMember(Name = "pluginName")] - public string? PluginName { get; set; } - - [DataMember(Name = "tours")] - public IEnumerable Tours { get; set; } - } + [DataMember(Name = "tours")] + public IEnumerable Tours { get; set; } } diff --git a/src/Umbraco.Core/Models/BackOfficeTourStep.cs b/src/Umbraco.Core/Models/BackOfficeTourStep.cs index aa2aaf7f53..296dcf8bc4 100644 --- a/src/Umbraco.Core/Models/BackOfficeTourStep.cs +++ b/src/Umbraco.Core/Models/BackOfficeTourStep.cs @@ -1,34 +1,43 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing a step in a tour. +/// +[DataContract(Name = "step", Namespace = "")] +public class BackOfficeTourStep { - /// - /// A model representing a step in a tour. - /// - [DataContract(Name = "step", Namespace = "")] - public class BackOfficeTourStep - { - [DataMember(Name = "title")] - public string? Title { get; set; } - [DataMember(Name = "content")] - public string? Content { get; set; } - [DataMember(Name = "type")] - public string? Type { get; set; } - [DataMember(Name = "element")] - public string? Element { get; set; } - [DataMember(Name = "elementPreventClick")] - public bool ElementPreventClick { get; set; } - [DataMember(Name = "backdropOpacity")] - public float? BackdropOpacity { get; set; } - [DataMember(Name = "event")] - public string? Event { get; set; } - [DataMember(Name = "view")] - public string? View { get; set; } - [DataMember(Name = "eventElement")] - public string? EventElement { get; set; } - [DataMember(Name = "customProperties")] - public object? CustomProperties { get; set; } - [DataMember(Name = "skipStepIfVisible")] - public string? SkipStepIfVisible { get; set; } - } + [DataMember(Name = "title")] + public string? Title { get; set; } + + [DataMember(Name = "content")] + public string? Content { get; set; } + + [DataMember(Name = "type")] + public string? Type { get; set; } + + [DataMember(Name = "element")] + public string? Element { get; set; } + + [DataMember(Name = "elementPreventClick")] + public bool ElementPreventClick { get; set; } + + [DataMember(Name = "backdropOpacity")] + public float? BackdropOpacity { get; set; } + + [DataMember(Name = "event")] + public string? Event { get; set; } + + [DataMember(Name = "view")] + public string? View { get; set; } + + [DataMember(Name = "eventElement")] + public string? EventElement { get; set; } + + [DataMember(Name = "customProperties")] + public object? CustomProperties { get; set; } + + [DataMember(Name = "skipStepIfVisible")] + public string? SkipStepIfVisible { get; set; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs index 400649ff05..ee158f9bd8 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs @@ -1,130 +1,126 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Represents a layout item for the Block List editor. +/// +/// +[DataContract(Name = "block", Namespace = "")] +public class BlockListItem : IBlockReference { /// - /// Represents a layout item for the Block List editor. + /// Initializes a new instance of the class. /// - /// - [DataContract(Name = "block", Namespace = "")] - public class BlockListItem : IBlockReference + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + /// + /// contentUdi + /// or + /// content + /// + public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) { - /// - /// Initializes a new instance of the class. - /// - /// The content UDI. - /// The content. - /// The settings UDI. - /// The settings. - /// contentUdi - /// or - /// content - public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) - { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); - Content = content ?? throw new ArgumentNullException(nameof(content)); - SettingsUdi = settingsUdi; - Settings = settings; - } - - /// - /// Gets the content UDI. - /// - /// - /// The content UDI. - /// - [DataMember(Name = "contentUdi")] - public Udi ContentUdi { get; } - - /// - /// Gets the content. - /// - /// - /// The content. - /// - [DataMember(Name = "content")] - public IPublishedElement Content { get; } - - /// - /// Gets the settings UDI. - /// - /// - /// The settings UDI. - /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } - - /// - /// Gets the settings. - /// - /// - /// The settings. - /// - [DataMember(Name = "settings")] - public IPublishedElement Settings { get; } + ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + SettingsUdi = settingsUdi; + Settings = settings; } /// - /// Represents a layout item with a generic content type for the Block List editor. + /// Gets the content. /// - /// The type of the content. - /// - public class BlockListItem : BlockListItem - where T : IPublishedElement - { - /// - /// Initializes a new instance of the class. - /// - /// The content UDI. - /// The content. - /// The settings UDI. - /// The settings. - public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) - : base(contentUdi, content, settingsUdi, settings) - { - Content = content; - } - - /// - /// Gets the content. - /// - /// - /// The content. - /// - public new T Content { get; } - } + /// + /// The content. + /// + [DataMember(Name = "content")] + public IPublishedElement Content { get; } /// - /// Represents a layout item with generic content and settings types for the Block List editor. + /// Gets the settings UDI. /// - /// The type of the content. - /// The type of the settings. - /// - public class BlockListItem : BlockListItem - where TContent : IPublishedElement - where TSettings : IPublishedElement - { - /// - /// Initializes a new instance of the class. - /// - /// The content udi. - /// The content. - /// The settings udi. - /// The settings. - public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) - : base(contentUdi, content, settingsUdi, settings) - { - Settings = settings; - } + /// + /// The settings UDI. + /// + [DataMember(Name = "settingsUdi")] + public Udi SettingsUdi { get; } - /// - /// Gets the settings. - /// - /// - /// The settings. - /// - public new TSettings Settings { get; } - } + /// + /// Gets the content UDI. + /// + /// + /// The content UDI. + /// + [DataMember(Name = "contentUdi")] + public Udi ContentUdi { get; } + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + [DataMember(Name = "settings")] + public IPublishedElement Settings { get; } +} + +/// +/// Represents a layout item with a generic content type for the Block List editor. +/// +/// The type of the content. +/// +public class BlockListItem : BlockListItem + where T : IPublishedElement +{ + /// + /// Initializes a new instance of the class. + /// + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) + : base(contentUdi, content, settingsUdi, settings) => + Content = content; + + /// + /// Gets the content. + /// + /// + /// The content. + /// + public new T Content { get; } +} + +/// +/// Represents a layout item with generic content and settings types for the Block List editor. +/// +/// The type of the content. +/// The type of the settings. +/// +public class BlockListItem : BlockListItem + where TContent : IPublishedElement + where TSettings : IPublishedElement +{ + /// + /// Initializes a new instance of the class. + /// + /// The content udi. + /// The content. + /// The settings udi. + /// The settings. + public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) + : base(contentUdi, content, settingsUdi, settings) => + Settings = settings; + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + public new TSettings Settings { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs index 33a711520b..79afb67d40 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -1,63 +1,63 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// The strongly typed model for the Block List editor. +/// +/// +[DataContract(Name = "blockList", Namespace = "")] +public class BlockListModel : ReadOnlyCollection { /// - /// The strongly typed model for the Block List editor. + /// Initializes a new instance of the class. /// - /// - [DataContract(Name = "blockList", Namespace = "")] - public class BlockListModel : ReadOnlyCollection + /// The list to wrap. + public BlockListModel(IList list) + : base(list) { - /// - /// Gets the empty . - /// - /// - /// The empty . - /// - public static BlockListModel Empty { get; } = new BlockListModel(); - - /// - /// Prevents a default instance of the class from being created. - /// - private BlockListModel() - : this(new List()) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The list to wrap. - public BlockListModel(IList list) - : base(list) - { } - - /// - /// Gets the with the specified content key. - /// - /// - /// The . - /// - /// The content key. - /// - /// The with the specified content key. - /// - public BlockListItem? this[Guid contentKey] => this.FirstOrDefault(x => x.Content.Key == contentKey); - - /// - /// Gets the with the specified content UDI. - /// - /// - /// The . - /// - /// The content UDI. - /// - /// The with the specified content UDI. - /// - public BlockListItem? this[Udi contentUdi] => contentUdi is GuidUdi guidUdi ? this.FirstOrDefault(x => x.Content.Key == guidUdi.Guid) : null; } + + /// + /// Prevents a default instance of the class from being created. + /// + private BlockListModel() + : this(new List()) + { + } + + /// + /// Gets the empty . + /// + /// + /// The empty . + /// + public static BlockListModel Empty { get; } = new(); + + /// + /// Gets the with the specified content key. + /// + /// + /// The . + /// + /// The content key. + /// + /// The with the specified content key. + /// + public BlockListItem? this[Guid contentKey] => this.FirstOrDefault(x => x.Content.Key == contentKey); + + /// + /// Gets the with the specified content UDI. + /// + /// + /// The . + /// + /// The content UDI. + /// + /// The with the specified content UDI. + /// + public BlockListItem? this[Udi contentUdi] => contentUdi is GuidUdi guidUdi + ? this.FirstOrDefault(x => x.Content.Key == guidUdi.Guid) + : null; } diff --git a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs index f8677490ee..96a81641fa 100644 --- a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs +++ b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs @@ -1,36 +1,32 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Blocks; -namespace Umbraco.Cms.Core.Models.Blocks +public struct ContentAndSettingsReference : IEquatable { - public struct ContentAndSettingsReference : IEquatable + public ContentAndSettingsReference(Udi? contentUdi, Udi? settingsUdi) { - public ContentAndSettingsReference(Udi? contentUdi, Udi? settingsUdi) - { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); - SettingsUdi = settingsUdi; - } - - public Udi ContentUdi { get; } - - public Udi? SettingsUdi { get; } - - public override bool Equals(object? obj) => obj is ContentAndSettingsReference reference && Equals(reference); - - public bool Equals(ContentAndSettingsReference other) => other != null - && EqualityComparer.Default.Equals(ContentUdi, other.ContentUdi) - && EqualityComparer.Default.Equals(SettingsUdi, other.SettingsUdi); - - public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode(); - - public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) - { - return left.Equals(right); - } - - public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right) - { - return !(left == right); - } + ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + SettingsUdi = settingsUdi; } + + public Udi ContentUdi { get; } + + public Udi? SettingsUdi { get; } + + public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) => + left.Equals(right); + + public override bool Equals(object? obj) => obj is ContentAndSettingsReference reference && Equals(reference); + + public bool Equals(ContentAndSettingsReference other) => other != null + && EqualityComparer.Default.Equals( + ContentUdi, + other.ContentUdi) + && EqualityComparer.Default.Equals( + SettingsUdi, + other.SettingsUdi); + + public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode(); + + public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right) => + !(left == right); } diff --git a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs index 48c2b85637..44533407d2 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs @@ -1,37 +1,38 @@ -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Represents a data item reference for a Block Editor implementation. +/// +/// +/// See: +/// https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed +/// +public interface IBlockReference { /// - /// Represents a data item reference for a Block Editor implementation. + /// Gets the content UDI. /// - /// - /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed - /// - public interface IBlockReference - { - /// - /// Gets the content UDI. - /// - /// - /// The content UDI. - /// - Udi ContentUdi { get; } - } - - /// - /// Represents a data item reference with settings for a Block editor implementation. - /// - /// The type of the settings. - /// - /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed - /// - public interface IBlockReference : IBlockReference - { - /// - /// Gets the settings. - /// - /// - /// The settings. - /// - TSettings Settings { get; } - } + /// + /// The content UDI. + /// + Udi ContentUdi { get; } +} + +/// +/// Represents a data item reference with settings for a Block editor implementation. +/// +/// The type of the settings. +/// +/// See: +/// https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed +/// +public interface IBlockReference : IBlockReference +{ + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + TSettings Settings { get; } } diff --git a/src/Umbraco.Core/Models/CacheInstruction.cs b/src/Umbraco.Core/Models/CacheInstruction.cs index 5434f443a0..a93ec030c8 100644 --- a/src/Umbraco.Core/Models/CacheInstruction.cs +++ b/src/Umbraco.Core/Models/CacheInstruction.cs @@ -1,51 +1,48 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a cache instruction. +/// +[Serializable] +[DataContract(IsReference = true)] +public class CacheInstruction { /// - /// Represents a cache instruction. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class CacheInstruction + public CacheInstruction(int id, DateTime utcStamp, string instructions, string originIdentity, int instructionCount) { - /// - /// Initializes a new instance of the class. - /// - public CacheInstruction(int id, DateTime utcStamp, string instructions, string originIdentity, int instructionCount) - { - Id = id; - UtcStamp = utcStamp; - Instructions = instructions; - OriginIdentity = originIdentity; - InstructionCount = instructionCount; - } - - /// - /// Cache instruction Id. - /// - public int Id { get; } - - /// - /// Cache instruction created date. - /// - public DateTime UtcStamp { get; } - - /// - /// Serialized instructions. - /// - public string Instructions { get; } - - /// - /// Identity of server originating the instruction. - /// - public string OriginIdentity { get; } - - /// - /// Count of instructions. - /// - public int InstructionCount { get; } - + Id = id; + UtcStamp = utcStamp; + Instructions = instructions; + OriginIdentity = originIdentity; + InstructionCount = instructionCount; } + + /// + /// Cache instruction Id. + /// + public int Id { get; } + + /// + /// Cache instruction created date. + /// + public DateTime UtcStamp { get; } + + /// + /// Serialized instructions. + /// + public string Instructions { get; } + + /// + /// Identity of server originating the instruction. + /// + public string OriginIdentity { get; } + + /// + /// Count of instructions. + /// + public int InstructionCount { get; } } diff --git a/src/Umbraco.Core/Models/ChangingPasswordModel.cs b/src/Umbraco.Core/Models/ChangingPasswordModel.cs index be19f13b75..946bcde9ab 100644 --- a/src/Umbraco.Core/Models/ChangingPasswordModel.cs +++ b/src/Umbraco.Core/Models/ChangingPasswordModel.cs @@ -1,29 +1,28 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing the data required to set a member/user password depending on the provider installed. +/// +public class ChangingPasswordModel { /// - /// A model representing the data required to set a member/user password depending on the provider installed. + /// The password value /// - public class ChangingPasswordModel - { - /// - /// The password value - /// - [DataMember(Name = "newPassword")] - public string? NewPassword { get; set; } + [DataMember(Name = "newPassword")] + public string? NewPassword { get; set; } - /// - /// The old password - used to change a password when: EnablePasswordRetrieval = false - /// - [DataMember(Name = "oldPassword")] - public string? OldPassword { get; set; } + /// + /// The old password - used to change a password when: EnablePasswordRetrieval = false + /// + [DataMember(Name = "oldPassword")] + public string? OldPassword { get; set; } - /// - /// The ID of the current user/member requesting the password change - /// For users, required to allow changing password without the entire UserSave model - /// - [DataMember(Name = "id")] - public int Id { get; set; } - } + /// + /// The ID of the current user/member requesting the password change + /// For users, required to allow changing password without the entire UserSave model + /// + [DataMember(Name = "id")] + public int Id { get; set; } } diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs index 2354c67b1e..e71f040ba8 100644 --- a/src/Umbraco.Core/Models/Consent.cs +++ b/src/Umbraco.Core/Models/Consent.cs @@ -1,86 +1,96 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a consent. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Consent : EntityBase, IConsent { + private string? _action; + private string? _comment; + private string? _context; + private bool _current; + private string? _source; + private ConsentState _state; + /// - /// Represents a consent. + /// Gets the previous states of this consent. /// - [Serializable] - [DataContract(IsReference = true)] - public class Consent : EntityBase, IConsent + public List? HistoryInternal { get; set; } + + /// + public bool Current { - private bool _current; - private string? _source; - private string? _context; - private string? _action; - private ConsentState _state; - private string? _comment; - - /// - public bool Current - { - get => _current; - set => SetPropertyValueAndDetectChanges(value, ref _current, nameof(Current)); - } - - /// - public string? Source - { - get => _source; - set - { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _source, nameof(Source)); - } - } - - /// - public string? Context - { - get => _context; - set - { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _context, nameof(Context)); - } - } - - /// - public string? Action - { - get => _action; - set - { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _action, nameof(Action)); - } - } - - /// - public ConsentState State - { - get => _state; - // note: we probably should validate the state here, but since the - // enum is [Flags] with many combinations, this could be expensive - set => SetPropertyValueAndDetectChanges(value, ref _state, nameof(State)); - } - - /// - public string? Comment - { - get => _comment; - set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); - } - - /// - public IEnumerable? History => HistoryInternal; - - /// - /// Gets the previous states of this consent. - /// - public List? HistoryInternal { get; set; } + get => _current; + set => SetPropertyValueAndDetectChanges(value, ref _current, nameof(Current)); } + + /// + public string? Source + { + get => _source; + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(nameof(value)); + } + + SetPropertyValueAndDetectChanges(value, ref _source, nameof(Source)); + } + } + + /// + public string? Context + { + get => _context; + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(nameof(value)); + } + + SetPropertyValueAndDetectChanges(value, ref _context, nameof(Context)); + } + } + + /// + public string? Action + { + get => _action; + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(nameof(value)); + } + + SetPropertyValueAndDetectChanges(value, ref _action, nameof(Action)); + } + } + + /// + public ConsentState State + { + get => _state; + + // note: we probably should validate the state here, but since the + // enum is [Flags] with many combinations, this could be expensive + set => SetPropertyValueAndDetectChanges(value, ref _state, nameof(State)); + } + + /// + public string? Comment + { + get => _comment; + set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); + } + + /// + public IEnumerable? History => HistoryInternal; } diff --git a/src/Umbraco.Core/Models/ConsentExtensions.cs b/src/Umbraco.Core/Models/ConsentExtensions.cs index b95c7b66f9..1dc6cadde8 100644 --- a/src/Umbraco.Core/Models/ConsentExtensions.cs +++ b/src/Umbraco.Core/Models/ConsentExtensions.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the interface. +/// +public static class ConsentExtensions { /// - /// Provides extension methods for the interface. + /// Determines whether the consent is granted. /// - public static class ConsentExtensions - { - /// - /// Determines whether the consent is granted. - /// - public static bool IsGranted(this IConsent consent) => (consent.State & ConsentState.Granted) > 0; + public static bool IsGranted(this IConsent consent) => (consent.State & ConsentState.Granted) > 0; - /// - /// Determines whether the consent is revoked. - /// - public static bool IsRevoked(this IConsent consent) => (consent.State & ConsentState.Revoked) > 0; - } + /// + /// Determines whether the consent is revoked. + /// + public static bool IsRevoked(this IConsent consent) => (consent.State & ConsentState.Revoked) > 0; } diff --git a/src/Umbraco.Core/Models/ConsentState.cs b/src/Umbraco.Core/Models/ConsentState.cs index 0828561ff8..8a6846b28c 100644 --- a/src/Umbraco.Core/Models/ConsentState.cs +++ b/src/Umbraco.Core/Models/ConsentState.cs @@ -1,38 +1,35 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents the state of a consent. +/// +[Flags] +public enum ConsentState // : int { + // note - this is a [Flags] enumeration + // on can create detailed flags such as: + // GrantedOptIn = Granted | 0x0001 + // GrandedByForce = Granted | 0x0002 + // + // 16 situations for each Pending/Granted/Revoked should be ok + /// - /// Represents the state of a consent. + /// There is no consent. /// - [Flags] - public enum ConsentState // : int - { - // note - this is a [Flags] enumeration - // on can create detailed flags such as: - //GrantedOptIn = Granted | 0x0001 - //GrandedByForce = Granted | 0x0002 - // - // 16 situations for each Pending/Granted/Revoked should be ok + None = 0, - /// - /// There is no consent. - /// - None = 0, + /// + /// Consent is pending and has not been granted yet. + /// + Pending = 0x10000, - /// - /// Consent is pending and has not been granted yet. - /// - Pending = 0x10000, + /// + /// Consent has been granted. + /// + Granted = 0x20000, - /// - /// Consent has been granted. - /// - Granted = 0x20000, - - /// - /// Consent has been revoked. - /// - Revoked = 0x40000 - } + /// + /// Consent has been revoked. + /// + Revoked = 0x40000, } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index bc77e52624..4e251a323e 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -1,470 +1,567 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Content object +/// +[Serializable] +[DataContract(IsReference = true)] +public class Content : ContentBase, IContent { + private HashSet? _editedCultures; + private bool _published; + private PublishedState _publishedState; + private ContentCultureInfosCollection? _publishInfos; + private int? _templateId; + /// - /// Represents a Content object + /// Constructor for creating a Content object /// - [Serializable] - [DataContract(IsReference = true)] - public class Content : ContentBase, IContent + /// Name of the content + /// Parent object + /// ContentType for the current Content object + /// An optional culture. + public Content(string name, IContent parent, IContentType contentType, string? culture = null) + : this(name, parent, contentType, new PropertyCollection(), culture) { - private int? _templateId; - private bool _published; - private PublishedState _publishedState; - private HashSet? _editedCultures; - private ContentCultureInfosCollection? _publishInfos; + } - #region Used for change tracking + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Parent object + /// ContentType for the current Content object + /// The identifier of the user creating the Content object + /// An optional culture. + public Content(string name, IContent parent, IContentType contentType, int userId, string? culture = null) + : this(name, parent, contentType, new PropertyCollection(), culture) + { + CreatorId = userId; + WriterId = userId; + } - private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) _currentPublishCultureChanges; - private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) _previousPublishCultureChanges; - - #endregion - - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Parent object - /// ContentType for the current Content object - /// An optional culture. - public Content(string name, IContent parent, IContentType contentType, string? culture = null) - : this(name, parent, contentType, new PropertyCollection(), culture) - { } - - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Parent object - /// ContentType for the current Content object - /// The identifier of the user creating the Content object - /// An optional culture. - public Content(string name, IContent parent, IContentType contentType, int userId, string? culture = null) - : this(name, parent, contentType, new PropertyCollection(), culture) + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Parent object + /// ContentType for the current Content object + /// Collection of properties + /// An optional culture. + public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties, string? culture = null) + : base(name, parent, contentType, properties, culture) + { + if (contentType == null) { - CreatorId = userId; - WriterId = userId; + throw new ArgumentNullException(nameof(contentType)); } - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Parent object - /// ContentType for the current Content object - /// Collection of properties - /// An optional culture. - public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties, string? culture = null) - : base(name, parent, contentType, properties, culture) + _publishedState = PublishedState.Unpublished; + PublishedVersionId = 0; + } + + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Id of the Parent content + /// ContentType for the current Content object + /// An optional culture. + public Content(string? name, int parentId, IContentType? contentType, string? culture = null) + : this(name, parentId, contentType, new PropertyCollection(), culture) + { + } + + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Id of the Parent content + /// ContentType for the current Content object + /// The identifier of the user creating the Content object + /// An optional culture. + public Content(string name, int parentId, IContentType contentType, int userId, string? culture = null) + : this(name, parentId, contentType, new PropertyCollection(), culture) + { + CreatorId = userId; + WriterId = userId; + } + + /// + /// Constructor for creating a Content object + /// + /// Name of the content + /// Id of the Parent content + /// ContentType for the current Content object + /// Collection of properties + /// An optional culture. + public Content(string? name, int parentId, IContentType? contentType, PropertyCollection properties, string? culture = null) + : base(name, parentId, contentType, properties, culture) + { + if (contentType == null) { - if (contentType == null) throw new ArgumentNullException(nameof(contentType)); - _publishedState = PublishedState.Unpublished; - PublishedVersionId = 0; + throw new ArgumentNullException(nameof(contentType)); } - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Id of the Parent content - /// ContentType for the current Content object - /// An optional culture. - public Content(string? name, int parentId, IContentType? contentType, string? culture = null) - : this(name, parentId, contentType, new PropertyCollection(), culture) - { } + _publishedState = PublishedState.Unpublished; + PublishedVersionId = 0; + } - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Id of the Parent content - /// ContentType for the current Content object - /// The identifier of the user creating the Content object - /// An optional culture. - public Content(string name, int parentId, IContentType contentType, int userId, string? culture = null) - : this(name, parentId, contentType, new PropertyCollection(), culture) + /// + /// Gets or sets the template used by the Content. + /// This is used to override the default one from the ContentType. + /// + /// + /// If no template is explicitly set on the Content object, + /// the Default template from the ContentType will be returned. + /// + [DataMember] + public int? TemplateId + { + get => _templateId; + set => SetPropertyValueAndDetectChanges(value, ref _templateId, nameof(TemplateId)); + } + + /// + /// Gets or sets a value indicating whether this content item is published or not. + /// + /// + /// the setter is should only be invoked from + /// - the ContentFactory when creating a content entity from a dto + /// - the ContentRepository when updating a content entity + /// + [DataMember] + public bool Published + { + get => _published; + set { - CreatorId = userId; - WriterId = userId; - } - - /// - /// Constructor for creating a Content object - /// - /// Name of the content - /// Id of the Parent content - /// ContentType for the current Content object - /// Collection of properties - /// An optional culture. - public Content(string? name, int parentId, IContentType? contentType, PropertyCollection properties, string? culture = null) - : base(name, parentId, contentType, properties, culture) - { - if (contentType == null) throw new ArgumentNullException(nameof(contentType)); - _publishedState = PublishedState.Unpublished; - PublishedVersionId = 0; - } - - /// - /// Gets or sets the template used by the Content. - /// This is used to override the default one from the ContentType. - /// - /// - /// If no template is explicitly set on the Content object, - /// the Default template from the ContentType will be returned. - /// - [DataMember] - public int? TemplateId - { - get => _templateId; - set => SetPropertyValueAndDetectChanges(value, ref _templateId, nameof(TemplateId)); - } - - /// - /// Gets or sets a value indicating whether this content item is published or not. - /// - /// - /// the setter is should only be invoked from - /// - the ContentFactory when creating a content entity from a dto - /// - the ContentRepository when updating a content entity - /// - [DataMember] - public bool Published - { - get => _published; - set - { - SetPropertyValueAndDetectChanges(value, ref _published, nameof(Published)); - _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; - } - } - - /// - /// Gets the published state of the content item. - /// - /// The state should be Published or Unpublished, depending on whether Published - /// is true or false, but can also temporarily be Publishing or Unpublishing when the - /// content item is about to be saved. - [DataMember] - public PublishedState PublishedState - { - get => _publishedState; - set - { - if (value != PublishedState.Publishing && value != PublishedState.Unpublishing) - throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted."); - _publishedState = value; - } - } - - [IgnoreDataMember] - public bool Edited { get; set; } - - /// - [IgnoreDataMember] - public DateTime? PublishDate { get; set; } // set by persistence - - /// - [IgnoreDataMember] - public int? PublisherId { get; set; } // set by persistence - - /// - [IgnoreDataMember] - public int? PublishTemplateId { get; set; } // set by persistence - - /// - [IgnoreDataMember] - public string? PublishName { get; set; } // set by persistence - - /// - [IgnoreDataMember] - public IEnumerable? EditedCultures - { - get => CultureInfos?.Keys.Where(IsCultureEdited); - set => _editedCultures = value == null ? null : new HashSet(value, StringComparer.OrdinalIgnoreCase); - } - - /// - [IgnoreDataMember] - public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); - - /// - public bool IsCulturePublished(string culture) - // just check _publishInfos - // a non-available culture could not become published anyways - => !culture.IsNullOrWhiteSpace() && _publishInfos != null && _publishInfos.ContainsKey(culture); - - /// - public bool IsCultureEdited(string culture) - => IsCultureAvailable(culture) && // is available, and - (!IsCulturePublished(culture) || // is not published, or - (_editedCultures != null && _editedCultures.Contains(culture))); // is edited - - /// - [IgnoreDataMember] - public ContentCultureInfosCollection? PublishCultureInfos - { - get - { - if (_publishInfos != null) return _publishInfos; - _publishInfos = new ContentCultureInfosCollection(); - _publishInfos.CollectionChanged += PublishNamesCollectionChanged; - return _publishInfos; - } - set - { - if (_publishInfos != null) - { - _publishInfos.ClearCollectionChangedEvents(); - } - - _publishInfos = value; - if (_publishInfos != null) - { - _publishInfos.CollectionChanged += PublishNamesCollectionChanged; - } - } - } - - /// - public string? GetPublishName(string? culture) - { - if (culture.IsNullOrWhiteSpace()) return PublishName; - if (!ContentType.VariesByCulture()) return null; - if (_publishInfos == null) return null; - return _publishInfos.TryGetValue(culture!, out var infos) ? infos.Name : null; - } - - /// - public DateTime? GetPublishDate(string culture) - { - if (culture.IsNullOrWhiteSpace()) return PublishDate; - if (!ContentType.VariesByCulture()) return null; - if (_publishInfos == null) return null; - return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?)null; - } - - /// - /// Handles culture infos collection changes. - /// - private void PublishNamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(PublishCultureInfos)); - - //we don't need to handle other actions, only add/remove, however we could implement Replace and track updated cultures in _updatedCultures too - //which would allows us to continue doing WasCulturePublished, but don't think we need it anymore - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - { - var cultureInfo = e.NewItems?.Cast().First(); - if (_currentPublishCultureChanges.addedCultures == null) _currentPublishCultureChanges.addedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (_currentPublishCultureChanges.updatedCultures == null) _currentPublishCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentPublishCultureChanges.addedCultures.Add(cultureInfo.Culture); - _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); - _currentPublishCultureChanges.removedCultures?.Remove(cultureInfo.Culture); - } - break; - } - case NotifyCollectionChangedAction.Remove: - { - //remove listening for changes - var cultureInfo = e.OldItems?.Cast().First(); - if (_currentPublishCultureChanges.removedCultures == null) _currentPublishCultureChanges.removedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentPublishCultureChanges.removedCultures.Add(cultureInfo.Culture); - _currentPublishCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); - _currentPublishCultureChanges.addedCultures?.Remove(cultureInfo.Culture); - } - break; - } - case NotifyCollectionChangedAction.Replace: - { - //replace occurs when an Update occurs - var cultureInfo = e.NewItems?.Cast().First(); - if (_currentPublishCultureChanges.updatedCultures == null) _currentPublishCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); - } - break; - } - } - } - - [IgnoreDataMember] - public int PublishedVersionId { get; set; } - - [DataMember] - public bool Blueprint { get; set; } - - /// - /// Changes the for the current content object - /// - /// New ContentType for this content - /// Leaves PropertyTypes intact after change - internal void ChangeContentType(IContentType contentType) - { - ChangeContentType(contentType, false); - } - - /// - /// Changes the for the current content object and removes PropertyTypes, - /// which are not part of the new ContentType. - /// - /// New ContentType for this content - /// Boolean indicating whether to clear PropertyTypes upon change - internal void ChangeContentType(IContentType contentType, bool clearProperties) - { - ChangeContentType(new SimpleContentType(contentType)); - - if (clearProperties) - Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes); - else - Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); - - Properties.ClearCollectionChangedEvents(); // be sure not to double add - Properties.CollectionChanged += PropertiesChanged; - } - - public override void ResetWereDirtyProperties() - { - base.ResetWereDirtyProperties(); - _previousPublishCultureChanges.updatedCultures = null; - _previousPublishCultureChanges.removedCultures = null; - _previousPublishCultureChanges.addedCultures = null; - } - - public override void ResetDirtyProperties(bool rememberDirty) - { - base.ResetDirtyProperties(rememberDirty); - - if (rememberDirty) - { - _previousPublishCultureChanges.addedCultures = _currentPublishCultureChanges.addedCultures == null || _currentPublishCultureChanges.addedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase); - _previousPublishCultureChanges.removedCultures = _currentPublishCultureChanges.removedCultures == null || _currentPublishCultureChanges.removedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase); - _previousPublishCultureChanges.updatedCultures = _currentPublishCultureChanges.updatedCultures == null || _currentPublishCultureChanges.updatedCultures.Count == 0 ? null : new HashSet(_currentPublishCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase); - } - else - { - _previousPublishCultureChanges.addedCultures = null; - _previousPublishCultureChanges.removedCultures = null; - _previousPublishCultureChanges.updatedCultures = null; - } - _currentPublishCultureChanges.addedCultures?.Clear(); - _currentPublishCultureChanges.removedCultures?.Clear(); - _currentPublishCultureChanges.updatedCultures?.Clear(); - - // take care of the published state + SetPropertyValueAndDetectChanges(value, ref _published, nameof(Published)); _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; - - if (_publishInfos == null) return; - - foreach (var infos in _publishInfos) - infos.ResetDirtyProperties(rememberDirty); - } - - /// - /// Overridden to check special keys. - public override bool IsPropertyDirty(string propertyName) - { - //Special check here since we want to check if the request is for changed cultures - if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); - return _currentPublishCultureChanges.addedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); - return _currentPublishCultureChanges.removedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); - return _currentPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; - } - - return base.IsPropertyDirty(propertyName); - } - - /// - /// Overridden to check special keys. - public override bool WasPropertyDirty(string propertyName) - { - //Special check here since we want to check if the request is for changed cultures - if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); - return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); - return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); - return _previousPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; - } - - return base.WasPropertyDirty(propertyName); - } - - /// - /// Creates a deep clone of the current entity with its identity and it's property identities reset - /// - /// - public IContent DeepCloneWithResetIdentities() - { - var clone = (Content)DeepClone(); - clone.Key = Guid.Empty; - clone.VersionId = clone.PublishedVersionId = 0; - clone.ResetIdentity(); - - foreach (var property in clone.Properties) - property.ResetIdentity(); - - return clone; - } - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedContent = (Content)clone; - - //fixme - need to reset change tracking bits - - //if culture infos exist then deal with event bindings - if (clonedContent._publishInfos != null) - { - clonedContent._publishInfos.ClearCollectionChangedEvents(); //clear this event handler if any - clonedContent._publishInfos = (ContentCultureInfosCollection?)_publishInfos?.DeepClone(); //manually deep clone - if (clonedContent._publishInfos is not null) - { - clonedContent._publishInfos.CollectionChanged += - clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler - } - } - - clonedContent._currentPublishCultureChanges.updatedCultures = null; - clonedContent._currentPublishCultureChanges.addedCultures = null; - clonedContent._currentPublishCultureChanges.removedCultures = null; - - clonedContent._previousPublishCultureChanges.updatedCultures = null; - clonedContent._previousPublishCultureChanges.addedCultures = null; - clonedContent._previousPublishCultureChanges.removedCultures = null; } } + + /// + /// Gets the published state of the content item. + /// + /// + /// The state should be Published or Unpublished, depending on whether Published + /// is true or false, but can also temporarily be Publishing or Unpublishing when the + /// content item is about to be saved. + /// + [DataMember] + public PublishedState PublishedState + { + get => _publishedState; + set + { + if (value != PublishedState.Publishing && value != PublishedState.Unpublishing) + { + throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted."); + } + + _publishedState = value; + } + } + + [IgnoreDataMember] + public bool Edited { get; set; } + + /// + [IgnoreDataMember] + public DateTime? PublishDate { get; set; } // set by persistence + + /// + [IgnoreDataMember] + public int? PublisherId { get; set; } // set by persistence + + /// + [IgnoreDataMember] + public int? PublishTemplateId { get; set; } // set by persistence + + /// + [IgnoreDataMember] + public string? PublishName { get; set; } // set by persistence + + /// + [IgnoreDataMember] + public IEnumerable? EditedCultures + { + get => CultureInfos?.Keys.Where(IsCultureEdited); + set => _editedCultures = value == null ? null : new HashSet(value, StringComparer.OrdinalIgnoreCase); + } + + /// + [IgnoreDataMember] + public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); + + /// + public bool IsCulturePublished(string culture) + + // just check _publishInfos + // a non-available culture could not become published anyways + => !culture.IsNullOrWhiteSpace() && _publishInfos != null && _publishInfos.ContainsKey(culture); + + /// + public bool IsCultureEdited(string culture) + => IsCultureAvailable(culture) && // is available, and + (!IsCulturePublished(culture) || // is not published, or + (_editedCultures != null && _editedCultures.Contains(culture))); // is edited + + /// + [IgnoreDataMember] + public ContentCultureInfosCollection? PublishCultureInfos + { + get + { + if (_publishInfos != null) + { + return _publishInfos; + } + + _publishInfos = new ContentCultureInfosCollection(); + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; + return _publishInfos; + } + + set + { + if (_publishInfos != null) + { + _publishInfos.ClearCollectionChangedEvents(); + } + + _publishInfos = value; + if (_publishInfos != null) + { + _publishInfos.CollectionChanged += PublishNamesCollectionChanged; + } + } + } + + /// + public string? GetPublishName(string? culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return PublishName; + } + + if (!ContentType.VariesByCulture()) + { + return null; + } + + if (_publishInfos == null) + { + return null; + } + + return _publishInfos.TryGetValue(culture!, out ContentCultureInfos infos) ? infos.Name : null; + } + + /// + public DateTime? GetPublishDate(string culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return PublishDate; + } + + if (!ContentType.VariesByCulture()) + { + return null; + } + + if (_publishInfos == null) + { + return null; + } + + return _publishInfos.TryGetValue(culture, out ContentCultureInfos infos) ? infos.Date : null; + } + + [IgnoreDataMember] + public int PublishedVersionId { get; set; } + + [DataMember] + public bool Blueprint { get; set; } + + public override void ResetWereDirtyProperties() + { + base.ResetWereDirtyProperties(); + _previousPublishCultureChanges.updatedCultures = null; + _previousPublishCultureChanges.removedCultures = null; + _previousPublishCultureChanges.addedCultures = null; + } + + public override void ResetDirtyProperties(bool rememberDirty) + { + base.ResetDirtyProperties(rememberDirty); + + if (rememberDirty) + { + _previousPublishCultureChanges.addedCultures = + _currentPublishCultureChanges.addedCultures == null || + _currentPublishCultureChanges.addedCultures.Count == 0 + ? null + : new HashSet(_currentPublishCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase); + _previousPublishCultureChanges.removedCultures = + _currentPublishCultureChanges.removedCultures == null || + _currentPublishCultureChanges.removedCultures.Count == 0 + ? null + : new HashSet(_currentPublishCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase); + _previousPublishCultureChanges.updatedCultures = + _currentPublishCultureChanges.updatedCultures == null || + _currentPublishCultureChanges.updatedCultures.Count == 0 + ? null + : new HashSet(_currentPublishCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase); + } + else + { + _previousPublishCultureChanges.addedCultures = null; + _previousPublishCultureChanges.removedCultures = null; + _previousPublishCultureChanges.updatedCultures = null; + } + + _currentPublishCultureChanges.addedCultures?.Clear(); + _currentPublishCultureChanges.removedCultures?.Clear(); + _currentPublishCultureChanges.updatedCultures?.Clear(); + + // take care of the published state + _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; + + if (_publishInfos == null) + { + return; + } + + foreach (ContentCultureInfos infos in _publishInfos) + { + infos.ResetDirtyProperties(rememberDirty); + } + } + + /// + /// Overridden to check special keys. + public override bool IsPropertyDirty(string propertyName) + { + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); + return _currentPublishCultureChanges.addedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); + return _currentPublishCultureChanges.removedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); + return _currentPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return base.IsPropertyDirty(propertyName); + } + + /// + /// Overridden to check special keys. + public override bool WasPropertyDirty(string propertyName) + { + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture); + return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture); + return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture); + return _previousPublishCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return base.WasPropertyDirty(propertyName); + } + + /// + /// Creates a deep clone of the current entity with its identity and it's property identities reset + /// + /// + public IContent DeepCloneWithResetIdentities() + { + var clone = (Content)DeepClone(); + clone.Key = Guid.Empty; + clone.VersionId = clone.PublishedVersionId = 0; + clone.ResetIdentity(); + + foreach (IProperty property in clone.Properties) + { + property.ResetIdentity(); + } + + return clone; + } + + /// + /// Handles culture infos collection changes. + /// + private void PublishNamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(PublishCultureInfos)); + + // we don't need to handle other actions, only add/remove, however we could implement Replace and track updated cultures in _updatedCultures too + // which would allows us to continue doing WasCulturePublished, but don't think we need it anymore + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentPublishCultureChanges.addedCultures == null) + { + _currentPublishCultureChanges.addedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (_currentPublishCultureChanges.updatedCultures == null) + { + _currentPublishCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentPublishCultureChanges.addedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.removedCultures?.Remove(cultureInfo.Culture); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + // Remove listening for changes + ContentCultureInfos? cultureInfo = e.OldItems?.Cast().First(); + if (_currentPublishCultureChanges.removedCultures == null) + { + _currentPublishCultureChanges.removedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentPublishCultureChanges.removedCultures.Add(cultureInfo.Culture); + _currentPublishCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); + _currentPublishCultureChanges.addedCultures?.Remove(cultureInfo.Culture); + } + + break; + } + + case NotifyCollectionChangedAction.Replace: + { + // Replace occurs when an Update occurs + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentPublishCultureChanges.updatedCultures == null) + { + _currentPublishCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture); + } + + break; + } + } + } + + /// + /// Changes the for the current content object + /// + /// New ContentType for this content + /// Leaves PropertyTypes intact after change + internal void ChangeContentType(IContentType contentType) => ChangeContentType(contentType, false); + + /// + /// Changes the for the current content object and removes PropertyTypes, + /// which are not part of the new ContentType. + /// + /// New ContentType for this content + /// Boolean indicating whether to clear PropertyTypes upon change + internal void ChangeContentType(IContentType contentType, bool clearProperties) + { + ChangeContentType(new SimpleContentType(contentType)); + + if (clearProperties) + { + Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes); + } + else + { + Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); + } + + Properties.ClearCollectionChangedEvents(); // be sure not to double add + Properties.CollectionChanged += PropertiesChanged; + } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedContent = (Content)clone; + + // fixme - need to reset change tracking bits + + // if culture infos exist then deal with event bindings + if (clonedContent._publishInfos != null) + { + // Clear this event handler if any + clonedContent._publishInfos.ClearCollectionChangedEvents(); + + // Manually deep clone + clonedContent._publishInfos = (ContentCultureInfosCollection?)_publishInfos?.DeepClone(); + if (clonedContent._publishInfos is not null) + { + // Re-assign correct event handler + clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; + } + } + + clonedContent._currentPublishCultureChanges.updatedCultures = null; + clonedContent._currentPublishCultureChanges.addedCultures = null; + clonedContent._currentPublishCultureChanges.removedCultures = null; + + clonedContent._previousPublishCultureChanges.updatedCultures = null; + clonedContent._previousPublishCultureChanges.addedCultures = null; + clonedContent._previousPublishCultureChanges.removedCultures = null; + } + + #region Used for change tracking + + private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) + _currentPublishCultureChanges; + + private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) + _previousPublishCultureChanges; + + #endregion } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index d9223130d6..e9fcc61e7c 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -1,530 +1,639 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; +using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an abstract class for base Content properties and methods +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentType.Alias}")] +public abstract class ContentBase : TreeEntityBase, IContentBase { + private int _contentTypeId; + private ContentCultureInfosCollection? _cultureInfos; + private IPropertyCollection _properties; + private int _writerId; + /// - /// Represents an abstract class for base Content properties and methods + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentType.Alias}")] - public abstract class ContentBase : TreeEntityBase, IContentBase + protected ContentBase(string? name, int parentId, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) + : this(name, contentType, properties, culture) { - private int _contentTypeId; - private int _writerId; - private IPropertyCollection _properties; - private ContentCultureInfosCollection? _cultureInfos; - internal IReadOnlyList AllPropertyTypes { get; } - - #region Used for change tracking - - private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) _currentCultureChanges; - private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) _previousCultureChanges; - - public static class ChangeTrackingPrefix + if (parentId == 0) { - public const string UpdatedCulture = "_updatedCulture_"; - public const string ChangedCulture = "_changedCulture_"; - public const string PublishedCulture = "_publishedCulture_"; - public const string UnpublishedCulture = "_unpublishedCulture_"; - public const string AddedCulture = "_addedCulture_"; - public const string RemovedCulture = "_removedCulture_"; + throw new ArgumentOutOfRangeException(nameof(parentId)); } - #endregion + ParentId = parentId; + } - /// - /// Initializes a new instance of the class. - /// - protected ContentBase(string? name, int parentId, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) - : this(name, contentType, properties, culture) + /// + /// Initializes a new instance of the class. + /// + protected ContentBase(string? name, IContentBase? parent, IContentTypeComposition contentType, IPropertyCollection properties, string? culture = null) + : this(name, contentType, properties, culture) + { + if (parent == null) { - if (parentId == 0) throw new ArgumentOutOfRangeException(nameof(parentId)); - ParentId = parentId; + throw new ArgumentNullException(nameof(parent)); } - /// - /// Initializes a new instance of the class. - /// - protected ContentBase(string? name, IContentBase? parent, IContentTypeComposition contentType, IPropertyCollection properties, string? culture = null) - : this(name, contentType, properties, culture) + SetParent(parent); + } + + private ContentBase(string? name, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) + { + ContentType = contentType?.ToSimple() ?? throw new ArgumentNullException(nameof(contentType)); + + // initially, all new instances have + Id = 0; // no identity + VersionId = 0; // no versions + + SetCultureName(name, culture); + + _contentTypeId = contentType.Id; + _properties = properties ?? throw new ArgumentNullException(nameof(properties)); + _properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); + + // track all property types on this content type, these can never change during the lifetime of this single instance + // there is no real extra memory overhead of doing this since these property types are already cached on this object via the + // properties already. + AllPropertyTypes = new List(contentType.CompositionPropertyTypes); + } + + internal IReadOnlyList AllPropertyTypes { get; } + + [IgnoreDataMember] + public ISimpleContentType ContentType { get; private set; } + + /// + /// Id of the user who wrote/updated this entity + /// + [DataMember] + public int WriterId + { + get => _writerId; + set => SetPropertyValueAndDetectChanges(value, ref _writerId, nameof(WriterId)); + } + + [IgnoreDataMember] + public int VersionId { get; set; } + + /// + /// Integer Id of the default ContentType + /// + [DataMember] + public int ContentTypeId + { + get { - if (parent == null) throw new ArgumentNullException(nameof(parent)); - SetParent(parent); - } - - private ContentBase(string? name, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null) - { - ContentType = contentType?.ToSimple() ?? throw new ArgumentNullException(nameof(contentType)); - - // initially, all new instances have - Id = 0; // no identity - VersionId = 0; // no versions - - SetCultureName(name, culture); - - _contentTypeId = contentType.Id; - _properties = properties ?? throw new ArgumentNullException(nameof(properties)); - _properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes); - - //track all property types on this content type, these can never change during the lifetime of this single instance - //there is no real extra memory overhead of doing this since these property types are already cached on this object via the - //properties already. - AllPropertyTypes = new List(contentType.CompositionPropertyTypes); - } - - [IgnoreDataMember] - public ISimpleContentType ContentType { get; private set; } - - public void ChangeContentType(ISimpleContentType contentType) - { - ContentType = contentType; - ContentTypeId = contentType.Id; - } - - protected void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(Properties)); - } - - /// - /// Id of the user who wrote/updated this entity - /// - [DataMember] - public int WriterId - { - get => _writerId; - set => SetPropertyValueAndDetectChanges(value, ref _writerId, nameof(WriterId)); - } - - [IgnoreDataMember] - public int VersionId { get; set; } - - /// - /// Integer Id of the default ContentType - /// - [DataMember] - public int ContentTypeId - { - get + // There will be cases where this has not been updated to reflect the true content type ID. + // This will occur when inserting new content. + if (_contentTypeId == 0 && ContentType != null) { - //There will be cases where this has not been updated to reflect the true content type ID. - //This will occur when inserting new content. - if (_contentTypeId == 0 && ContentType != null) - { - _contentTypeId = ContentType.Id; - } - return _contentTypeId; + _contentTypeId = ContentType.Id; } - private set => SetPropertyValueAndDetectChanges(value, ref _contentTypeId, nameof(ContentTypeId)); + + return _contentTypeId; } + private set => SetPropertyValueAndDetectChanges(value, ref _contentTypeId, nameof(ContentTypeId)); + } - /// - /// Gets or sets the collection of properties for the entity. - /// - /// - /// Marked DoNotClone since we'll manually clone the underlying field to deal with the event handling - /// - [DataMember] - [DoNotClone] - public IPropertyCollection Properties + /// + /// Gets or sets the collection of properties for the entity. + /// + /// + /// Marked DoNotClone since we'll manually clone the underlying field to deal with the event handling + /// + [DataMember] + [DoNotClone] + public IPropertyCollection Properties + { + get => _properties; + set { - get => _properties; - set + if (_properties != null) { - if (_properties != null) - { - _properties.ClearCollectionChangedEvents(); - } + _properties.ClearCollectionChangedEvents(); + } - _properties = value; - _properties.CollectionChanged += PropertiesChanged; + _properties = value; + _properties.CollectionChanged += PropertiesChanged; + } + } + + public void ChangeContentType(ISimpleContentType contentType) + { + ContentType = contentType; + ContentTypeId = contentType.Id; + } + + protected void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) => + OnPropertyChanged(nameof(Properties)); + + /// + /// + /// Overridden to deal with specific object instances + /// + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedContent = (ContentBase)clone; + + // Need to manually clone this since it's not settable + clonedContent.ContentType = ContentType; + + // If culture infos exist then deal with event bindings + if (clonedContent._cultureInfos != null) + { + clonedContent._cultureInfos.ClearCollectionChangedEvents(); // clear this event handler if any + clonedContent._cultureInfos = + (ContentCultureInfosCollection?)_cultureInfos?.DeepClone(); // manually deep clone + if (clonedContent._cultureInfos is not null) + { + clonedContent._cultureInfos.CollectionChanged += + clonedContent.CultureInfosCollectionChanged; // re-assign correct event handler } } - #region Cultures - - // notes - common rules - // - setting a variant value on an invariant content type throws - // - getting a variant value on an invariant content type returns null - // - setting and getting the invariant value is always possible - // - setting a null value clears the value - - /// - public IEnumerable AvailableCultures - => _cultureInfos?.Keys ?? Enumerable.Empty(); - - /// - public bool IsCultureAvailable(string culture) - => _cultureInfos != null && _cultureInfos.ContainsKey(culture); - - /// - [DataMember] - public ContentCultureInfosCollection? CultureInfos + // if properties exist then deal with event bindings + if (clonedContent._properties != null) { - get + clonedContent._properties.ClearCollectionChangedEvents(); // clear this event handler if any + clonedContent._properties = (IPropertyCollection)_properties.DeepClone(); // manually deep clone + clonedContent._properties.CollectionChanged += + clonedContent.PropertiesChanged; // re-assign correct event handler + } + + clonedContent._currentCultureChanges.updatedCultures = null; + clonedContent._currentCultureChanges.addedCultures = null; + clonedContent._currentCultureChanges.removedCultures = null; + + clonedContent._previousCultureChanges.updatedCultures = null; + clonedContent._previousCultureChanges.addedCultures = null; + clonedContent._previousCultureChanges.removedCultures = null; + } + + #region Used for change tracking + + private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) + _currentCultureChanges; + + private (HashSet? addedCultures, HashSet? removedCultures, HashSet? updatedCultures) + _previousCultureChanges; + + public static class ChangeTrackingPrefix + { + public const string UpdatedCulture = "_updatedCulture_"; + public const string ChangedCulture = "_changedCulture_"; + public const string PublishedCulture = "_publishedCulture_"; + public const string UnpublishedCulture = "_unpublishedCulture_"; + public const string AddedCulture = "_addedCulture_"; + public const string RemovedCulture = "_removedCulture_"; + } + + #endregion + + #region Cultures + + // notes - common rules + // - setting a variant value on an invariant content type throws + // - getting a variant value on an invariant content type returns null + // - setting and getting the invariant value is always possible + // - setting a null value clears the value + + /// + public IEnumerable AvailableCultures + => _cultureInfos?.Keys ?? Enumerable.Empty(); + + /// + public bool IsCultureAvailable(string culture) + => _cultureInfos != null && _cultureInfos.ContainsKey(culture); + + /// + [DataMember] + public ContentCultureInfosCollection? CultureInfos + { + get + { + if (_cultureInfos != null) { - if (_cultureInfos != null) return _cultureInfos; - _cultureInfos = new ContentCultureInfosCollection(); - _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; return _cultureInfos; } - set + + _cultureInfos = new ContentCultureInfosCollection(); + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; + return _cultureInfos; + } + + set + { + if (_cultureInfos != null) { - if (_cultureInfos != null) - { - _cultureInfos.ClearCollectionChangedEvents(); - } - _cultureInfos = value; - if (_cultureInfos != null) - { - _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; - } + _cultureInfos.ClearCollectionChangedEvents(); + } + + _cultureInfos = value; + if (_cultureInfos != null) + { + _cultureInfos.CollectionChanged += CultureInfosCollectionChanged; } } + } - /// - public string? GetCultureName(string? culture) + /// + public string? GetCultureName(string? culture) + { + if (culture.IsNullOrWhiteSpace()) { - if (culture.IsNullOrWhiteSpace()) return Name; - if (!ContentType.VariesByCulture()) return null; - if (_cultureInfos == null) return null; - return _cultureInfos.TryGetValue(culture!, out var infos) ? infos.Name : null; + return Name; } - /// - public DateTime? GetUpdateDate(string culture) + if (!ContentType.VariesByCulture()) { - if (culture.IsNullOrWhiteSpace()) return null; - if (!ContentType.VariesByCulture()) return null; - if (_cultureInfos == null) return null; - return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?)null; + return null; } - /// - public void SetCultureName(string? name, string? culture) + if (_cultureInfos == null) { - if (ContentType.VariesByCulture()) // set on variant content type - { - if (culture.IsNullOrWhiteSpace()) // invariant is ok - { - Name = name; // may be null - } - else if (name.IsNullOrWhiteSpace()) // clear - { - ClearCultureInfo(culture!); - } - else // set - { - this.SetCultureInfo(culture!, name, DateTime.Now); - } - } - else // set on invariant content type - { - if (!culture.IsNullOrWhiteSpace()) // invariant is NOT ok - throw new NotSupportedException("Content type does not vary by culture."); + return null; + } + return _cultureInfos.TryGetValue(culture!, out ContentCultureInfos infos) ? infos.Name : null; + } + + /// + public DateTime? GetUpdateDate(string culture) + { + if (culture.IsNullOrWhiteSpace()) + { + return null; + } + + if (!ContentType.VariesByCulture()) + { + return null; + } + + if (_cultureInfos == null) + { + return null; + } + + return _cultureInfos.TryGetValue(culture, out ContentCultureInfos infos) ? infos.Date : null; + } + + /// + public void SetCultureName(string? name, string? culture) + { + // set on variant content type + if (ContentType.VariesByCulture()) + { + // invariant is ok + if (culture.IsNullOrWhiteSpace()) + { Name = name; // may be null } - } - private void ClearCultureInfo(string culture) - { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - if (_cultureInfos == null) return; - _cultureInfos.Remove(culture); - if (_cultureInfos.Count == 0) - _cultureInfos = null; - } - - /// - /// Handles culture infos collection changes. - /// - private void CultureInfosCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(CultureInfos)); - - switch (e.Action) + // clear + else if (name.IsNullOrWhiteSpace()) { - case NotifyCollectionChangedAction.Add: - { - var cultureInfo = e.NewItems?.Cast().First(); - if (_currentCultureChanges.addedCultures == null) _currentCultureChanges.addedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (_currentCultureChanges.updatedCultures == null) _currentCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentCultureChanges.addedCultures.Add(cultureInfo.Culture); - _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); - _currentCultureChanges.removedCultures?.Remove(cultureInfo.Culture); - } + ClearCultureInfo(culture!); + } - break; - } - case NotifyCollectionChangedAction.Remove: - { - //remove listening for changes - var cultureInfo = e.OldItems?.Cast().First(); - if (_currentCultureChanges.removedCultures == null) _currentCultureChanges.removedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentCultureChanges.removedCultures.Add(cultureInfo.Culture); - _currentCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); - _currentCultureChanges.addedCultures?.Remove(cultureInfo.Culture); - } - - break; - } - case NotifyCollectionChangedAction.Replace: - { - //replace occurs when an Update occurs - var cultureInfo = e.NewItems?.Cast().First(); - if (_currentCultureChanges.updatedCultures == null) _currentCultureChanges.updatedCultures = new HashSet(StringComparer.InvariantCultureIgnoreCase); - if (cultureInfo is not null) - { - _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); - } - - break; - } + // set + else + { + this.SetCultureInfo(culture!, name, DateTime.Now); } } - #endregion - - #region Has, Get, Set, Publish Property Value - - /// - public bool HasProperty(string propertyTypeAlias) - => Properties.Contains(propertyTypeAlias); - - /// - public object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) + // set on invariant content type + else { - return Properties.TryGetValue(propertyTypeAlias, out var property) - ? property?.GetValue(culture, segment, published) - : null; + // invariant is NOT ok + if (!culture.IsNullOrWhiteSpace()) + { + throw new NotSupportedException("Content type does not vary by culture."); + } + + Name = name; // may be null + } + } + + private void ClearCultureInfo(string culture) + { + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); } - /// - public TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) + if (string.IsNullOrWhiteSpace(culture)) { - if (!Properties.TryGetValue(propertyTypeAlias, out var property)) - return default; - - var convertAttempt = property?.GetValue(culture, segment, published).TryConvertTo(); - return convertAttempt?.Success is not null && (convertAttempt?.Success ?? false) ? convertAttempt.Value.Result : default; + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); } - /// - public void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null) + if (_cultureInfos == null) { - if (!Properties.TryGetValue(propertyTypeAlias, out var property)) - throw new InvalidOperationException($"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); - - property?.SetValue(value, culture, segment); - - //bump the culture to be flagged for updating - this.TouchCulture(culture); + return; } - #endregion - - #region Dirty - - public override void ResetWereDirtyProperties() + _cultureInfos.Remove(culture); + if (_cultureInfos.Count == 0) + { + _cultureInfos = null; + } + } + + /// + /// Handles culture infos collection changes. + /// + private void CultureInfosCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(CultureInfos)); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentCultureChanges.addedCultures == null) + { + _currentCultureChanges.addedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (_currentCultureChanges.updatedCultures == null) + { + _currentCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentCultureChanges.addedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.removedCultures?.Remove(cultureInfo.Culture); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + // Remove listening for changes + ContentCultureInfos? cultureInfo = e.OldItems?.Cast().First(); + if (_currentCultureChanges.removedCultures == null) + { + _currentCultureChanges.removedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentCultureChanges.removedCultures.Add(cultureInfo.Culture); + _currentCultureChanges.updatedCultures?.Remove(cultureInfo.Culture); + _currentCultureChanges.addedCultures?.Remove(cultureInfo.Culture); + } + + break; + } + + case NotifyCollectionChangedAction.Replace: + { + // Replace occurs when an Update occurs + ContentCultureInfos? cultureInfo = e.NewItems?.Cast().First(); + if (_currentCultureChanges.updatedCultures == null) + { + _currentCultureChanges.updatedCultures = + new HashSet(StringComparer.InvariantCultureIgnoreCase); + } + + if (cultureInfo is not null) + { + _currentCultureChanges.updatedCultures.Add(cultureInfo.Culture); + } + + break; + } + } + } + + #endregion + + #region Has, Get, Set, Publish Property Value + + /// + public bool HasProperty(string propertyTypeAlias) + => Properties.Contains(propertyTypeAlias); + + /// + public object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) => + Properties.TryGetValue(propertyTypeAlias, out IProperty? property) + ? property?.GetValue(culture, segment, published) + : null; + + /// + public TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false) + { + if (!Properties.TryGetValue(propertyTypeAlias, out IProperty? property)) + { + return default; + } + + Attempt? convertAttempt = property?.GetValue(culture, segment, published).TryConvertTo(); + return convertAttempt?.Success is not null && (convertAttempt?.Success ?? false) + ? convertAttempt.Value.Result + : default; + } + + /// + public void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null) + { + if (!Properties.TryGetValue(propertyTypeAlias, out IProperty? property)) + { + throw new InvalidOperationException( + $"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); + } + + property?.SetValue(value, culture, segment); + + // bump the culture to be flagged for updating + this.TouchCulture(culture); + } + + #endregion + + #region Dirty + + public override void ResetWereDirtyProperties() + { + base.ResetWereDirtyProperties(); + _previousCultureChanges.addedCultures = null; + _previousCultureChanges.removedCultures = null; + _previousCultureChanges.updatedCultures = null; + } + + /// + /// Overridden to include user properties. + public override void ResetDirtyProperties(bool rememberDirty) + { + base.ResetDirtyProperties(rememberDirty); + + if (rememberDirty) + { + _previousCultureChanges.addedCultures = + _currentCultureChanges.addedCultures == null || _currentCultureChanges.addedCultures.Count == 0 + ? null + : new HashSet( + _currentCultureChanges.addedCultures, + StringComparer.InvariantCultureIgnoreCase); + _previousCultureChanges.removedCultures = + _currentCultureChanges.removedCultures == null || _currentCultureChanges.removedCultures.Count == 0 + ? null + : new HashSet( + _currentCultureChanges.removedCultures, + StringComparer.InvariantCultureIgnoreCase); + _previousCultureChanges.updatedCultures = + _currentCultureChanges.updatedCultures == null || _currentCultureChanges.updatedCultures.Count == 0 + ? null + : new HashSet( + _currentCultureChanges.updatedCultures, + StringComparer.InvariantCultureIgnoreCase); + } + else { - base.ResetWereDirtyProperties(); _previousCultureChanges.addedCultures = null; _previousCultureChanges.removedCultures = null; _previousCultureChanges.updatedCultures = null; } - /// - /// Overridden to include user properties. - public override void ResetDirtyProperties(bool rememberDirty) + _currentCultureChanges.addedCultures?.Clear(); + _currentCultureChanges.removedCultures?.Clear(); + _currentCultureChanges.updatedCultures?.Clear(); + + // also reset dirty changes made to user's properties + foreach (IProperty prop in Properties) { - base.ResetDirtyProperties(rememberDirty); - - if (rememberDirty) - { - _previousCultureChanges.addedCultures = _currentCultureChanges.addedCultures == null || _currentCultureChanges.addedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase); - _previousCultureChanges.removedCultures = _currentCultureChanges.removedCultures == null || _currentCultureChanges.removedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase); - _previousCultureChanges.updatedCultures = _currentCultureChanges.updatedCultures == null || _currentCultureChanges.updatedCultures.Count == 0 ? null : new HashSet(_currentCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase); - } - else - { - _previousCultureChanges.addedCultures = null; - _previousCultureChanges.removedCultures = null; - _previousCultureChanges.updatedCultures = null; - } - _currentCultureChanges.addedCultures?.Clear(); - _currentCultureChanges.removedCultures?.Clear(); - _currentCultureChanges.updatedCultures?.Clear(); - - // also reset dirty changes made to user's properties - foreach (var prop in Properties) - prop.ResetDirtyProperties(rememberDirty); - - // take care of culture infos - if (_cultureInfos == null) return; - - foreach (var cultureInfo in _cultureInfos) - cultureInfo.ResetDirtyProperties(rememberDirty); + prop.ResetDirtyProperties(rememberDirty); } - /// - /// Overridden to include user properties. - public override bool IsDirty() + // take care of culture infos + if (_cultureInfos == null) { - return IsEntityDirty() || this.IsAnyUserPropertyDirty(); + return; } - /// - /// Overridden to include user properties. - public override bool WasDirty() + foreach (ContentCultureInfos cultureInfo in _cultureInfos) { - return WasEntityDirty() || this.WasAnyUserPropertyDirty(); - } - - /// - /// Gets a value indicating whether the current entity's own properties (not user) are dirty. - /// - public bool IsEntityDirty() - { - return base.IsDirty(); - } - - /// - /// Gets a value indicating whether the current entity's own properties (not user) were dirty. - /// - public bool WasEntityDirty() - { - return base.WasDirty(); - } - - /// - /// Overridden to include user properties. - public override bool IsPropertyDirty(string propertyName) - { - if (base.IsPropertyDirty(propertyName)) - return true; - - //Special check here since we want to check if the request is for changed cultures - if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); - return _currentCultureChanges.addedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); - return _currentCultureChanges.removedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); - return _currentCultureChanges.updatedCultures?.Contains(culture) ?? false; - } - - return Properties.Contains(propertyName) && (Properties[propertyName]?.IsDirty() ?? false); - } - - /// - /// Overridden to include user properties. - public override bool WasPropertyDirty(string propertyName) - { - if (base.WasPropertyDirty(propertyName)) - return true; - - //Special check here since we want to check if the request is for changed cultures - if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); - return _previousCultureChanges.addedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); - return _previousCultureChanges.removedCultures?.Contains(culture) ?? false; - } - if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) - { - var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); - return _previousCultureChanges.updatedCultures?.Contains(culture) ?? false; - } - - return Properties.Contains(propertyName) && (Properties[propertyName]?.WasDirty() ?? false); - } - - /// - /// Overridden to include user properties. - public override IEnumerable GetDirtyProperties() - { - var instanceProperties = base.GetDirtyProperties(); - var propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias); - return instanceProperties.Concat(propertyTypes); - } - - /// - /// Overridden to include user properties. - public override IEnumerable GetWereDirtyProperties() - { - var instanceProperties = base.GetWereDirtyProperties(); - var propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); - return instanceProperties.Concat(propertyTypes); - } - - #endregion - - /// - /// - /// Overridden to deal with specific object instances - /// - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedContent = (ContentBase)clone; - - //need to manually clone this since it's not settable - clonedContent.ContentType = ContentType; - - //if culture infos exist then deal with event bindings - if (clonedContent._cultureInfos != null) - { - clonedContent._cultureInfos.ClearCollectionChangedEvents(); //clear this event handler if any - clonedContent._cultureInfos = (ContentCultureInfosCollection?)_cultureInfos?.DeepClone(); //manually deep clone - if (clonedContent._cultureInfos is not null) - { - clonedContent._cultureInfos.CollectionChanged += - clonedContent.CultureInfosCollectionChanged; //re-assign correct event handler - } - } - - //if properties exist then deal with event bindings - if (clonedContent._properties != null) - { - clonedContent._properties.ClearCollectionChangedEvents(); //clear this event handler if any - clonedContent._properties = (IPropertyCollection)_properties.DeepClone(); //manually deep clone - clonedContent._properties.CollectionChanged += clonedContent.PropertiesChanged; //re-assign correct event handler - } - - clonedContent._currentCultureChanges.updatedCultures = null; - clonedContent._currentCultureChanges.addedCultures = null; - clonedContent._currentCultureChanges.removedCultures = null; - - clonedContent._previousCultureChanges.updatedCultures = null; - clonedContent._previousCultureChanges.addedCultures = null; - clonedContent._previousCultureChanges.removedCultures = null; + cultureInfo.ResetDirtyProperties(rememberDirty); } } + + /// + /// Overridden to include user properties. + public override bool IsDirty() => IsEntityDirty() || this.IsAnyUserPropertyDirty(); + + /// + /// Overridden to include user properties. + public override bool WasDirty() => WasEntityDirty() || this.WasAnyUserPropertyDirty(); + + /// + /// Gets a value indicating whether the current entity's own properties (not user) are dirty. + /// + public bool IsEntityDirty() => base.IsDirty(); + + /// + /// Gets a value indicating whether the current entity's own properties (not user) were dirty. + /// + public bool WasEntityDirty() => base.WasDirty(); + + /// + /// Overridden to include user properties. + public override bool IsPropertyDirty(string propertyName) + { + if (base.IsPropertyDirty(propertyName)) + { + return true; + } + + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); + return _currentCultureChanges.addedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); + return _currentCultureChanges.removedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); + return _currentCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return Properties.Contains(propertyName) && (Properties[propertyName]?.IsDirty() ?? false); + } + + /// + /// Overridden to include user properties. + public override bool WasPropertyDirty(string propertyName) + { + if (base.WasPropertyDirty(propertyName)) + { + return true; + } + + // Special check here since we want to check if the request is for changed cultures + if (propertyName.StartsWith(ChangeTrackingPrefix.AddedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.AddedCulture); + return _previousCultureChanges.addedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.RemovedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.RemovedCulture); + return _previousCultureChanges.removedCultures?.Contains(culture) ?? false; + } + + if (propertyName.StartsWith(ChangeTrackingPrefix.UpdatedCulture)) + { + var culture = propertyName.TrimStart(ChangeTrackingPrefix.UpdatedCulture); + return _previousCultureChanges.updatedCultures?.Contains(culture) ?? false; + } + + return Properties.Contains(propertyName) && (Properties[propertyName]?.WasDirty() ?? false); + } + + /// + /// Overridden to include user properties. + public override IEnumerable GetDirtyProperties() + { + IEnumerable instanceProperties = base.GetDirtyProperties(); + IEnumerable propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias); + return instanceProperties.Concat(propertyTypes); + } + + /// + /// Overridden to include user properties. + public override IEnumerable GetWereDirtyProperties() + { + IEnumerable instanceProperties = base.GetWereDirtyProperties(); + IEnumerable propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); + return instanceProperties.Concat(propertyTypes); + } + + #endregion } diff --git a/src/Umbraco.Core/Models/ContentBaseExtensions.cs b/src/Umbraco.Core/Models/ContentBaseExtensions.cs index b81cf398bf..656db0f82f 100644 --- a/src/Umbraco.Core/Models/ContentBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentBaseExtensions.cs @@ -1,43 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to IContentBase to get URL segments. +/// +public static class ContentBaseExtensions { + private static DefaultUrlSegmentProvider? _defaultUrlSegmentProvider; + /// - /// Provides extension methods to IContentBase to get URL segments. + /// Gets the URL segment for a specified content and culture. /// - public static class ContentBaseExtensions + /// The content. + /// + /// + /// The culture. + /// The URL segment. + public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable urlSegmentProviders, string? culture = null) { - /// - /// Gets the URL segment for a specified content and culture. - /// - /// The content. - /// - /// - /// The culture. - /// The URL segment. - public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable urlSegmentProviders, string? culture = null) + if (content == null) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders)); - - var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null); - if (url == null) - { - if (s_defaultUrlSegmentProvider == null) - { - s_defaultUrlSegmentProvider = new DefaultUrlSegmentProvider(shortStringHelper); - } - - url = s_defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe - } - - return url; + throw new ArgumentNullException(nameof(content)); } - private static DefaultUrlSegmentProvider? s_defaultUrlSegmentProvider; + if (urlSegmentProviders == null) + { + throw new ArgumentNullException(nameof(urlSegmentProviders)); + } + + var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null); + if (url == null) + { + if (_defaultUrlSegmentProvider == null) + { + _defaultUrlSegmentProvider = new DefaultUrlSegmentProvider(shortStringHelper); + } + + url = _defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe + } + + return url; } } diff --git a/src/Umbraco.Core/Models/ContentCultureInfos.cs b/src/Umbraco.Core/Models/ContentCultureInfos.cs index 47f0765d63..8975c1fc58 100644 --- a/src/Umbraco.Core/Models/ContentCultureInfos.cs +++ b/src/Umbraco.Core/Models/ContentCultureInfos.cs @@ -1,109 +1,106 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The name of a content variant for a given culture +/// +public class ContentCultureInfos : BeingDirtyBase, IDeepCloneable, IEquatable { + private DateTime _date; + private string? _name; + /// - /// The name of a content variant for a given culture + /// Initializes a new instance of the class. /// - public class ContentCultureInfos : BeingDirtyBase, IDeepCloneable, IEquatable + public ContentCultureInfos(string culture) { - private DateTime _date; - private string? _name; - - /// - /// Initializes a new instance of the class. - /// - public ContentCultureInfos(string culture) + if (culture == null) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - Culture = culture; + throw new ArgumentNullException(nameof(culture)); } - /// - /// Initializes a new instance of the class. - /// - /// Used for cloning, without change tracking. - internal ContentCultureInfos(ContentCultureInfos other) - : this(other.Culture) + if (string.IsNullOrWhiteSpace(culture)) { - _name = other.Name; - _date = other.Date; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); } - /// - /// Gets the culture. - /// - public string Culture { get; } + Culture = culture; + } - /// - /// Gets the name. - /// - public string? Name + /// + /// Initializes a new instance of the class. + /// + /// Used for cloning, without change tracking. + internal ContentCultureInfos(ContentCultureInfos other) + : this(other.Culture) + { + _name = other.Name; + _date = other.Date; + } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the name. + /// + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets the date. + /// + public DateTime Date + { + get => _date; + set => SetPropertyValueAndDetectChanges(value, ref _date, nameof(Date)); + } + + /// + public object DeepClone() => new ContentCultureInfos(this); + + /// + public bool Equals(ContentCultureInfos? other) => other != null && Culture == other.Culture && Name == other.Name; + + /// + public override bool Equals(object? obj) => obj is ContentCultureInfos other && Equals(other); + + /// + public override int GetHashCode() + { + var hashCode = 479558943; + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Culture); + if (Name is not null) { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Name); } - /// - /// Gets the date. - /// - public DateTime Date - { - get => _date; - set => SetPropertyValueAndDetectChanges(value, ref _date, nameof(Date)); - } + return hashCode; + } - /// - public object DeepClone() - { - return new ContentCultureInfos(this); - } + /// + /// Deconstructs into culture and name. + /// + public void Deconstruct(out string culture, out string? name) + { + culture = Culture; + name = Name; + } - /// - public override bool Equals(object? obj) - { - return obj is ContentCultureInfos other && Equals(other); - } - - /// - public bool Equals(ContentCultureInfos? other) - { - return other != null && Culture == other.Culture && Name == other.Name; - } - - /// - public override int GetHashCode() - { - var hashCode = 479558943; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Culture); - if (Name is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Name); - } - - return hashCode; - } - - /// - /// Deconstructs into culture and name. - /// - public void Deconstruct(out string culture, out string? name) - { - culture = Culture; - name = Name; - } - - /// - /// Deconstructs into culture, name and date. - /// - public void Deconstruct(out string culture, out string? name, out DateTime date) - { - Deconstruct(out culture, out name); - date = Date; - } + /// + /// Deconstructs into culture, name and date. + /// + public void Deconstruct(out string culture, out string? name, out DateTime date) + { + Deconstruct(out culture, out name); + date = Date; } } diff --git a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs index 0d480ede6d..cf8a2f0328 100644 --- a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs +++ b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs @@ -1,60 +1,65 @@ -using System; using System.Collections.Specialized; using Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The culture names of a content's variants +/// +public class ContentCultureInfosCollection : ObservableDictionary, IDeepCloneable { /// - /// The culture names of a content's variants + /// Initializes a new instance of the class. /// - public class ContentCultureInfosCollection : ObservableDictionary, IDeepCloneable + public ContentCultureInfosCollection() + : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) { - /// - /// Initializes a new instance of the class. - /// - public ContentCultureInfosCollection() - : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) - { } + } - /// - /// Adds or updates a instance. - /// - public void AddOrUpdate(string culture, string? name, DateTime date) + /// + public object DeepClone() + { + var clone = new ContentCultureInfosCollection(); + + foreach (ContentCultureInfos item in this) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - culture = culture.ToLowerInvariant(); - - if (TryGetValue(culture, out var item)) - { - item.Name = name; - item.Date = date; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, item)); - } - else - { - Add(new ContentCultureInfos(culture) - { - Name = name, - Date = date - }); - } + var itemClone = (ContentCultureInfos)item.DeepClone(); + itemClone.ResetDirtyProperties(false); + clone.Add(itemClone); } - /// - public object DeepClone() + return clone; + } + + /// + /// Adds or updates a instance. + /// + public void AddOrUpdate(string culture, string? name, DateTime date) + { + if (culture == null) { - var clone = new ContentCultureInfosCollection(); + throw new ArgumentNullException(nameof(culture)); + } - foreach (var item in this) - { - var itemClone = (ContentCultureInfos) item.DeepClone(); - itemClone.ResetDirtyProperties(false); - clone.Add(itemClone); - } + if (string.IsNullOrWhiteSpace(culture)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); + } - return clone; + culture = culture.ToLowerInvariant(); + + if (TryGetValue(culture, out ContentCultureInfos item)) + { + item.Name = name; + item.Date = date; + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, item)); + } + else + { + Add(new ContentCultureInfos(culture) { Name = name, Date = date }); } } } diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs index 8a13a26e40..f4ad0b0dfc 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs @@ -1,48 +1,42 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class ContentDataIntegrityReport { - public class ContentDataIntegrityReport + public ContentDataIntegrityReport(IReadOnlyDictionary detectedIssues) => + DetectedIssues = detectedIssues; + + public enum IssueType { - public ContentDataIntegrityReport(IReadOnlyDictionary detectedIssues) - { - DetectedIssues = detectedIssues; - } + /// + /// The item's level and path are inconsistent with it's parent's path and level + /// + InvalidPathAndLevelByParentId, - public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed); + /// + /// The item's path doesn't contain all required parts + /// + InvalidPathEmpty, - public IReadOnlyDictionary DetectedIssues { get; } + /// + /// The item's path parts are inconsistent with it's level value + /// + InvalidPathLevelMismatch, - public IReadOnlyDictionary FixedIssues - => DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value); + /// + /// The item's path does not end with it's own ID + /// + InvalidPathById, - 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, - } + /// + /// The item's path does not have it's parent Id as the 2nd last entry + /// + InvalidPathByParentId, } + + public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed); + + public IReadOnlyDictionary DetectedIssues { get; } + + public IReadOnlyDictionary FixedIssues + => DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value); } diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs index e6138addbc..fe0ebce549 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs @@ -1,13 +1,10 @@ -namespace Umbraco.Cms.Core.Models -{ - public class ContentDataIntegrityReportEntry - { - public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType) - { - IssueType = issueType; - } +namespace Umbraco.Cms.Core.Models; - public ContentDataIntegrityReport.IssueType IssueType { get; } - public bool Fixed { get; set; } - } +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 index 52ea3d4032..40657069ff 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Models -{ - public class ContentDataIntegrityReportOptions - { - /// - /// Set to true to try to automatically resolve data integrity issues - /// - public bool FixIssues { get; set; } +namespace Umbraco.Cms.Core.Models; - // 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... - } +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/Models/ContentEditing/AssignedContentPermissions.cs b/src/Umbraco.Core/Models/ContentEditing/AssignedContentPermissions.cs index 2c73bcd590..18229d2124 100644 --- a/src/Umbraco.Core/Models/ContentEditing/AssignedContentPermissions.cs +++ b/src/Umbraco.Core/Models/ContentEditing/AssignedContentPermissions.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The permissions assigned to a content node +/// +/// +/// The underlying data such as Name, etc... is that of the Content item +/// +[DataContract(Name = "contentPermissions", Namespace = "")] +public class AssignedContentPermissions : EntityBasic { /// - /// The permissions assigned to a content node + /// The assigned permissions to the content item organized by permission group name /// - /// - /// The underlying data such as Name, etc... is that of the Content item - /// - [DataContract(Name = "contentPermissions", Namespace = "")] - public class AssignedContentPermissions : EntityBasic - { - /// - /// The assigned permissions to the content item organized by permission group name - /// - [DataMember(Name = "permissions")] - public IDictionary>? AssignedPermissions { get; set; } - } + [DataMember(Name = "permissions")] + public IDictionary>? AssignedPermissions { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/AssignedUserGroupPermissions.cs b/src/Umbraco.Core/Models/ContentEditing/AssignedUserGroupPermissions.cs index b6aca05515..867784d19d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/AssignedUserGroupPermissions.cs +++ b/src/Umbraco.Core/Models/ContentEditing/AssignedUserGroupPermissions.cs @@ -1,42 +1,40 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The user group permissions assigned to a content node +/// +/// +/// The underlying data such as Name, etc... is that of the User Group +/// +[DataContract(Name = "userGroupPermissions", Namespace = "")] +public class AssignedUserGroupPermissions : EntityBasic { /// - /// The user group permissions assigned to a content node + /// The assigned permissions for the user group organized by permission group name /// - /// - /// The underlying data such as Name, etc... is that of the User Group - /// - [DataContract(Name = "userGroupPermissions", Namespace = "")] - public class AssignedUserGroupPermissions : EntityBasic + [DataMember(Name = "permissions")] + public IDictionary>? AssignedPermissions { get; set; } + + /// + /// The default permissions for the user group organized by permission group name + /// + [DataMember(Name = "defaultPermissions")] + public IDictionary>? DefaultPermissions { get; set; } + + public static IDictionary> ClonePermissions( + IDictionary>? permissions) { - /// - /// The assigned permissions for the user group organized by permission group name - /// - [DataMember(Name = "permissions")] - public IDictionary>? AssignedPermissions { get; set; } - - /// - /// The default permissions for the user group organized by permission group name - /// - [DataMember(Name = "defaultPermissions")] - public IDictionary>? DefaultPermissions { get; set; } - - public static IDictionary> ClonePermissions(IDictionary>? permissions) + var result = new Dictionary>(); + if (permissions is not null) { - var result = new Dictionary>(); - if (permissions is not null) + foreach (KeyValuePair> permission in permissions) { - foreach (var permission in permissions) - { - result[permission.Key] = new List(permission.Value.Select(x => (Permission)x.Clone())); - } + result[permission.Key] = new List(permission.Value.Select(x => (Permission)x.Clone())); } - - return result; } + + return result; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/AuditLog.cs b/src/Umbraco.Core/Models/ContentEditing/AuditLog.cs index 5f40ace6ca..e7b744bd59 100644 --- a/src/Umbraco.Core/Models/ContentEditing/AuditLog.cs +++ b/src/Umbraco.Core/Models/ContentEditing/AuditLog.cs @@ -1,36 +1,34 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "auditLog", Namespace = "")] +public class AuditLog { - [DataContract(Name = "auditLog", Namespace = "")] - public class AuditLog - { - [DataMember(Name = "userId")] - public int UserId { get; set; } + [DataMember(Name = "userId")] + public int UserId { get; set; } - [DataMember(Name = "userName")] - public string? UserName { get; set; } + [DataMember(Name = "userName")] + public string? UserName { get; set; } - [DataMember(Name = "userAvatars")] - public string[]? UserAvatars { get; set; } + [DataMember(Name = "userAvatars")] + public string[]? UserAvatars { get; set; } - [DataMember(Name = "nodeId")] - public int NodeId { get; set; } + [DataMember(Name = "nodeId")] + public int NodeId { get; set; } - [DataMember(Name = "timestamp")] - public DateTime Timestamp { get; set; } + [DataMember(Name = "timestamp")] + public DateTime Timestamp { get; set; } - [DataMember(Name = "logType")] - public string? LogType { get; set; } + [DataMember(Name = "logType")] + public string? LogType { get; set; } - [DataMember(Name = "entityType")] - public string? EntityType { get; set; } + [DataMember(Name = "entityType")] + public string? EntityType { get; set; } - [DataMember(Name = "comment")] - public string? Comment { get; set; } + [DataMember(Name = "comment")] + public string? Comment { get; set; } - [DataMember(Name = "parameters")] - public string? Parameters { get; set; } - } + [DataMember(Name = "parameters")] + public string? Parameters { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/BackOfficeNotification.cs b/src/Umbraco.Core/Models/ContentEditing/BackOfficeNotification.cs index 982f46d912..1cf1e60e25 100644 --- a/src/Umbraco.Core/Models/ContentEditing/BackOfficeNotification.cs +++ b/src/Umbraco.Core/Models/ContentEditing/BackOfficeNotification.cs @@ -1,29 +1,27 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "notification", Namespace = "")] +public class BackOfficeNotification { - [DataContract(Name = "notification", Namespace = "")] - public class BackOfficeNotification + public BackOfficeNotification() { - public BackOfficeNotification() - { - - } - - public BackOfficeNotification(string header, string message, NotificationStyle notificationType) - { - Header = header; - Message = message; - NotificationType = notificationType; - } - - [DataMember(Name = "header")] - public string? Header { get; set; } - - [DataMember(Name = "message")] - public string? Message { get; set; } - - [DataMember(Name = "type")] - public NotificationStyle NotificationType { get; set; } } + + public BackOfficeNotification(string header, string message, NotificationStyle notificationType) + { + Header = header; + Message = message; + NotificationType = notificationType; + } + + [DataMember(Name = "header")] + public string? Header { get; set; } + + [DataMember(Name = "message")] + public string? Message { get; set; } + + [DataMember(Name = "type")] + public NotificationStyle NotificationType { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/CodeFileDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/CodeFileDisplay.cs index 37243c702c..b172fccb5a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/CodeFileDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/CodeFileDisplay.cs @@ -1,76 +1,70 @@ -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "scriptFile", Namespace = "")] +public class CodeFileDisplay : INotificationModel, IValidatableObject { - [DataContract(Name = "scriptFile", Namespace = "")] - public class CodeFileDisplay : INotificationModel, IValidatableObject + public CodeFileDisplay() => Notifications = new List(); + + /// + /// VirtualPath is the path to the file on disk + /// /views/partials/file.cshtml + /// + [DataMember(Name = "virtualPath", IsRequired = true)] + public string? VirtualPath { get; set; } + + /// + /// Path represents the path used by the backoffice tree + /// For files stored on disk, this is a URL encoded, comma separated + /// path to the file, always starting with -1. + /// -1,Partials,Parials%2FFolder,Partials%2FFolder%2FFile.cshtml + /// + [DataMember(Name = "path")] + [ReadOnly(true)] + public string? Path { get; set; } + + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } + + [DataMember(Name = "content", IsRequired = true)] + public string? Content { get; set; } + + [DataMember(Name = "fileType", IsRequired = true)] + public string? FileType { get; set; } + + [DataMember(Name = "snippet")] + [ReadOnly(true)] + public string? Snippet { get; set; } + + [DataMember(Name = "id")] + [ReadOnly(true)] + public string? Id { get; set; } + + public List Notifications { get; } + + /// + /// Some custom validation is required for valid file names + /// + /// + /// + public IEnumerable Validate(ValidationContext validationContext) { - public CodeFileDisplay() + var illegalChars = System.IO.Path.GetInvalidFileNameChars(); + if (Name?.ContainsAny(illegalChars) ?? false) { - Notifications = new List(); + yield return new ValidationResult( + "The file name cannot contain illegal characters", + new[] { "Name" }); } - - /// - /// VirtualPath is the path to the file on disk - /// /views/partials/file.cshtml - /// - [DataMember(Name = "virtualPath", IsRequired = true)] - public string? VirtualPath { get; set; } - - /// - /// Path represents the path used by the backoffice tree - /// For files stored on disk, this is a URL encoded, comma separated - /// path to the file, always starting with -1. - /// - /// -1,Partials,Parials%2FFolder,Partials%2FFolder%2FFile.cshtml - /// - [DataMember(Name = "path")] - [ReadOnly(true)] - public string? Path { get; set; } - - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } - - [DataMember(Name = "content", IsRequired = true)] - public string? Content { get; set; } - - [DataMember(Name = "fileType", IsRequired = true)] - public string? FileType { get; set; } - - [DataMember(Name = "snippet")] - [ReadOnly(true)] - public string? Snippet { get; set; } - - [DataMember(Name = "id")] - [ReadOnly(true)] - public string? Id { get; set; } - - public List Notifications { get; private set; } - - /// - /// Some custom validation is required for valid file names - /// - /// - /// - public IEnumerable Validate(ValidationContext validationContext) + else if (System.IO.Path.GetFileNameWithoutExtension(Name).IsNullOrWhiteSpace()) { - var illegalChars = System.IO.Path.GetInvalidFileNameChars(); - if (Name?.ContainsAny(illegalChars) ?? false) - { - yield return new ValidationResult( - "The file name cannot contain illegal characters", - new[] { "Name" }); - } - else if (System.IO.Path.GetFileNameWithoutExtension(Name).IsNullOrWhiteSpace()) - { - yield return new ValidationResult( - "The file name cannot be empty", - new[] { "Name" }); - } + yield return new ValidationResult( + "The file name cannot be empty", + new[] { "Name" }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs index 3dc88df3dd..02c32adc54 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs @@ -1,78 +1,78 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content app. +/// +/// +/// Content apps are editor extensions. +/// +[DataContract(Name = "app", Namespace = "")] +public class ContentApp { /// - /// Represents a content app. + /// Gets the name of the content app. + /// + [DataMember(Name = "name")] + public string? Name { get; set; } + + /// + /// Gets the unique alias of the content app. /// /// - /// Content apps are editor extensions. + /// Must be a valid javascript identifier, ie no spaces etc. /// - [DataContract(Name = "app", Namespace = "")] - public class ContentApp - { - /// - /// Gets the name of the content app. - /// - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - /// - /// Gets the unique alias of the content app. - /// - /// - /// Must be a valid javascript identifier, ie no spaces etc. - /// - [DataMember(Name = "alias")] - public string? Alias { get; set; } + /// + /// Gets or sets the weight of the content app. + /// + /// + /// Content apps are ordered by weight, from left (lowest values) to right (highest values). + /// Some built-in apps have special weights: listview is -666, content is -100 and infos is +100. + /// + /// The default weight is 0, meaning somewhere in-between content and infos, but weight could + /// be used for ordering between user-level apps, or anything really. + /// + /// + [DataMember(Name = "weight")] + public int Weight { get; set; } - /// - /// Gets or sets the weight of the content app. - /// - /// - /// Content apps are ordered by weight, from left (lowest values) to right (highest values). - /// Some built-in apps have special weights: listview is -666, content is -100 and infos is +100. - /// The default weight is 0, meaning somewhere in-between content and infos, but weight could - /// be used for ordering between user-level apps, or anything really. - /// - [DataMember(Name = "weight")] - public int Weight { get; set; } + /// + /// Gets the icon of the content app. + /// + /// + /// Must be a valid helveticons class name (see http://hlvticons.ch/). + /// + [DataMember(Name = "icon")] + public string? Icon { get; set; } - /// - /// Gets the icon of the content app. - /// - /// - /// Must be a valid helveticons class name (see http://hlvticons.ch/). - /// - [DataMember(Name = "icon")] - public string? Icon { get; set; } + /// + /// Gets the view for rendering the content app. + /// + [DataMember(Name = "view")] + public string? View { get; set; } - /// - /// Gets the view for rendering the content app. - /// - [DataMember(Name = "view")] - public string? View { get; set; } + /// + /// The view model specific to this app + /// + [DataMember(Name = "viewModel")] + public object? ViewModel { get; set; } - /// - /// The view model specific to this app - /// - [DataMember(Name = "viewModel")] - public object? ViewModel { get; set; } + /// + /// Gets a value indicating whether the app is active. + /// + /// + /// Normally reserved for Angular to deal with but in some cases this can be set on the server side. + /// + [DataMember(Name = "active")] + public bool Active { get; set; } - /// - /// Gets a value indicating whether the app is active. - /// - /// - /// Normally reserved for Angular to deal with but in some cases this can be set on the server side. - /// - [DataMember(Name = "active")] - public bool Active { get; set; } - - /// - /// Gets or sets the content app badge. - /// - [DataMember(Name = "badge")] - public ContentAppBadge? Badge { get; set; } - } + /// + /// Gets or sets the content app badge. + /// + [DataMember(Name = "badge")] + public ContentAppBadge? Badge { get; set; } } - diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs index 4e1089c97b..7a50073d17 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs @@ -1,37 +1,33 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content app badge +/// +[DataContract(Name = "badge", Namespace = "")] +public class ContentAppBadge { /// - /// Represents a content app badge + /// Initializes a new instance of the class. /// - [DataContract(Name = "badge", Namespace = "")] - public class ContentAppBadge - { - /// - /// Initializes a new instance of the class. - /// - public ContentAppBadge() - { - this.Type = ContentAppBadgeType.Default; - } + public ContentAppBadge() => Type = ContentAppBadgeType.Default; - /// - /// Gets or sets the number displayed in the badge - /// - [DataMember(Name = "count")] - public int Count { get; set; } + /// + /// Gets or sets the number displayed in the badge + /// + [DataMember(Name = "count")] + public int Count { get; set; } - /// - /// Gets or sets the type of badge to display - /// - /// - /// This controls the background color of the badge. - /// Warning will display a dark yellow badge - /// Alert will display a red badge - /// Default will display a turquoise badge - /// - [DataMember(Name = "type")] - public ContentAppBadgeType Type { get; set; } - } + /// + /// Gets or sets the type of badge to display + /// + /// + /// This controls the background color of the badge. + /// Warning will display a dark yellow badge + /// Alert will display a red badge + /// Default will display a turquoise badge + /// + [DataMember(Name = "type")] + public ContentAppBadgeType Type { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs index 9bcadd1383..718b36db33 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs @@ -1,25 +1,24 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +// TODO: This was marked with `[StringEnumConverter]` to inform the serializer +// to serialize the values to string instead of INT (which is the default) +// so we need to either invent our own attribute and make the implementation aware of it +// or ... something else? + +/// +/// Represent the content app badge types +/// +[DataContract(Name = "contentAppBadgeType")] +public enum ContentAppBadgeType { - // TODO: This was marked with `[StringEnumConverter]` to inform the serializer - // to serialize the values to string instead of INT (which is the default) - // so we need to either invent our own attribute and make the implementation aware of it - // or ... something else? + [EnumMember(Value = "default")] + Default = 0, - /// - /// Represent the content app badge types - /// - [DataContract(Name = "contentAppBadgeType")] - public enum ContentAppBadgeType - { - [EnumMember(Value = "default")] - Default = 0, + [EnumMember(Value = "warning")] + Warning = 1, - [EnumMember(Value = "warning")] - Warning = 1, - - [EnumMember(Value = "alert")] - Alert = 2 - } + [EnumMember(Value = "alert")] + Alert = 2, } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs index d7f026aeab..241cde46b4 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs @@ -1,62 +1,61 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a content item to be saved +/// +[DataContract(Name = "content", Namespace = "")] +public abstract class ContentBaseSave : ContentItemBasic, IContentSave + where TPersisted : IContentBase { - /// - /// A model representing a content item to be saved - /// - [DataContract(Name = "content", Namespace = "")] - public abstract class ContentBaseSave : ContentItemBasic, IContentSave - where TPersisted : IContentBase + protected ContentBaseSave() => UploadedFiles = new List(); + + #region IContentSave + + /// + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } + + [DataMember(Name = "properties")] + public override IEnumerable Properties { - protected ContentBaseSave() - { - UploadedFiles = new List(); - } - - #region IContentSave - /// - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } - - [DataMember(Name = "properties")] - public override IEnumerable Properties - { - get => base.Properties; - set => base.Properties = value; - } - - [IgnoreDataMember] - public List UploadedFiles { get; } - - //These need explicit implementation because we are using internal models - /// - [IgnoreDataMember] - TPersisted IContentSave.PersistedContent { get; set; } = default!; - - //Non explicit internal getter so we don't need to explicitly cast in our own code - [IgnoreDataMember] - public TPersisted PersistedContent - { - get => ((IContentSave)this).PersistedContent; - set => ((IContentSave) this).PersistedContent = value; - } - - /// - /// The property DTO object is used to gather all required property data including data type information etc... for use with validation - used during inbound model binding - /// - /// - /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need - /// instead of having to look up all the data individually. - /// This is not used for outgoing model information. - /// - [IgnoreDataMember] - public ContentPropertyCollectionDto? PropertyCollectionDto { get; set; } - - #endregion + get => base.Properties; + set => base.Properties = value; } + + [IgnoreDataMember] + public List UploadedFiles { get; } + + // These need explicit implementation because we are using internal models + + /// + [IgnoreDataMember] + TPersisted IContentSave.PersistedContent { get; set; } = default!; + + // Non explicit internal getter so we don't need to explicitly cast in our own code + [IgnoreDataMember] + public TPersisted PersistedContent + { + get => ((IContentSave)this).PersistedContent; + set => ((IContentSave)this).PersistedContent = value; + } + + /// + /// The property DTO object is used to gather all required property data including data type information etc... for use + /// with validation - used during inbound model binding + /// + /// + /// We basically use this object to hydrate all required data from the database into one object so we can validate + /// everything we need + /// instead of having to look up all the data individually. + /// This is not used for outgoing model information. + /// + [IgnoreDataMember] + public ContentPropertyCollectionDto? PropertyCollectionDto { get; set; } + + #endregion } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs b/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs index 86af3de89a..ca24b08567 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - [DataContract(Name = "ContentDomainsAndCulture")] - public class ContentDomainsAndCulture - { - [DataMember(Name = "domains")] - public IEnumerable? Domains { get; set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "language")] - public string? Language { get; set; } - } +[DataContract(Name = "ContentDomainsAndCulture")] +public class ContentDomainsAndCulture +{ + [DataMember(Name = "domains")] + public IEnumerable? Domains { get; set; } + + [DataMember(Name = "language")] + public string? Language { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemBasic.cs index 9b1fcdc129..fd277308f7 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemBasic.cs @@ -1,106 +1,105 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a basic content item +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentItemBasic : EntityBasic { + [DataMember(Name = "updateDate")] + public DateTime UpdateDate { get; set; } + + [DataMember(Name = "createDate")] + public DateTime CreateDate { get; set; } + /// - /// A model representing a basic content item + /// Boolean indicating if this item is published or not based on it's /// - [DataContract(Name = "content", Namespace = "")] - public class ContentItemBasic : EntityBasic + [DataMember(Name = "published")] + public bool Published => State == ContentSavedState.Published || State == ContentSavedState.PublishedPendingChanges; + + /// + /// Determines if the content item is a draft + /// + [DataMember(Name = "edited")] + public bool Edited { get; set; } + + [DataMember(Name = "owner")] + public UserProfile? Owner { get; set; } + + [DataMember(Name = "updater")] + public UserProfile? Updater { get; set; } + + public int ContentTypeId { get; set; } + + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + /// + /// The saved/published state of an item + /// + /// + /// This is nullable since it's only relevant for content (non-content like media + members will be null) + /// + [DataMember(Name = "state")] + public ContentSavedState? State { get; set; } + + [DataMember(Name = "variesByCulture")] + public bool VariesByCulture { get; set; } + + public override bool Equals(object? obj) { - [DataMember(Name = "updateDate")] - public DateTime UpdateDate { get; set; } - - [DataMember(Name = "createDate")] - public DateTime CreateDate { get; set; } - - /// - /// Boolean indicating if this item is published or not based on it's - /// - [DataMember(Name = "published")] - public bool Published => State == ContentSavedState.Published || State == ContentSavedState.PublishedPendingChanges; - - /// - /// Determines if the content item is a draft - /// - [DataMember(Name = "edited")] - public bool Edited { get; set; } - - [DataMember(Name = "owner")] - public UserProfile? Owner { get; set; } - - [DataMember(Name = "updater")] - public UserProfile? Updater { get; set; } - - public int ContentTypeId { get; set; } - - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; - - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - - /// - /// The saved/published state of an item - /// - /// - /// This is nullable since it's only relevant for content (non-content like media + members will be null) - /// - [DataMember(Name = "state")] - public ContentSavedState? State { get; set; } - - [DataMember(Name = "variesByCulture")] - public bool VariesByCulture { get; set; } - - protected bool Equals(ContentItemBasic other) + if (ReferenceEquals(null, obj)) { - return Id == other.Id; + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - var other = obj as ContentItemBasic; - return other != null && Equals(other); + return true; } - public override int GetHashCode() - { - if (Id is not null) - { - return Id.GetHashCode(); - } - - return base.GetHashCode(); - } + return obj is ContentItemBasic other && Equals(other); } - /// - /// A model representing a basic content item with properties - /// - [DataContract(Name = "content", Namespace = "")] - public class ContentItemBasic : ContentItemBasic, IContentProperties - where T : ContentPropertyBasic + protected bool Equals(ContentItemBasic other) => Id == other.Id; + + public override int GetHashCode() { - public ContentItemBasic() + if (Id is not null) { - //ensure its not null - _properties = Enumerable.Empty(); + return Id.GetHashCode(); } - private IEnumerable _properties; - - [DataMember(Name = "properties")] - public virtual IEnumerable Properties - { - get => _properties; - set => _properties = value; - } + return base.GetHashCode(); + } +} + +/// +/// A model representing a basic content item with properties +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentItemBasic : ContentItemBasic, IContentProperties + where T : ContentPropertyBasic +{ + private IEnumerable _properties; + + public ContentItemBasic() => + + // ensure its not null + _properties = Enumerable.Empty(); + + [DataMember(Name = "properties")] + public virtual IEnumerable Properties + { + get => _properties; + set => _properties = value; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs index 874f2f085a..e2fcf71053 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs @@ -1,219 +1,225 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - public class ContentItemDisplay : ContentItemDisplay { } +namespace Umbraco.Cms.Core.Models.ContentEditing; - public class ContentItemDisplayWithSchedule : ContentItemDisplay { } +public class ContentItemDisplay : ContentItemDisplay +{ +} + +public class ContentItemDisplayWithSchedule : ContentItemDisplay +{ +} + +/// +/// A model representing a content item to be displayed in the back office +/// +[DataContract(Name = "content", Namespace = "")] +public class + ContentItemDisplay : INotificationModel, + IErrorModel // ListViewAwareContentItemDisplayBase + where TVariant : ContentVariantDisplay +{ + public ContentItemDisplay() + { + AllowPreview = true; + Notifications = new List(); + Errors = new Dictionary(); + Variants = new List(); + ContentApps = new List(); + } + + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } + + [DataMember(Name = "udi")] + [ReadOnly(true)] + public Udi? Udi { get; set; } + + [DataMember(Name = "icon")] + public string? Icon { get; set; } + + [DataMember(Name = "trashed")] + [ReadOnly(true)] + public bool Trashed { get; set; } /// - /// A model representing a content item to be displayed in the back office + /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider /// - [DataContract(Name = "content", Namespace = "")] - public class ContentItemDisplay : INotificationModel, IErrorModel //ListViewAwareContentItemDisplayBase - where TVariant : ContentVariantDisplay - { - public ContentItemDisplay() - { - AllowPreview = true; - Notifications = new List(); - Errors = new Dictionary(); - Variants = new List(); - ContentApps = new List(); - } + [DataMember(Name = "key")] + public Guid? Key { get; set; } - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int? ParentId { get; set; } - [DataMember(Name = "udi")] - [ReadOnly(true)] - public Udi? Udi { get; set; } + /// + /// The path of the entity + /// + [DataMember(Name = "path")] + public string? Path { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } + /// + /// A collection of content variants + /// + /// + /// If a content item is invariant, this collection will only contain one item, else it will contain all culture + /// variants + /// + [DataMember(Name = "variants")] + public IEnumerable Variants { get; set; } - [DataMember(Name = "trashed")] - [ReadOnly(true)] - public bool Trashed { get; set; } + [DataMember(Name = "owner")] + public UserProfile? Owner { get; set; } - /// - /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider - /// - [DataMember(Name = "key")] - public Guid? Key { get; set; } + [DataMember(Name = "updater")] + public UserProfile? Updater { get; set; } - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int? ParentId { get; set; } + /// + /// The name of the content type + /// + [DataMember(Name = "contentTypeName")] + public string? ContentTypeName { get; set; } - /// - /// The path of the entity - /// - [DataMember(Name = "path")] - public string? Path { get; set; } + /// + /// Indicates if the content is configured as a list view container + /// + [DataMember(Name = "isContainer")] + public bool IsContainer { get; set; } - /// - /// A collection of content variants - /// - /// - /// If a content item is invariant, this collection will only contain one item, else it will contain all culture variants - /// - [DataMember(Name = "variants")] - public IEnumerable Variants { get; set; } + /// + /// Indicates if the content is configured as an element + /// + [DataMember(Name = "isElement")] + public bool IsElement { get; set; } - [DataMember(Name = "owner")] - public UserProfile? Owner { get; set; } + /// + /// Property indicating if this item is part of a list view parent + /// + [DataMember(Name = "isChildOfListView")] + public bool IsChildOfListView { get; set; } - [DataMember(Name = "updater")] - public UserProfile? Updater { get; set; } + /// + /// Property for the entity's individual tree node URL + /// + /// + /// This is required if the item is a child of a list view since the tree won't actually be loaded, + /// so the app will need to go fetch the individual tree node in order to be able to load it's action list (menu) + /// + [DataMember(Name = "treeNodeUrl")] + public string? TreeNodeUrl { get; set; } - /// - /// The name of the content type - /// - [DataMember(Name = "contentTypeName")] - public string? ContentTypeName { get; set; } + [DataMember(Name = "contentTypeId")] + public int? ContentTypeId { get; set; } - /// - /// Indicates if the content is configured as a list view container - /// - [DataMember(Name = "isContainer")] - public bool IsContainer { get; set; } + [DataMember(Name = "contentTypeKey")] + public Guid ContentTypeKey { get; set; } - /// - /// Indicates if the content is configured as an element - /// - [DataMember(Name = "isElement")] - public bool IsElement { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; - /// - /// Property indicating if this item is part of a list view parent - /// - [DataMember(Name = "isChildOfListView")] - public bool IsChildOfListView { get; set; } + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } - /// - /// Property for the entity's individual tree node URL - /// - /// - /// This is required if the item is a child of a list view since the tree won't actually be loaded, - /// so the app will need to go fetch the individual tree node in order to be able to load it's action list (menu) - /// - [DataMember(Name = "treeNodeUrl")] - public string? TreeNodeUrl { get; set; } + /// + /// This is the last updated date for the entire content object regardless of variants + /// + /// + /// Each variant has it's own update date assigned as well + /// + [DataMember(Name = "updateDate")] + public DateTime UpdateDate { get; set; } - [DataMember(Name = "contentTypeId")] - public int? ContentTypeId { get; set; } + [DataMember(Name = "template")] + public string? TemplateAlias { get; set; } - [DataMember(Name = "contentTypeKey")] - public Guid ContentTypeKey { get; set; } + [DataMember(Name = "templateId")] + public int TemplateId { get; set; } - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; + [DataMember(Name = "allowedTemplates")] + public IDictionary? AllowedTemplates { get; set; } - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } + [DataMember(Name = "documentType")] + public ContentTypeBasic? DocumentType { get; set; } - /// - /// This is the last updated date for the entire content object regardless of variants - /// - /// - /// Each variant has it's own update date assigned as well - /// - [DataMember(Name = "updateDate")] - public DateTime UpdateDate { get; set; } + [DataMember(Name = "urls")] + public UrlInfo[]? Urls { get; set; } - [DataMember(Name = "template")] - public string? TemplateAlias { get; set; } + /// + /// Determines whether previewing is allowed for this node + /// + /// + /// By default this is true but by using events developers can toggle this off for certain documents if there is + /// nothing to preview + /// + [DataMember(Name = "allowPreview")] + public bool AllowPreview { get; set; } - [DataMember(Name = "templateId")] - public int TemplateId { get; set; } + /// + /// The allowed 'actions' based on the user's permissions - Create, Update, Publish, Send to publish + /// + /// + /// Each char represents a button which we can then map on the front-end to the correct actions + /// + [DataMember(Name = "allowedActions")] + public IEnumerable? AllowedActions { get; set; } - [DataMember(Name = "allowedTemplates")] - public IDictionary? AllowedTemplates { get; set; } + [DataMember(Name = "isBlueprint")] + public bool IsBlueprint { get; set; } - [DataMember(Name = "documentType")] - public ContentTypeBasic? DocumentType { get; set; } + [DataMember(Name = "apps")] + public IEnumerable ContentApps { get; set; } - [DataMember(Name = "urls")] - public UrlInfo[]? Urls { get; set; } + /// + /// The real persisted content object - used during inbound model binding + /// + /// + /// This is not used for outgoing model information. + /// + [IgnoreDataMember] + public IContent? PersistedContent { get; set; } - /// - /// Determines whether previewing is allowed for this node - /// - /// - /// By default this is true but by using events developers can toggle this off for certain documents if there is nothing to preview - /// - [DataMember(Name = "allowPreview")] - public bool AllowPreview { get; set; } + /// + /// The DTO object used to gather all required content data including data type information etc... for use with + /// validation - used during inbound model binding + /// + /// + /// We basically use this object to hydrate all required data from the database into one object so we can validate + /// everything we need + /// instead of having to look up all the data individually. + /// This is not used for outgoing model information. + /// + [IgnoreDataMember] + public ContentPropertyCollectionDto? ContentDto { get; set; } - /// - /// The allowed 'actions' based on the user's permissions - Create, Update, Publish, Send to publish - /// - /// - /// Each char represents a button which we can then map on the front-end to the correct actions - /// - [DataMember(Name = "allowedActions")] - public IEnumerable? AllowedActions { get; set; } + /// + /// A collection of extra data that is available for this specific entity/entity type + /// + [DataMember(Name = "metaData")] + [ReadOnly(true)] + public IDictionary? AdditionalData { get; private set; } - [DataMember(Name = "isBlueprint")] - public bool IsBlueprint { get; set; } + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. + /// + [DataMember(Name = "ModelState")] + [ReadOnly(true)] + public IDictionary Errors { get; set; } - [DataMember(Name = "apps")] - public IEnumerable ContentApps { get; set; } - - /// - /// The real persisted content object - used during inbound model binding - /// - /// - /// This is not used for outgoing model information. - /// - [IgnoreDataMember] - public IContent? PersistedContent { get; set; } - - /// - /// The DTO object used to gather all required content data including data type information etc... for use with validation - used during inbound model binding - /// - /// - /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need - /// instead of having to look up all the data individually. - /// This is not used for outgoing model information. - /// - [IgnoreDataMember] - public ContentPropertyCollectionDto? ContentDto { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - [ReadOnly(true)] - public List Notifications { get; private set; } - - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. - /// - [DataMember(Name = "ModelState")] - [ReadOnly(true)] - public IDictionary Errors { get; set; } - - /// - /// A collection of extra data that is available for this specific entity/entity type - /// - [DataMember(Name = "metaData")] - [ReadOnly(true)] - public IDictionary? AdditionalData { get; private set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs index a06fa62b1a..1adf69371b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplayBase.cs @@ -1,49 +1,46 @@ -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public abstract class ContentItemDisplayBase : TabbedContentItem, INotificationModel, IErrorModel + where T : ContentPropertyBasic { - public abstract class ContentItemDisplayBase : TabbedContentItem, INotificationModel, IErrorModel - where T : ContentPropertyBasic + protected ContentItemDisplayBase() { - protected ContentItemDisplayBase() - { - Notifications = new List(); - Errors = new Dictionary(); - } - - /// - /// The name of the content type - /// - [DataMember(Name = "contentTypeName")] - public string? ContentTypeName { get; set; } - - /// - /// Indicates if the content is configured as a list view container - /// - [DataMember(Name = "isContainer")] - public bool IsContainer { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - [ReadOnly(true)] - public List Notifications { get; private set; } - - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. - /// - [DataMember(Name = "ModelState")] - [ReadOnly(true)] - public IDictionary Errors { get; set; } + Notifications = new List(); + Errors = new Dictionary(); } + + /// + /// The name of the content type + /// + [DataMember(Name = "contentTypeName")] + public string? ContentTypeName { get; set; } + + /// + /// Indicates if the content is configured as a list view container + /// + [DataMember(Name = "isContainer")] + public bool IsContainer { get; set; } + + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. + /// + [DataMember(Name = "ModelState")] + [ReadOnly(true)] + public IDictionary Errors { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs index fed33c52b0..400436421b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs @@ -1,66 +1,64 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a content item to be saved +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentItemSave : IContentSave { - /// - /// A model representing a content item to be saved - /// - [DataContract(Name = "content", Namespace = "")] - public class ContentItemSave : IContentSave + public ContentItemSave() { - public ContentItemSave() - { - UploadedFiles = new List(); - Variants = new List(); - } - - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } - - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } - - [DataMember(Name = "variants", IsRequired = true)] - public IEnumerable Variants { get; set; } - - [DataMember(Name = "contentTypeAlias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string ContentTypeAlias { get; set; } = null!; - - /// - /// The template alias to save - /// - [DataMember(Name = "templateAlias")] - public string? TemplateAlias { get; set; } - - #region IContentSave - - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } - - [IgnoreDataMember] - public List UploadedFiles { get; } - - //These need explicit implementation because we are using internal models - /// - [IgnoreDataMember] - IContent IContentSave.PersistedContent { get; set; } = null!; - - //Non explicit internal getter so we don't need to explicitly cast in our own code - [IgnoreDataMember] - public IContent PersistedContent - { - get => ((IContentSave)this).PersistedContent; - set => ((IContentSave)this).PersistedContent = value; - } - - #endregion - + UploadedFiles = new List(); + Variants = new List(); } + + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } + + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } + + [DataMember(Name = "variants", IsRequired = true)] + public IEnumerable Variants { get; set; } + + [DataMember(Name = "contentTypeAlias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string ContentTypeAlias { get; set; } = null!; + + /// + /// The template alias to save + /// + [DataMember(Name = "templateAlias")] + public string? TemplateAlias { get; set; } + + #region IContentSave + + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } + + [IgnoreDataMember] + public List UploadedFiles { get; } + + // These need explicit implementation because we are using internal models + + /// + [IgnoreDataMember] + IContent IContentSave.PersistedContent { get; set; } = null!; + + // Non explicit internal getter so we don't need to explicitly cast in our own code + [IgnoreDataMember] + public IContent PersistedContent + { + get => ((IContentSave)this).PersistedContent; + set => ((IContentSave)this).PersistedContent = value; + } + + #endregion } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs index ee5a4600d4..c4a3d7791b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs @@ -1,73 +1,71 @@ -using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content property to be saved +/// +[DataContract(Name = "property", Namespace = "")] +public class ContentPropertyBasic { /// - /// Represents a content property to be saved + /// This is the PropertyData ID /// - [DataContract(Name = "property", Namespace = "")] - public class ContentPropertyBasic - { - /// - /// This is the PropertyData ID - /// - /// - /// This is not really used for anything - /// - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } + /// + /// This is not really used for anything + /// + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } - [DataMember(Name = "dataTypeKey", IsRequired = false)] - [ReadOnly(true)] - public Guid DataTypeKey { get; set; } + [DataMember(Name = "dataTypeKey", IsRequired = false)] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } - [DataMember(Name = "value")] - public object? Value { get; set; } + [DataMember(Name = "value")] + public object? Value { get; set; } - [DataMember(Name = "alias", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string Alias { get; set; } = null!; + [DataMember(Name = "alias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string Alias { get; set; } = null!; - [DataMember(Name = "editor", IsRequired = false)] - public string? Editor { get; set; } + [DataMember(Name = "editor", IsRequired = false)] + public string? Editor { get; set; } - /// - /// Flags the property to denote that it can contain sensitive data - /// - [DataMember(Name = "isSensitive", IsRequired = false)] - public bool IsSensitive { get; set; } + /// + /// Flags the property to denote that it can contain sensitive data + /// + [DataMember(Name = "isSensitive", IsRequired = false)] + public bool IsSensitive { get; set; } - /// - /// The culture of the property - /// - /// - /// If this is a variant property then this culture value will be the same as it's variant culture but if this - /// is an invariant property then this will be a null value. - /// - [DataMember(Name = "culture")] - [ReadOnly(true)] - public string? Culture { get; set; } + /// + /// The culture of the property + /// + /// + /// If this is a variant property then this culture value will be the same as it's variant culture but if this + /// is an invariant property then this will be a null value. + /// + [DataMember(Name = "culture")] + [ReadOnly(true)] + public string? Culture { get; set; } - /// - /// The segment of the property - /// - /// - /// The segment value of a property can always be null but can only have a non-null value - /// when the property can be varied by segment. - /// - [DataMember(Name = "segment")] - [ReadOnly(true)] - public string? Segment { get; set; } + /// + /// The segment of the property + /// + /// + /// The segment value of a property can always be null but can only have a non-null value + /// when the property can be varied by segment. + /// + [DataMember(Name = "segment")] + [ReadOnly(true)] + public string? Segment { get; set; } - /// - /// Used internally during model mapping - /// - [IgnoreDataMember] - public IDataEditor? PropertyEditor { get; set; } - } + /// + /// Used internally during model mapping + /// + [IgnoreDataMember] + public IDataEditor? PropertyEditor { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyCollectionDto.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyCollectionDto.cs index 3c772c0866..35423f19a8 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyCollectionDto.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyCollectionDto.cs @@ -1,21 +1,14 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.ContentEditing +/// +/// Used to map property values when saving content/media/members +/// +/// +/// This is only used during mapping operations, it is not used for angular purposes +/// +public class ContentPropertyCollectionDto : IContentProperties { - /// - /// Used to map property values when saving content/media/members - /// - /// - /// This is only used during mapping operations, it is not used for angular purposes - /// - public class ContentPropertyCollectionDto : IContentProperties - { - public ContentPropertyCollectionDto() - { - Properties = Enumerable.Empty(); - } + public ContentPropertyCollectionDto() => Properties = Enumerable.Empty(); - public IEnumerable Properties { get; set; } - } + public IEnumerable Properties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs index b31caaa901..ca8c2f1fc2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs @@ -1,45 +1,43 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content property that is displayed in the UI +/// +[DataContract(Name = "property", Namespace = "")] +public class ContentPropertyDisplay : ContentPropertyBasic { - /// - /// Represents a content property that is displayed in the UI - /// - [DataContract(Name = "property", Namespace = "")] - public class ContentPropertyDisplay : ContentPropertyBasic + public ContentPropertyDisplay() { - public ContentPropertyDisplay() - { - Config = new Dictionary(); - Validation = new PropertyTypeValidation(); - } - - [DataMember(Name = "label", IsRequired = true)] - [Required] - public string? Label { get; set; } - - [DataMember(Name = "description")] - public string? Description { get; set; } - - [DataMember(Name = "view", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string? View { get; set; } - - [DataMember(Name = "config")] - public IDictionary? Config { get; set; } - - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } - - [DataMember(Name = "labelOnTop")] - public bool? LabelOnTop { get; set; } - - [DataMember(Name = "validation")] - public PropertyTypeValidation Validation { get; set; } - - [DataMember(Name = "readonly")] - public bool Readonly { get; set; } + Config = new Dictionary(); + Validation = new PropertyTypeValidation(); } + + [DataMember(Name = "label", IsRequired = true)] + [Required] + public string? Label { get; set; } + + [DataMember(Name = "description")] + public string? Description { get; set; } + + [DataMember(Name = "view", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string? View { get; set; } + + [DataMember(Name = "config")] + public IDictionary? Config { get; set; } + + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } + + [DataMember(Name = "labelOnTop")] + public bool? LabelOnTop { get; set; } + + [DataMember(Name = "validation")] + public PropertyTypeValidation Validation { get; set; } + + [DataMember(Name = "readonly")] + public bool Readonly { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs index d40a25805e..b0045bb038 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDto.cs @@ -1,24 +1,23 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content property from the database +/// +public class ContentPropertyDto : ContentPropertyBasic { - /// - /// Represents a content property from the database - /// - public class ContentPropertyDto : ContentPropertyBasic - { - public IDataType? DataType { get; set; } + public IDataType? DataType { get; set; } - public string? Label { get; set; } + public string? Label { get; set; } - public string? Description { get; set; } + public string? Description { get; set; } - public bool? IsRequired { get; set; } + public bool? IsRequired { get; set; } - public bool? LabelOnTop { get; set; } + public bool? LabelOnTop { get; set; } - public string? IsRequiredMessage { get; set; } + public string? IsRequiredMessage { get; set; } - public string? ValidationRegExp { get; set; } + public string? ValidationRegExp { get; set; } - public string? ValidationRegExpMessage { get; set; } - } + public string? ValidationRegExpMessage { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentRedirectUrl.cs b/src/Umbraco.Core/Models/ContentEditing/ContentRedirectUrl.cs index 99ea69b364..62bf29ce82 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentRedirectUrl.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentRedirectUrl.cs @@ -1,27 +1,25 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentRedirectUrl", Namespace = "")] +public class ContentRedirectUrl { - [DataContract(Name = "contentRedirectUrl", Namespace = "")] - public class ContentRedirectUrl - { - [DataMember(Name = "redirectId")] - public Guid RedirectId { get; set; } + [DataMember(Name = "redirectId")] + public Guid RedirectId { get; set; } - [DataMember(Name = "originalUrl")] - public string? OriginalUrl { get; set; } + [DataMember(Name = "originalUrl")] + public string? OriginalUrl { get; set; } - [DataMember(Name = "destinationUrl")] - public string? DestinationUrl { get; set; } + [DataMember(Name = "destinationUrl")] + public string? DestinationUrl { get; set; } - [DataMember(Name = "createDateUtc")] - public DateTime CreateDateUtc { get; set; } + [DataMember(Name = "createDateUtc")] + public DateTime CreateDateUtc { get; set; } - [DataMember(Name = "contentId")] - public int ContentId { get; set; } + [DataMember(Name = "contentId")] + public int ContentId { get; set; } - [DataMember(Name = "culture")] - public string? Culture { get; set; } - } + [DataMember(Name = "culture")] + public string? Culture { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs b/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs index 3beb970564..889b03db6d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs @@ -1,68 +1,69 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The action associated with saving a content item +/// +public enum ContentSaveAction { /// - /// The action associated with saving a content item + /// Saves the content item, no publish /// - public enum ContentSaveAction - { - /// - /// Saves the content item, no publish - /// - Save = 0, + Save = 0, - /// - /// Creates a new content item - /// - SaveNew = 1, + /// + /// Creates a new content item + /// + SaveNew = 1, - /// - /// Saves and publishes the content item - /// - Publish = 2, + /// + /// Saves and publishes the content item + /// + Publish = 2, - /// - /// Creates and publishes a new content item - /// - PublishNew = 3, + /// + /// Creates and publishes a new content item + /// + PublishNew = 3, - /// - /// Saves and sends publish notification - /// - SendPublish = 4, + /// + /// Saves and sends publish notification + /// + SendPublish = 4, - /// - /// Creates and sends publish notification - /// - SendPublishNew = 5, + /// + /// Creates and sends publish notification + /// + SendPublishNew = 5, - /// - /// Saves and schedules publishing - /// - Schedule = 6, + /// + /// Saves and schedules publishing + /// + Schedule = 6, - /// - /// Creates and schedules publishing - /// - ScheduleNew = 7, + /// + /// Creates and schedules publishing + /// + ScheduleNew = 7, - /// - /// Saves and publishes the content item including all descendants that have a published version - /// - PublishWithDescendants = 8, + /// + /// Saves and publishes the content item including all descendants that have a published version + /// + PublishWithDescendants = 8, - /// - /// Creates and publishes the content item including all descendants that have a published version - /// - PublishWithDescendantsNew = 9, + /// + /// Creates and publishes the content item including all descendants that have a published version + /// + PublishWithDescendantsNew = 9, - /// - /// Saves and publishes the content item including all descendants regardless of whether they have a published version or not - /// - PublishWithDescendantsForce = 10, + /// + /// Saves and publishes the content item including all descendants regardless of whether they have a published version + /// or not + /// + PublishWithDescendantsForce = 10, - /// - /// Creates and publishes the content item including all descendants regardless of whether they have a published version or not - /// - PublishWithDescendantsForceNew = 11 - } + /// + /// Creates and publishes the content item including all descendants regardless of whether they have a published + /// version or not + /// + PublishWithDescendantsForceNew = 11, } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentSavedState.cs b/src/Umbraco.Core/Models/ContentEditing/ContentSavedState.cs index 00fce177b4..1635141934 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentSavedState.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentSavedState.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The saved state of a content item +/// +public enum ContentSavedState { /// - /// The saved state of a content item + /// The item isn't created yet /// - public enum ContentSavedState - { - /// - /// The item isn't created yet - /// - NotCreated = 1, + NotCreated = 1, - /// - /// The item is saved but isn't published - /// - Draft = 2, + /// + /// The item is saved but isn't published + /// + Draft = 2, - /// - /// The item is published and there are no pending changes - /// - Published = 3, + /// + /// The item is published and there are no pending changes + /// + Published = 3, - /// - /// The item is published and there are pending changes - /// - PublishedPendingChanges = 4 - } + /// + /// The item is published and there are pending changes + /// + PublishedPendingChanges = 4, } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentSortOrder.cs b/src/Umbraco.Core/Models/ContentEditing/ContentSortOrder.cs index 80c24b8cc0..17d751760d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentSortOrder.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentSortOrder.cs @@ -1,31 +1,28 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a new sort order for a content/media item +/// +[DataContract(Name = "content", Namespace = "")] +public class ContentSortOrder { /// - /// A model representing a new sort order for a content/media item + /// The parent Id of the nodes being sorted /// - [DataContract(Name = "content", Namespace = "")] - public class ContentSortOrder - { - /// - /// The parent Id of the nodes being sorted - /// - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } - - /// - /// An array of integer Ids representing the sort order - /// - /// - /// Of course all of these Ids should be at the same level in the hierarchy!! - /// - [DataMember(Name = "idSortOrder", IsRequired = true)] - [Required] - public int[]? IdSortOrder { get; set; } - - } + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } + /// + /// An array of integer Ids representing the sort order + /// + /// + /// Of course all of these Ids should be at the same level in the hierarchy!! + /// + [DataMember(Name = "idSortOrder", IsRequired = true)] + [Required] + public int[]? IdSortOrder { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs index f46d8dc8f8..90dd6ce5c9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeBasic.cs @@ -1,109 +1,108 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A basic version of a content type +/// +/// +/// Generally used to return the minimal amount of data about a content type +/// +[DataContract(Name = "contentType", Namespace = "")] +public class ContentTypeBasic : EntityBasic { - /// - /// A basic version of a content type - /// - /// - /// Generally used to return the minimal amount of data about a content type - /// - [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeBasic : EntityBasic + public ContentTypeBasic() { - public ContentTypeBasic() - { - Blueprints = new Dictionary(); - Alias = string.Empty; - } - - /// - /// Overridden to apply our own validation attributes since this is not always required for other classes - /// - [Required] - [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] - [DataMember(Name = "alias")] - public override string Alias { get; set; } - - [DataMember(Name = "updateDate")] - [ReadOnly(true)] - public DateTime UpdateDate { get; set; } - - [DataMember(Name = "createDate")] - [ReadOnly(true)] - public DateTime CreateDate { get; set; } - - [DataMember(Name = "description")] - public string? Description { get; set; } - - [DataMember(Name = "thumbnail")] - public string? Thumbnail { get; set; } - - /// - /// Returns true if the icon represents a CSS class instead of a file path - /// - [DataMember(Name = "iconIsClass")] - [ReadOnly(true)] - public bool IconIsClass - { - get - { - if (Icon.IsNullOrWhiteSpace()) - { - return true; - } - //if it starts with a '.' or doesn't contain a '.' at all then it is a class - return (Icon?.StartsWith(".") ?? false) || Icon?.Contains(".") == false; - } - } - - /// - /// Returns the icon file path if the icon is not a class, otherwise returns an empty string - /// - [DataMember(Name = "iconFilePath")] - [ReadOnly(true)] - public string? IconFilePath { get; set; } - - /// - /// Returns true if the icon represents a CSS class instead of a file path - /// - [DataMember(Name = "thumbnailIsClass")] - [ReadOnly(true)] - public bool ThumbnailIsClass - { - get - { - if (Thumbnail.IsNullOrWhiteSpace()) - { - return true; - } - //if it starts with a '.' or doesn't contain a '.' at all then it is a class - return (Thumbnail?.StartsWith(".") ?? false) || Thumbnail?.Contains(".") == false; - } - } - - /// - /// Returns the icon file path if the icon is not a class, otherwise returns an empty string - /// - [DataMember(Name = "thumbnailFilePath")] - [ReadOnly(true)] - public string? ThumbnailFilePath { get; set; } - - [DataMember(Name = "blueprints")] - [ReadOnly(true)] - public IDictionary Blueprints { get; set; } - - [DataMember(Name = "isContainer")] - [ReadOnly(true)] - public bool IsContainer { get; set; } - - [DataMember(Name = "isElement")] - [ReadOnly(true)] - public bool IsElement { get; set; } + Blueprints = new Dictionary(); + Alias = string.Empty; } + + /// + /// Overridden to apply our own validation attributes since this is not always required for other classes + /// + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public override string Alias { get; set; } + + [DataMember(Name = "updateDate")] + [ReadOnly(true)] + public DateTime UpdateDate { get; set; } + + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + [DataMember(Name = "description")] + public string? Description { get; set; } + + [DataMember(Name = "thumbnail")] + public string? Thumbnail { get; set; } + + /// + /// Returns true if the icon represents a CSS class instead of a file path + /// + [DataMember(Name = "iconIsClass")] + [ReadOnly(true)] + public bool IconIsClass + { + get + { + if (Icon.IsNullOrWhiteSpace()) + { + return true; + } + + // if it starts with a '.' or doesn't contain a '.' at all then it is a class + return (Icon?.StartsWith(".") ?? false) || Icon?.Contains(".") == false; + } + } + + /// + /// Returns the icon file path if the icon is not a class, otherwise returns an empty string + /// + [DataMember(Name = "iconFilePath")] + [ReadOnly(true)] + public string? IconFilePath { get; set; } + + /// + /// Returns true if the icon represents a CSS class instead of a file path + /// + [DataMember(Name = "thumbnailIsClass")] + [ReadOnly(true)] + public bool ThumbnailIsClass + { + get + { + if (Thumbnail.IsNullOrWhiteSpace()) + { + return true; + } + + // if it starts with a '.' or doesn't contain a '.' at all then it is a class + return (Thumbnail?.StartsWith(".") ?? false) || Thumbnail?.Contains(".") == false; + } + } + + /// + /// Returns the icon file path if the icon is not a class, otherwise returns an empty string + /// + [DataMember(Name = "thumbnailFilePath")] + [ReadOnly(true)] + public string? ThumbnailFilePath { get; set; } + + [DataMember(Name = "blueprints")] + [ReadOnly(true)] + public IDictionary Blueprints { get; set; } + + [DataMember(Name = "isContainer")] + [ReadOnly(true)] + public bool IsContainer { get; set; } + + [DataMember(Name = "isElement")] + [ReadOnly(true)] + public bool IsElement { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeCompositionDisplay.cs index 4eb8563a6e..030923d291 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeCompositionDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeCompositionDisplay.cs @@ -1,74 +1,69 @@ -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public abstract class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel { - public abstract class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel + protected ContentTypeCompositionDisplay() { - protected ContentTypeCompositionDisplay() - { - //initialize collections so at least their never null - AllowedContentTypes = new List(); - CompositeContentTypes = new List(); - Notifications = new List(); - } - - //name, alias, icon, thumb, desc, inherited from basic - - [DataMember(Name = "listViewEditorName")] - [ReadOnly(true)] - public string? ListViewEditorName { get; set; } - - //Allowed child types - [DataMember(Name = "allowedContentTypes")] - public IEnumerable? AllowedContentTypes { get; set; } - - //Compositions - [DataMember(Name = "compositeContentTypes")] - public IEnumerable CompositeContentTypes { get; set; } - - //Locked compositions - [DataMember(Name = "lockedCompositeContentTypes")] - public IEnumerable? LockedCompositeContentTypes { get; set; } - - [DataMember(Name = "allowAsRoot")] - public bool AllowAsRoot { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - [ReadOnly(true)] - public List Notifications { get; private set; } - - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. - /// - [DataMember(Name = "ModelState")] - [ReadOnly(true)] - public IDictionary? Errors { get; set; } + // initialize collections so at least their never null + AllowedContentTypes = new List(); + CompositeContentTypes = new List(); + Notifications = new List(); } - [DataContract(Name = "contentType", Namespace = "")] - public abstract class ContentTypeCompositionDisplay : ContentTypeCompositionDisplay - where TPropertyTypeDisplay : PropertyTypeDisplay - { - protected ContentTypeCompositionDisplay() - { - //initialize collections so at least their never null - Groups = new List>(); - } + // name, alias, icon, thumb, desc, inherited from basic + [DataMember(Name = "listViewEditorName")] + [ReadOnly(true)] + public string? ListViewEditorName { get; set; } - //Tabs - [DataMember(Name = "groups")] - public IEnumerable> Groups { get; set; } - } + // Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable? AllowedContentTypes { get; set; } + + // Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } + + // Locked compositions + [DataMember(Name = "lockedCompositeContentTypes")] + public IEnumerable? LockedCompositeContentTypes { get; set; } + + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { get; set; } + + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. + /// + [DataMember(Name = "ModelState")] + [ReadOnly(true)] + public IDictionary? Errors { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } +} + +[DataContract(Name = "contentType", Namespace = "")] +public abstract class ContentTypeCompositionDisplay : ContentTypeCompositionDisplay + where TPropertyTypeDisplay : PropertyTypeDisplay +{ + protected ContentTypeCompositionDisplay() => + + // initialize collections so at least their never null + Groups = new List>(); + + // Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs index 55c4a07cfd..d6ad7c7ba2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs @@ -1,121 +1,120 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Abstract model used to save content types +/// +[DataContract(Name = "contentType", Namespace = "")] +public abstract class ContentTypeSave : ContentTypeBasic, IValidatableObject { - /// - /// Abstract model used to save content types - /// - [DataContract(Name = "contentType", Namespace = "")] - public abstract class ContentTypeSave : ContentTypeBasic, IValidatableObject + protected ContentTypeSave() { - protected ContentTypeSave() - { - AllowedContentTypes = new List(); - CompositeContentTypes = new List(); - } - - //Compositions - [DataMember(Name = "compositeContentTypes")] - public IEnumerable CompositeContentTypes { get; set; } - - [DataMember(Name = "allowAsRoot")] - public bool AllowAsRoot { get; set; } - - //Allowed child types - [DataMember(Name = "allowedContentTypes")] - public IEnumerable AllowedContentTypes { get; set; } - - [DataMember(Name = "historyCleanup")] - public HistoryCleanupViewModel? HistoryCleanup { get; set; } - - /// - /// Custom validation - /// - /// - /// - public virtual IEnumerable Validate(ValidationContext validationContext) - { - if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) - yield return new ValidationResult("Composite Content Type value cannot be null", new[] { "CompositeContentTypes" }); - } + AllowedContentTypes = new List(); + CompositeContentTypes = new List(); } + // Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } + + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { get; set; } + + // Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable AllowedContentTypes { get; set; } + + [DataMember(Name = "historyCleanup")] + public HistoryCleanupViewModel? HistoryCleanup { get; set; } + /// - /// Abstract model used to save content types + /// Custom validation /// - /// - [DataContract(Name = "contentType", Namespace = "")] - public abstract class ContentTypeSave : ContentTypeSave - where TPropertyType : PropertyTypeBasic + /// + /// + public virtual IEnumerable Validate(ValidationContext validationContext) { - protected ContentTypeSave() + if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) { - Groups = new List>(); - } - - /// - /// A rule for defining how a content type can be varied - /// - /// - /// This is only supported on document types right now but in the future it could be media types too - /// - [DataMember(Name = "allowCultureVariant")] - public bool AllowCultureVariant { get; set; } - - [DataMember(Name = "allowSegmentVariant")] - public bool AllowSegmentVariant { get; set; } - - //Tabs - [DataMember(Name = "groups")] - public IEnumerable> Groups { get; set; } - - /// - /// Custom validation - /// - /// - /// - public override IEnumerable Validate(ValidationContext validationContext) - { - foreach (var validationResult in base.Validate(validationContext)) - { - yield return validationResult; - } - - foreach (var duplicateGroupAlias in Groups.GroupBy(x => x.Alias).Where(x => x.Count() > 1)) - { - var lastGroupIndex = Groups.IndexOf(duplicateGroupAlias.Last()); - yield return new ValidationResult("Duplicate aliases are not allowed: " + duplicateGroupAlias.Key, new[] - { - // TODO: We don't display the alias yet, so add the validation message to the name - $"Groups[{lastGroupIndex}].Name" - }); - } - - foreach (var duplicateGroupName in Groups.GroupBy(x => (x.GetParentAlias(), x.Name)).Where(x => x.Count() > 1)) - { - var lastGroupIndex = Groups.IndexOf(duplicateGroupName.Last()); - yield return new ValidationResult("Duplicate names are not allowed", new[] - { - $"Groups[{lastGroupIndex}].Name" - }); - } - - foreach (var duplicatePropertyAlias in Groups.SelectMany(x => x.Properties).GroupBy(x => x.Alias).Where(x => x.Count() > 1)) - { - var lastProperty = duplicatePropertyAlias.Last(); - var propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty)); - var lastPropertyIndex = propertyGroup.Properties.IndexOf(lastProperty); - var propertyGroupIndex = Groups.IndexOf(propertyGroup); - - yield return new ValidationResult("Duplicate property aliases not allowed: " + duplicatePropertyAlias.Key, new[] - { - $"Groups[{propertyGroupIndex}].Properties[{lastPropertyIndex}].Alias" - }); - } + yield return new ValidationResult( + "Composite Content Type value cannot be null", + new[] { "CompositeContentTypes" }); + } + } +} + +/// +/// Abstract model used to save content types +/// +/// +[DataContract(Name = "contentType", Namespace = "")] +public abstract class ContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic +{ + protected ContentTypeSave() => Groups = new List>(); + + /// + /// A rule for defining how a content type can be varied + /// + /// + /// This is only supported on document types right now but in the future it could be media types too + /// + [DataMember(Name = "allowCultureVariant")] + public bool AllowCultureVariant { get; set; } + + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } + + // Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } + + /// + /// Custom validation + /// + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) + { + foreach (ValidationResult validationResult in base.Validate(validationContext)) + { + yield return validationResult; + } + + foreach (IGrouping> duplicateGroupAlias in Groups + .GroupBy(x => x.Alias).Where(x => x.Count() > 1)) + { + var lastGroupIndex = Groups.IndexOf(duplicateGroupAlias.Last()); + yield return new ValidationResult("Duplicate aliases are not allowed: " + duplicateGroupAlias.Key, new[] + { + // TODO: We don't display the alias yet, so add the validation message to the name + $"Groups[{lastGroupIndex}].Name", + }); + } + + foreach (IGrouping<(string?, string? Name), PropertyGroupBasic> duplicateGroupName in Groups + .GroupBy(x => (x.GetParentAlias(), x.Name)).Where(x => x.Count() > 1)) + { + var lastGroupIndex = Groups.IndexOf(duplicateGroupName.Last()); + yield return new ValidationResult( + "Duplicate names are not allowed", + new[] { $"Groups[{lastGroupIndex}].Name" }); + } + + foreach (IGrouping duplicatePropertyAlias in Groups.SelectMany(x => x.Properties) + .GroupBy(x => x.Alias).Where(x => x.Count() > 1)) + { + TPropertyType lastProperty = duplicatePropertyAlias.Last(); + PropertyGroupBasic propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty)); + var lastPropertyIndex = propertyGroup.Properties.IndexOf(lastProperty); + var propertyGroupIndex = Groups.IndexOf(propertyGroup); + + yield return new ValidationResult( + "Duplicate property aliases not allowed: " + duplicatePropertyAlias.Key, + new[] { $"Groups[{propertyGroupIndex}].Properties[{lastPropertyIndex}].Alias" }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs index 57b1c98d54..476e772743 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs @@ -1,26 +1,25 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model for retrieving multiple content types based on their aliases. +/// +[DataContract(Name = "contentTypes", Namespace = "")] +public class ContentTypesByAliases { /// - /// A model for retrieving multiple content types based on their aliases. + /// Id of the parent of the content type. /// - [DataContract(Name = "contentTypes", Namespace = "")] - public class ContentTypesByAliases - { - /// - /// Id of the parent of the content type. - /// - [DataMember(Name = "parentId")] - [Required] - public int ParentId { get; set; } + [DataMember(Name = "parentId")] + [Required] + public int ParentId { get; set; } - /// - /// The alias of every content type to get. - /// - [DataMember(Name = "contentTypeAliases")] - [Required] - public string[]? ContentTypeAliases { get; set; } - } + /// + /// The alias of every content type to get. + /// + [DataMember(Name = "contentTypeAliases")] + [Required] + public string[]? ContentTypeAliases { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs index 0a2bea7f88..2b728c04da 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs @@ -1,27 +1,25 @@ -using System; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model for retrieving multiple content types based on their keys. +/// +[DataContract(Name = "contentTypes", Namespace = "")] +public class ContentTypesByKeys { /// - /// A model for retrieving multiple content types based on their keys. + /// ID of the parent of the content type. /// - [DataContract(Name = "contentTypes", Namespace = "")] - public class ContentTypesByKeys - { - /// - /// ID of the parent of the content type. - /// - [DataMember(Name = "parentId")] - [Required] - public int ParentId { get; set; } + [DataMember(Name = "parentId")] + [Required] + public int ParentId { get; set; } - /// - /// The id of every content type to get. - /// - [DataMember(Name = "contentTypeKeys")] - [Required] - public Guid[]? ContentTypeKeys { get; set; } - } + /// + /// The id of every content type to get. + /// + [DataMember(Name = "contentTypeKeys")] + [Required] + public Guid[]? ContentTypeKeys { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs index f7ea69f7ce..ed9568590f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs @@ -1,73 +1,69 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Validation; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentVariant", Namespace = "")] +public class ContentVariantSave : IContentProperties { - [DataContract(Name = "contentVariant", Namespace = "")] - public class ContentVariantSave : IContentProperties - { - public ContentVariantSave() - { - Properties = new List(); - } + public ContentVariantSave() => Properties = new List(); - [DataMember(Name = "name", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - [MaxLength(255, ErrorMessage ="Name must be less than 255 characters")] - public string? Name { get; set; } + [DataMember(Name = "name", IsRequired = true)] + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + [MaxLength(255, ErrorMessage = "Name must be less than 255 characters")] + public string? Name { get; set; } - [DataMember(Name = "properties")] - public IEnumerable Properties { get; set; } + /// + /// The culture of this variant, if this is invariant than this is null or empty + /// + [DataMember(Name = "culture")] + public string? Culture { get; set; } - /// - /// The culture of this variant, if this is invariant than this is null or empty - /// - [DataMember(Name = "culture")] - public string? Culture { get; set; } + /// + /// The segment of this variant, if this is invariant than this is null or empty + /// + [DataMember(Name = "segment")] + public string? Segment { get; set; } - /// - /// The segment of this variant, if this is invariant than this is null or empty - /// - [DataMember(Name = "segment")] - public string? Segment { get; set; } + /// + /// Indicates if the variant should be updated + /// + /// + /// If this is false, this variant data will not be updated at all + /// + [DataMember(Name = "save")] + public bool Save { get; set; } - /// - /// Indicates if the variant should be updated - /// - /// - /// If this is false, this variant data will not be updated at all - /// - [DataMember(Name = "save")] - public bool Save { get; set; } + /// + /// Indicates if the variant should be published + /// + /// + /// This option will have no affect if is false. + /// This is not used to unpublish. + /// + [DataMember(Name = "publish")] + public bool Publish { get; set; } - /// - /// Indicates if the variant should be published - /// - /// - /// This option will have no affect if is false. - /// This is not used to unpublish. - /// - [DataMember(Name = "publish")] - public bool Publish { get; set; } + [DataMember(Name = "expireDate")] + public DateTime? ExpireDate { get; set; } - [DataMember(Name = "expireDate")] - public DateTime? ExpireDate { get; set; } + [DataMember(Name = "releaseDate")] + public DateTime? ReleaseDate { get; set; } - [DataMember(Name = "releaseDate")] - public DateTime? ReleaseDate { get; set; } + /// + /// The property DTO object is used to gather all required property data including data type information etc... for use + /// with validation - used during inbound model binding + /// + /// + /// We basically use this object to hydrate all required data from the database into one object so we can validate + /// everything we need + /// instead of having to look up all the data individually. + /// This is not used for outgoing model information. + /// + [IgnoreDataMember] + public ContentPropertyCollectionDto? PropertyCollectionDto { get; set; } - /// - /// The property DTO object is used to gather all required property data including data type information etc... for use with validation - used during inbound model binding - /// - /// - /// We basically use this object to hydrate all required data from the database into one object so we can validate everything we need - /// instead of having to look up all the data individually. - /// This is not used for outgoing model information. - /// - [IgnoreDataMember] - public ContentPropertyCollectionDto? PropertyCollectionDto { get; set; } - } + [DataMember(Name = "properties")] + public IEnumerable Properties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs index 44f0b31c25..15b97ab24f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentVariationDisplay.cs @@ -1,82 +1,80 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents the variant info for a content item +/// +[DataContract(Name = "contentVariant", Namespace = "")] +public class ContentVariantDisplay : ITabbedContent, IContentProperties, INotificationModel { + public ContentVariantDisplay() + { + Tabs = new List>(); + Notifications = new List(); + } + + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } + + [DataMember(Name = "displayName")] + public string? DisplayName { get; set; } + /// - /// Represents the variant info for a content item + /// The language/culture assigned to this content variation /// - [DataContract(Name = "contentVariant", Namespace = "")] - public class ContentVariantDisplay : ITabbedContent, IContentProperties, INotificationModel - { - public ContentVariantDisplay() - { - Tabs = new List>(); - Notifications = new List(); - } + /// + /// If this is null it means this content variant is an invariant culture + /// + [DataMember(Name = "language")] + public Language? Language { get; set; } - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } + [DataMember(Name = "segment")] + public string? Segment { get; set; } - [DataMember(Name = "displayName")] - public string? DisplayName { get; set; } + [DataMember(Name = "state")] + public ContentSavedState State { get; set; } - /// - /// Defines the tabs containing display properties - /// - [DataMember(Name = "tabs")] - public IEnumerable> Tabs { get; set; } + [DataMember(Name = "updateDate")] + public DateTime UpdateDate { get; set; } - /// - /// Internal property used for tests to get all properties from all tabs - /// - [IgnoreDataMember] - IEnumerable IContentProperties.Properties => Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); + [DataMember(Name = "createDate")] + public DateTime CreateDate { get; set; } - /// - /// The language/culture assigned to this content variation - /// - /// - /// If this is null it means this content variant is an invariant culture - /// - [DataMember(Name = "language")] - public Language? Language { get; set; } + [DataMember(Name = "publishDate")] + public DateTime? PublishDate { get; set; } - [DataMember(Name = "segment")] - public string? Segment { get; set; } + /// + /// Internal property used for tests to get all properties from all tabs + /// + [IgnoreDataMember] + IEnumerable IContentProperties.Properties => + Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); - [DataMember(Name = "state")] - public ContentSavedState State { get; set; } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + /// + /// The notifications assigned to a variant are currently only used to show custom messages in the save/publish + /// dialogs. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } - [DataMember(Name = "updateDate")] - public DateTime UpdateDate { get; set; } - - [DataMember(Name = "createDate")] - public DateTime CreateDate { get; set; } - - [DataMember(Name = "publishDate")] - public DateTime? PublishDate { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - /// - /// The notifications assigned to a variant are currently only used to show custom messages in the save/publish dialogs. - /// - [DataMember(Name = "notifications")] - [ReadOnly(true)] - public List Notifications { get; private set; } - } - - public class ContentVariantScheduleDisplay : ContentVariantDisplay - { - [DataMember(Name = "releaseDate")] - public DateTime? ReleaseDate { get; set; } - - [DataMember(Name = "expireDate")] - public DateTime? ExpireDate { get; set; } - } + /// + /// Defines the tabs containing display properties + /// + [DataMember(Name = "tabs")] + public IEnumerable> Tabs { get; set; } +} + +public class ContentVariantScheduleDisplay : ContentVariantDisplay +{ + [DataMember(Name = "releaseDate")] + public DateTime? ReleaseDate { get; set; } + + [DataMember(Name = "expireDate")] + public DateTime? ExpireDate { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs b/src/Umbraco.Core/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs index b1db2759f0..a6f99ab586 100644 --- a/src/Umbraco.Core/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs +++ b/src/Umbraco.Core/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The result of creating a content type collection in the UI +/// +[DataContract(Name = "contentTypeCollection", Namespace = "")] +public class CreatedContentTypeCollectionResult { - /// - /// The result of creating a content type collection in the UI - /// - [DataContract(Name = "contentTypeCollection", Namespace = "")] - public class CreatedContentTypeCollectionResult - { - [DataMember(Name = "collectionId")] - public int CollectionId { get; set; } + [DataMember(Name = "collectionId")] + public int CollectionId { get; set; } - [DataMember(Name = "containerId")] - public int ContainerId { get; set; } - } + [DataMember(Name = "containerId")] + public int ContainerId { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeBasic.cs index 153f495a70..7bb0d427ea 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeBasic.cs @@ -1,27 +1,26 @@ -using System.ComponentModel; +using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The basic data type information +/// +[DataContract(Name = "dataType", Namespace = "")] +public class DataTypeBasic : EntityBasic { /// - /// The basic data type information + /// Whether or not this is a system data type, in which case it cannot be deleted /// - [DataContract(Name = "dataType", Namespace = "")] - public class DataTypeBasic : EntityBasic - { - /// - /// Whether or not this is a system data type, in which case it cannot be deleted - /// - [DataMember(Name = "isSystem")] - [ReadOnly(true)] - public bool IsSystemDataType { get; set; } + [DataMember(Name = "isSystem")] + [ReadOnly(true)] + public bool IsSystemDataType { get; set; } - [DataMember(Name = "group")] - [ReadOnly(true)] - public string? Group { get; set; } + [DataMember(Name = "group")] + [ReadOnly(true)] + public string? Group { get; set; } - [DataMember(Name = "hasPrevalues")] - [ReadOnly(true)] - public bool HasPrevalues { get; set; } - } + [DataMember(Name = "hasPrevalues")] + [ReadOnly(true)] + public bool HasPrevalues { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldDisplay.cs index 97a2177167..a324bb4bce 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldDisplay.cs @@ -1,42 +1,40 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a datatype configuration field model for editing. +/// +[DataContract(Name = "preValue", Namespace = "")] +public class DataTypeConfigurationFieldDisplay : DataTypeConfigurationFieldSave { /// - /// Represents a datatype configuration field model for editing. + /// The name to display for this pre-value field /// - [DataContract(Name = "preValue", Namespace = "")] - public class DataTypeConfigurationFieldDisplay : DataTypeConfigurationFieldSave - { - /// - /// The name to display for this pre-value field - /// - [DataMember(Name = "label", IsRequired = true)] - public string? Name { get; set; } + [DataMember(Name = "label", IsRequired = true)] + public string? Name { get; set; } - /// - /// The description to display for this pre-value field - /// - [DataMember(Name = "description")] - public string? Description { get; set; } + /// + /// The description to display for this pre-value field + /// + [DataMember(Name = "description")] + public string? Description { get; set; } - /// - /// Specifies whether to hide the label for the pre-value - /// - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } + /// + /// Specifies whether to hide the label for the pre-value + /// + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } - /// - /// The view to render for the field - /// - [DataMember(Name = "view", IsRequired = true)] - public string? View { get; set; } + /// + /// The view to render for the field + /// + [DataMember(Name = "view", IsRequired = true)] + public string? View { get; set; } - /// - /// This allows for custom configuration to be injected into the pre-value editor - /// - [DataMember(Name = "config")] - public IDictionary? Config { get; set; } - } + /// + /// This allows for custom configuration to be injected into the pre-value editor + /// + [DataMember(Name = "config")] + public IDictionary? Config { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldSave.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldSave.cs index a82a6eb257..514f9b8618 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeConfigurationFieldSave.cs @@ -1,23 +1,22 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a datatype configuration field model for editing. +/// +[DataContract(Name = "preValue", Namespace = "")] +public class DataTypeConfigurationFieldSave { /// - /// Represents a datatype configuration field model for editing. + /// Gets or sets the configuration field key. /// - [DataContract(Name = "preValue", Namespace = "")] - public class DataTypeConfigurationFieldSave - { - /// - /// Gets or sets the configuration field key. - /// - [DataMember(Name = "key", IsRequired = true)] - public string Key { get; set; } = null!; + [DataMember(Name = "key", IsRequired = true)] + public string Key { get; set; } = null!; - /// - /// Gets or sets the configuration field value. - /// - [DataMember(Name = "value", IsRequired = true)] - public object? Value { get; set; } - } + /// + /// Gets or sets the configuration field value. + /// + [DataMember(Name = "value", IsRequired = true)] + public object? Value { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeDisplay.cs index cbe5552b1e..7f3c93d126 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeDisplay.cs @@ -1,37 +1,32 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a data type that is being edited +/// +[DataContract(Name = "dataType", Namespace = "")] +public class DataTypeDisplay : DataTypeBasic, INotificationModel { + public DataTypeDisplay() => Notifications = new List(); + /// - /// Represents a data type that is being edited + /// The alias of the property editor /// - [DataContract(Name = "dataType", Namespace = "")] - public class DataTypeDisplay : DataTypeBasic, INotificationModel - { - public DataTypeDisplay() - { - Notifications = new List(); - } + [DataMember(Name = "selectedEditor", IsRequired = true)] + [Required] + public string? SelectedEditor { get; set; } - /// - /// The alias of the property editor - /// - [DataMember(Name = "selectedEditor", IsRequired = true)] - [Required] - public string? SelectedEditor { get; set; } + [DataMember(Name = "availableEditors")] + public IEnumerable? AvailableEditors { get; set; } - [DataMember(Name = "availableEditors")] - public IEnumerable? AvailableEditors { get; set; } + [DataMember(Name = "preValues")] + public IEnumerable? PreValues { get; set; } - [DataMember(Name = "preValues")] - public IEnumerable? PreValues { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeReferences.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeReferences.cs index c8699472d5..47711fc0a3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeReferences.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeReferences.cs @@ -1,36 +1,33 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "dataTypeReferences", Namespace = "")] +public class DataTypeReferences { - [DataContract(Name = "dataTypeReferences", Namespace = "")] - public class DataTypeReferences + [DataMember(Name = "documentTypes")] + public IEnumerable DocumentTypes { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "mediaTypes")] + public IEnumerable MediaTypes { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "memberTypes")] + public IEnumerable MemberTypes { get; set; } = Enumerable.Empty(); + + [DataContract(Name = "contentType", Namespace = "")] + public class ContentTypeReferences : EntityBasic { - [DataMember(Name = "documentTypes")] - public IEnumerable DocumentTypes { get; set; } = Enumerable.Empty(); + [DataMember(Name = "properties")] + public object? Properties { get; set; } - [DataMember(Name = "mediaTypes")] - public IEnumerable MediaTypes { get; set; } = Enumerable.Empty(); - - [DataMember(Name = "memberTypes")] - public IEnumerable MemberTypes { get; set; } = Enumerable.Empty(); - - [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeReferences : EntityBasic + [DataContract(Name = "property", Namespace = "")] + public class PropertyTypeReferences { - [DataMember(Name = "properties")] - public object? Properties { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataContract(Name = "property", Namespace = "")] - public class PropertyTypeReferences - { - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "alias")] - public string? Alias { get; set; } - } + [DataMember(Name = "alias")] + public string? Alias { get; set; } } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DataTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/DataTypeSave.cs index 3795e42782..8968fb0795 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DataTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DataTypeSave.cs @@ -1,49 +1,47 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a datatype model for editing. +/// +[DataContract(Name = "dataType", Namespace = "")] +public class DataTypeSave : EntityBasic { /// - /// Represents a datatype model for editing. + /// Gets or sets the action to perform. /// - [DataContract(Name = "dataType", Namespace = "")] - public class DataTypeSave : EntityBasic - { - /// - /// Gets or sets the action to perform. - /// - /// - /// Some values (publish) are illegal here. - /// - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } + /// + /// Some values (publish) are illegal here. + /// + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } - /// - /// Gets or sets the datatype editor. - /// - [DataMember(Name = "selectedEditor", IsRequired = true)] - [Required] - public string? EditorAlias { get; set; } + /// + /// Gets or sets the datatype editor. + /// + [DataMember(Name = "selectedEditor", IsRequired = true)] + [Required] + public string? EditorAlias { get; set; } - /// - /// Gets or sets the datatype configuration fields. - /// - [DataMember(Name = "preValues")] - public IEnumerable? ConfigurationFields { get; set; } + /// + /// Gets or sets the datatype configuration fields. + /// + [DataMember(Name = "preValues")] + public IEnumerable? ConfigurationFields { get; set; } - /// - /// Gets or sets the persisted data type. - /// - [IgnoreDataMember] - public IDataType? PersistedDataType { get; set; } + /// + /// Gets or sets the persisted data type. + /// + [IgnoreDataMember] + public IDataType? PersistedDataType { get; set; } - /// - /// Gets or sets the property editor. - /// - [IgnoreDataMember] - public IDataEditor? PropertyEditor { get; set; } - } + /// + /// Gets or sets the property editor. + /// + [IgnoreDataMember] + public IDataEditor? PropertyEditor { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs index d8cfaf1104..59e5bffb4b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs @@ -1,48 +1,45 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The dictionary display model +/// +[DataContract(Name = "dictionary", Namespace = "")] +public class DictionaryDisplay : EntityBasic, INotificationModel { /// - /// The dictionary display model + /// Initializes a new instance of the class. /// - [DataContract(Name = "dictionary", Namespace = "")] - public class DictionaryDisplay : EntityBasic, INotificationModel + public DictionaryDisplay() { - /// - /// Initializes a new instance of the class. - /// - public DictionaryDisplay() - { - Notifications = new List(); - Translations = new List(); - ContentApps = new List(); - } - - /// - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - - /// - /// Gets or sets the parent id. - /// - [DataMember(Name = "parentId")] - public new Guid ParentId { get; set; } - - /// - /// Gets the translations. - /// - [DataMember(Name = "translations")] - public List Translations { get; private set; } - - /// - /// Apps for the dictionary item - /// - [DataMember(Name = "apps")] - public List ContentApps { get; private set; } + Notifications = new List(); + Translations = new List(); + ContentApps = new List(); } + + /// + /// Gets or sets the parent id. + /// + [DataMember(Name = "parentId")] + public new Guid ParentId { get; set; } + + /// + /// Gets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; private set; } + + /// + /// Apps for the dictionary item + /// + [DataMember(Name = "apps")] + public List ContentApps { get; private set; } + + /// + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewDisplay.cs index adf279c412..15aab0c7ef 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewDisplay.cs @@ -1,44 +1,39 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The dictionary overview display. +/// +[DataContract(Name = "dictionary", Namespace = "")] +public class DictionaryOverviewDisplay { /// - /// The dictionary overview display. + /// Initializes a new instance of the class. /// - [DataContract(Name = "dictionary", Namespace = "")] - public class DictionaryOverviewDisplay - { - /// - /// Initializes a new instance of the class. - /// - public DictionaryOverviewDisplay() - { - Translations = new List(); - } + public DictionaryOverviewDisplay() => Translations = new List(); - /// - /// Gets or sets the key. - /// - [DataMember(Name = "name")] - public string? Name { get; set; } + /// + /// Gets or sets the key. + /// + [DataMember(Name = "name")] + public string? Name { get; set; } - /// - /// Gets or sets the id. - /// - [DataMember(Name = "id")] - public int Id { get; set; } + /// + /// Gets or sets the id. + /// + [DataMember(Name = "id")] + public int Id { get; set; } - /// - /// Gets or sets the level. - /// - [DataMember(Name = "level")] - public int Level { get; set; } + /// + /// Gets or sets the level. + /// + [DataMember(Name = "level")] + public int Level { get; set; } - /// - /// Gets or sets the translations. - /// - [DataMember(Name = "translations")] - public List Translations { get; set; } - } + /// + /// Gets or sets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs index 00d8b339f9..9e534820fa 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs @@ -1,23 +1,22 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The dictionary translation overview display. +/// +[DataContract(Name = "dictionaryTranslation", Namespace = "")] +public class DictionaryOverviewTranslationDisplay { /// - /// The dictionary translation overview display. + /// Gets or sets the display name. /// - [DataContract(Name = "dictionaryTranslation", Namespace = "")] - public class DictionaryOverviewTranslationDisplay - { - /// - /// Gets or sets the display name. - /// - [DataMember(Name = "displayName")] - public string? DisplayName { get; set; } + [DataMember(Name = "displayName")] + public string? DisplayName { get; set; } - /// - /// Gets or sets a value indicating whether has translation. - /// - [DataMember(Name = "hasTranslation")] - public bool HasTranslation { get; set; } - } + /// + /// Gets or sets a value indicating whether has translation. + /// + [DataMember(Name = "hasTranslation")] + public bool HasTranslation { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionarySave.cs b/src/Umbraco.Core/Models/ContentEditing/DictionarySave.cs index 0e652e7160..85585c45ba 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionarySave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionarySave.cs @@ -1,39 +1,33 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Dictionary Save model +/// +[DataContract(Name = "dictionary", Namespace = "")] +public class DictionarySave : EntityBasic { /// - /// Dictionary Save model + /// Initializes a new instance of the class. /// - [DataContract(Name = "dictionary", Namespace = "")] - public class DictionarySave : EntityBasic - { - /// - /// Initializes a new instance of the class. - /// - public DictionarySave() - { - Translations = new List(); - } + public DictionarySave() => Translations = new List(); - /// - /// Gets or sets a value indicating whether name is dirty. - /// - [DataMember(Name = "nameIsDirty")] - public bool NameIsDirty { get; set; } + /// + /// Gets or sets a value indicating whether name is dirty. + /// + [DataMember(Name = "nameIsDirty")] + public bool NameIsDirty { get; set; } - /// - /// Gets the translations. - /// - [DataMember(Name = "translations")] - public List Translations { get; private set; } + /// + /// Gets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; private set; } - /// - /// Gets or sets the parent id. - /// - [DataMember(Name = "parentId")] - public new Guid ParentId { get; set; } - } + /// + /// Gets or sets the parent id. + /// + [DataMember(Name = "parentId")] + public new Guid ParentId { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs index 4ad4002b77..afd36b6acc 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs @@ -1,18 +1,17 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// +/// The dictionary translation display model +/// +[DataContract(Name = "dictionaryTranslation", Namespace = "")] +public class DictionaryTranslationDisplay : DictionaryTranslationSave { - /// /// - /// The dictionary translation display model + /// Gets or sets the display name. /// - [DataContract(Name = "dictionaryTranslation", Namespace = "")] - public class DictionaryTranslationDisplay : DictionaryTranslationSave - { - /// - /// Gets or sets the display name. - /// - [DataMember(Name = "displayName")] - public string? DisplayName { get; set; } - } + [DataMember(Name = "displayName")] + public string? DisplayName { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationSave.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationSave.cs index aa42abbf56..cf58bcb2ec 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationSave.cs @@ -1,29 +1,28 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The dictionary translation save model +/// +[DataContract(Name = "dictionaryTranslation", Namespace = "")] +public class DictionaryTranslationSave { /// - /// The dictionary translation save model + /// Gets or sets the ISO code. /// - [DataContract(Name = "dictionaryTranslation", Namespace = "")] - public class DictionaryTranslationSave - { - /// - /// Gets or sets the ISO code. - /// - [DataMember(Name = "isoCode")] - public string? IsoCode { get; set; } + [DataMember(Name = "isoCode")] + public string? IsoCode { get; set; } - /// - /// Gets or sets the translation. - /// - [DataMember(Name = "translation")] - public string Translation { get; set; } = null!; + /// + /// Gets or sets the translation. + /// + [DataMember(Name = "translation")] + public string Translation { get; set; } = null!; - /// - /// Gets or sets the language id. - /// - [DataMember(Name = "languageId")] - public int LanguageId { get; set; } - } + /// + /// Gets or sets the language id. + /// + [DataMember(Name = "languageId")] + public int LanguageId { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs index 6f56c92292..3c292a7e6a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs @@ -1,34 +1,33 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentType", Namespace = "")] +public class DocumentTypeDisplay : ContentTypeCompositionDisplay { - [DataContract(Name = "contentType", Namespace = "")] - public class DocumentTypeDisplay : ContentTypeCompositionDisplay - { - public DocumentTypeDisplay() => - //initialize collections so at least their never null - AllowedTemplates = new List(); + public DocumentTypeDisplay() => - //name, alias, icon, thumb, desc, inherited from the content type + // initialize collections so at least their never null + AllowedTemplates = new List(); - // Templates - [DataMember(Name = "allowedTemplates")] - public IEnumerable AllowedTemplates { get; set; } + // name, alias, icon, thumb, desc, inherited from the content type - [DataMember(Name = "defaultTemplate")] - public EntityBasic? DefaultTemplate { get; set; } + // Templates + [DataMember(Name = "allowedTemplates")] + public IEnumerable AllowedTemplates { get; set; } - [DataMember(Name = "allowCultureVariant")] - public bool AllowCultureVariant { get; set; } + [DataMember(Name = "defaultTemplate")] + public EntityBasic? DefaultTemplate { get; set; } - [DataMember(Name = "allowSegmentVariant")] - public bool AllowSegmentVariant { get; set; } + [DataMember(Name = "allowCultureVariant")] + public bool AllowCultureVariant { get; set; } - [DataMember(Name = "apps")] - public IEnumerable? ContentApps { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } - [DataMember(Name = "historyCleanup")] - public HistoryCleanupViewModel? HistoryCleanup { get; set; } - } + [DataMember(Name = "apps")] + public IEnumerable? ContentApps { get; set; } + + [DataMember(Name = "historyCleanup")] + public HistoryCleanupViewModel? HistoryCleanup { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeSave.cs index 2e509ea5db..af13e88f9b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeSave.cs @@ -1,43 +1,42 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Model used to save a document type +/// +[DataContract(Name = "contentType", Namespace = "")] +public class DocumentTypeSave : ContentTypeSave { /// - /// Model used to save a document type + /// The list of allowed templates to assign (template alias) /// - [DataContract(Name = "contentType", Namespace = "")] - public class DocumentTypeSave : ContentTypeSave + [DataMember(Name = "allowedTemplates")] + public IEnumerable? AllowedTemplates { get; set; } + + /// + /// The default template to assign (template alias) + /// + [DataMember(Name = "defaultTemplate")] + public string? DefaultTemplate { get; set; } + + /// + /// Custom validation + /// + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) { - /// - /// The list of allowed templates to assign (template alias) - /// - [DataMember(Name = "allowedTemplates")] - public IEnumerable? AllowedTemplates { get; set; } - - /// - /// The default template to assign (template alias) - /// - [DataMember(Name = "defaultTemplate")] - public string? DefaultTemplate { get; set; } - - /// - /// Custom validation - /// - /// - /// - public override IEnumerable Validate(ValidationContext validationContext) + if (AllowedTemplates?.Any(x => x.IsNullOrWhiteSpace()) ?? false) { - if (AllowedTemplates?.Any(x => x.IsNullOrWhiteSpace()) ?? false) - yield return new ValidationResult("Template value cannot be null", new[] { "AllowedTemplates" }); + yield return new ValidationResult("Template value cannot be null", new[] { "AllowedTemplates" }); + } - foreach (var v in base.Validate(validationContext)) - { - yield return v; - } + foreach (ValidationResult v in base.Validate(validationContext)) + { + yield return v; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DomainDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DomainDisplay.cs index 573909a610..7a6a584438 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DomainDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DomainDisplay.cs @@ -1,26 +1,25 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "DomainDisplay")] +public class DomainDisplay { - [DataContract(Name = "DomainDisplay")] - public class DomainDisplay + public DomainDisplay(string name, int lang) { - public DomainDisplay(string name, int lang) - { - Name = name; - Lang = lang; - } - - [DataMember(Name = "name")] - public string Name { get; } - - [DataMember(Name = "lang")] - public int Lang { get; } - - [DataMember(Name = "duplicate")] - public bool Duplicate { get; set; } - - [DataMember(Name = "other")] - public string? Other { get; set; } + Name = name; + Lang = lang; } + + [DataMember(Name = "name")] + public string Name { get; } + + [DataMember(Name = "lang")] + public int Lang { get; } + + [DataMember(Name = "duplicate")] + public bool Duplicate { get; set; } + + [DataMember(Name = "other")] + public string? Other { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/DomainSave.cs b/src/Umbraco.Core/Models/ContentEditing/DomainSave.cs index a91e740e79..391616b8dc 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DomainSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DomainSave.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "DomainSave")] +public class DomainSave { - [DataContract(Name = "DomainSave")] - public class DomainSave - { - [DataMember(Name = "valid")] - public bool Valid { get; set; } + [DataMember(Name = "valid")] + public bool Valid { get; set; } - [DataMember(Name = "nodeId")] - public int NodeId { get; set; } + [DataMember(Name = "nodeId")] + public int NodeId { get; set; } - [DataMember(Name = "language")] - public int Language { get; set; } + [DataMember(Name = "language")] + public int Language { get; set; } - [DataMember(Name = "domains")] - public DomainDisplay[]? Domains { get; set; } - } + [DataMember(Name = "domains")] + public DomainDisplay[]? Domains { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/EditorNavigation.cs b/src/Umbraco.Core/Models/ContentEditing/EditorNavigation.cs index 6c8c1b50e3..0920e45f29 100644 --- a/src/Umbraco.Core/Models/ContentEditing/EditorNavigation.cs +++ b/src/Umbraco.Core/Models/ContentEditing/EditorNavigation.cs @@ -1,26 +1,25 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing the navigation ("apps") inside an editor in the back office +/// +[DataContract(Name = "user", Namespace = "")] +public class EditorNavigation { - /// - /// A model representing the navigation ("apps") inside an editor in the back office - /// - [DataContract(Name = "user", Namespace = "")] - public class EditorNavigation - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } + [DataMember(Name = "icon")] + public string? Icon { get; set; } - [DataMember(Name = "view")] - public string? View { get; set; } + [DataMember(Name = "view")] + public string? View { get; set; } - [DataMember(Name = "active")] - public bool Active { get; set; } - } + [DataMember(Name = "active")] + public bool Active { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/EntityBasic.cs b/src/Umbraco.Core/Models/ContentEditing/EntityBasic.cs index 772da930e9..36a837e8e8 100644 --- a/src/Umbraco.Core/Models/ContentEditing/EntityBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/EntityBasic.cs @@ -1,70 +1,68 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Validation; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "entity", Namespace = "")] +public class EntityBasic { - [DataContract(Name = "entity", Namespace = "")] - public class EntityBasic + public EntityBasic() { - public EntityBasic() - { - AdditionalData = new Dictionary(); - Alias = string.Empty; - Path = string.Empty; - } - - [DataMember(Name = "name", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - public string? Name { get; set; } - - [DataMember(Name = "id", IsRequired = true)] - [Required] - public object? Id { get; set; } - - [DataMember(Name = "udi")] - [ReadOnly(true)] - public Udi? Udi { get; set; } - - [DataMember(Name = "icon")] - public string? Icon { get; set; } - - [DataMember(Name = "trashed")] - [ReadOnly(true)] - public bool Trashed { get; set; } - - /// - /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider - /// - [DataMember(Name = "key")] - public Guid Key { get; set; } - - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } - - /// - /// This will only be populated for some entities like macros - /// - /// - /// It is possible to override this to specify different validation attributes if required - /// - [DataMember(Name = "alias")] - public virtual string Alias { get; set; } - - /// - /// The path of the entity - /// - [DataMember(Name = "path")] - public string Path { get; set; } - /// - /// A collection of extra data that is available for this specific entity/entity type - /// - [DataMember(Name = "metaData")] - [ReadOnly(true)] - public IDictionary AdditionalData { get; private set; } + AdditionalData = new Dictionary(); + Alias = string.Empty; + Path = string.Empty; } + + [DataMember(Name = "name", IsRequired = true)] + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + public string? Name { get; set; } + + [DataMember(Name = "id", IsRequired = true)] + [Required] + public object? Id { get; set; } + + [DataMember(Name = "udi")] + [ReadOnly(true)] + public Udi? Udi { get; set; } + + [DataMember(Name = "icon")] + public string? Icon { get; set; } + + [DataMember(Name = "trashed")] + [ReadOnly(true)] + public bool Trashed { get; set; } + + /// + /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider + /// + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } + + /// + /// This will only be populated for some entities like macros + /// + /// + /// It is possible to override this to specify different validation attributes if required + /// + [DataMember(Name = "alias")] + public virtual string Alias { get; set; } + + /// + /// The path of the entity + /// + [DataMember(Name = "path")] + public string Path { get; set; } + + /// + /// A collection of extra data that is available for this specific entity/entity type + /// + [DataMember(Name = "metaData")] + [ReadOnly(true)] + public IDictionary AdditionalData { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/EntitySearchResults.cs b/src/Umbraco.Core/Models/ContentEditing/EntitySearchResults.cs index ff77e3aeb5..f345d881b6 100644 --- a/src/Umbraco.Core/Models/ContentEditing/EntitySearchResults.cs +++ b/src/Umbraco.Core/Models/ContentEditing/EntitySearchResults.cs @@ -1,25 +1,23 @@ using System.Collections; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "searchResults", Namespace = "")] +public class EntitySearchResults : IEnumerable { + private readonly IEnumerable _results; - [DataContract(Name = "searchResults", Namespace = "")] - public class EntitySearchResults : IEnumerable + public EntitySearchResults(IEnumerable results, long totalFound) { - private readonly IEnumerable _results; - - public EntitySearchResults(IEnumerable results, long totalFound) - { - _results = results; - TotalResults = totalFound; - } - - [DataMember(Name = "totalResults")] - public long TotalResults { get; set; } - - public IEnumerator GetEnumerator() => _results.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_results).GetEnumerator(); + _results = results; + TotalResults = totalFound; } + + [DataMember(Name = "totalResults")] + public long TotalResults { get; set; } + + public IEnumerator GetEnumerator() => _results.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_results).GetEnumerator(); } diff --git a/src/Umbraco.Core/Models/ContentEditing/GetAvailableCompositionsFilter.cs b/src/Umbraco.Core/Models/ContentEditing/GetAvailableCompositionsFilter.cs index d73687c039..c3f49d5b7d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/GetAvailableCompositionsFilter.cs +++ b/src/Umbraco.Core/Models/ContentEditing/GetAvailableCompositionsFilter.cs @@ -1,25 +1,27 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public class GetAvailableCompositionsFilter { - public class GetAvailableCompositionsFilter - { - public int ContentTypeId { get; set; } + public int ContentTypeId { get; set; } - /// - /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. - /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot - /// be looked up via the db, they need to be passed in. - /// - public string[]? FilterPropertyTypes { get; set; } + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have + /// these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to + /// it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + public string[]? FilterPropertyTypes { get; set; } - /// - /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out - /// along with any content types that have matching property types that are included in the filtered content types - /// - public string[]? FilterContentTypes { get; set; } + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing + /// those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + public string[]? FilterContentTypes { get; set; } - /// - /// Wether the content type is currently marked as an element type - /// - public bool IsElement { get; set; } - } + /// + /// Wether the content type is currently marked as an element type + /// + public bool IsElement { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs index a0d9bbbcb3..386ca5f12f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs +++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs @@ -1,34 +1,33 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "historyCleanup", Namespace = "")] +public class HistoryCleanup : BeingDirtyBase { - [DataContract(Name = "historyCleanup", Namespace = "")] - public class HistoryCleanup : BeingDirtyBase + private int? _keepAllVersionsNewerThanDays; + private int? _keepLatestVersionPerDayForDays; + private bool _preventCleanup; + + [DataMember(Name = "preventCleanup")] + public bool PreventCleanup { - private bool _preventCleanup; - private int? _keepAllVersionsNewerThanDays; - private int? _keepLatestVersionPerDayForDays; + get => _preventCleanup; + set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup)); + } - [DataMember(Name = "preventCleanup")] - public bool PreventCleanup - { - get => _preventCleanup; - set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup)); - } + [DataMember(Name = "keepAllVersionsNewerThanDays")] + public int? KeepAllVersionsNewerThanDays + { + get => _keepAllVersionsNewerThanDays; + set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays)); + } - [DataMember(Name = "keepAllVersionsNewerThanDays")] - public int? KeepAllVersionsNewerThanDays - { - get => _keepAllVersionsNewerThanDays; - set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays)); - } - - [DataMember(Name = "keepLatestVersionPerDayForDays")] - public int? KeepLatestVersionPerDayForDays - { - get => _keepLatestVersionPerDayForDays; - set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays, nameof(KeepLatestVersionPerDayForDays)); - } + [DataMember(Name = "keepLatestVersionPerDayForDays")] + public int? KeepLatestVersionPerDayForDays + { + get => _keepLatestVersionPerDayForDays; + set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays, nameof(KeepLatestVersionPerDayForDays)); } } diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs index 303ff4eda3..1860dc8feb 100644 --- a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs +++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs @@ -1,18 +1,16 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "historyCleanup", Namespace = "")] +public class HistoryCleanupViewModel : HistoryCleanup { - [DataContract(Name = "historyCleanup", Namespace = "")] - public class HistoryCleanupViewModel : HistoryCleanup - { + [DataMember(Name = "globalEnableCleanup")] + public bool GlobalEnableCleanup { get; set; } - [DataMember(Name = "globalEnableCleanup")] - public bool GlobalEnableCleanup { get; set; } + [DataMember(Name = "globalKeepAllVersionsNewerThanDays")] + public int? GlobalKeepAllVersionsNewerThanDays { get; set; } - [DataMember(Name = "globalKeepAllVersionsNewerThanDays")] - public int? GlobalKeepAllVersionsNewerThanDays { get; set; } - - [DataMember(Name = "globalKeepLatestVersionPerDayForDays")] - public int? GlobalKeepLatestVersionPerDayForDays { get; set; } - } + [DataMember(Name = "globalKeepLatestVersionPerDayForDays")] + public int? GlobalKeepLatestVersionPerDayForDays { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs index fc263a3b91..e0216f66f8 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs @@ -1,23 +1,24 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a content app factory. +/// +public interface IContentAppFactory { /// - /// Represents a content app factory. + /// Gets the content app for an object. /// - public interface IContentAppFactory - { - /// - /// Gets the content app for an object. - /// - /// The source object. - /// The content app for the object, or null. - /// - /// The definition must determine, based on , whether - /// the content app should be displayed or not, and return either a - /// instance, or null. - /// - ContentApp? GetContentAppFor(object source, IEnumerable userGroups); - } + /// The source object. + /// The user groups + /// The content app for the object, or null. + /// + /// + /// The definition must determine, based on , whether + /// the content app should be displayed or not, and return either a + /// instance, or null. + /// + /// + ContentApp? GetContentAppFor(object source, IEnumerable userGroups); } diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentProperties.cs b/src/Umbraco.Core/Models/ContentEditing/IContentProperties.cs index ca8b2439c2..3520c078b1 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentProperties.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentProperties.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.ContentEditing +public interface IContentProperties + where T : ContentPropertyBasic { - - public interface IContentProperties - where T : ContentPropertyBasic - { - IEnumerable Properties { get; } - } + IEnumerable Properties { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs b/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs index dfaf183479..effccf95fa 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs @@ -1,23 +1,23 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// An interface exposes the shared parts of content, media, members that we use during model binding in order to share +/// logic +/// +/// +public interface IContentSave : IHaveUploadedFiles + where TPersisted : IContentBase { /// - /// An interface exposes the shared parts of content, media, members that we use during model binding in order to share logic + /// The action to perform when saving this content item /// - /// - public interface IContentSave : IHaveUploadedFiles - where TPersisted : IContentBase - { - /// - /// The action to perform when saving this content item - /// - ContentSaveAction Action { get; set; } + ContentSaveAction Action { get; set; } - /// - /// The real persisted content object - used during inbound model binding - /// - /// - /// This is not used for outgoing model information. - /// - TPersisted PersistedContent { get; set; } - } + /// + /// The real persisted content object - used during inbound model binding + /// + /// + /// This is not used for outgoing model information. + /// + TPersisted PersistedContent { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IErrorModel.cs b/src/Umbraco.Core/Models/ContentEditing/IErrorModel.cs index 4352771cac..9607146eda 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IErrorModel.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IErrorModel.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.ContentEditing +public interface IErrorModel { - public interface IErrorModel - { - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - IDictionary Errors { get; set; } - } + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// + IDictionary Errors { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs b/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs index a1d4198427..7e467ff124 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public interface IHaveUploadedFiles { - public interface IHaveUploadedFiles - { - List UploadedFiles { get; } - } + List UploadedFiles { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/INotificationModel.cs b/src/Umbraco.Core/Models/ContentEditing/INotificationModel.cs index ac104c0e1b..15b75a82cf 100644 --- a/src/Umbraco.Core/Models/ContentEditing/INotificationModel.cs +++ b/src/Umbraco.Core/Models/ContentEditing/INotificationModel.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public interface INotificationModel { - public interface INotificationModel - { - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - List? Notifications { get; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + List? Notifications { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ITabbedContent.cs b/src/Umbraco.Core/Models/ContentEditing/ITabbedContent.cs index 3f1d847151..13f7375c3d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ITabbedContent.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ITabbedContent.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.ContentEditing +public interface ITabbedContent + where T : ContentPropertyBasic { - - public interface ITabbedContent - where T : ContentPropertyBasic - { - IEnumerable> Tabs { get; } - } + IEnumerable> Tabs { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/Language.cs b/src/Umbraco.Core/Models/ContentEditing/Language.cs index 0a0ed03a2a..15e63eabed 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Language.cs @@ -1,28 +1,27 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "language", Namespace = "")] +public class Language { - [DataContract(Name = "language", Namespace = "")] - public class Language - { - [DataMember(Name = "id")] - public int Id { get; set; } + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "culture", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string IsoCode { get; set; } = null!; + [DataMember(Name = "culture", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string IsoCode { get; set; } = null!; - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "isDefault")] - public bool IsDefault { get; set; } + [DataMember(Name = "isDefault")] + public bool IsDefault { get; set; } - [DataMember(Name = "isMandatory")] - public bool IsMandatory { get; set; } + [DataMember(Name = "isMandatory")] + public bool IsMandatory { get; set; } - [DataMember(Name = "fallbackLanguageId")] - public int? FallbackLanguageId { get; set; } - } + [DataMember(Name = "fallbackLanguageId")] + public int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/LinkDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/LinkDisplay.cs index 551065c566..9b7bde570d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/LinkDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/LinkDisplay.cs @@ -1,32 +1,31 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "link", Namespace = "")] +public class LinkDisplay { - [DataContract(Name = "link", Namespace = "")] - public class LinkDisplay - { - [DataMember(Name = "icon")] - public string? Icon { get; set; } + [DataMember(Name = "icon")] + public string? Icon { get; set; } - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "published")] - public bool Published { get; set; } + [DataMember(Name = "published")] + public bool Published { get; set; } - [DataMember(Name = "queryString")] - public string? QueryString { get; set; } + [DataMember(Name = "queryString")] + public string? QueryString { get; set; } - [DataMember(Name = "target")] - public string? Target { get; set; } + [DataMember(Name = "target")] + public string? Target { get; set; } - [DataMember(Name = "trashed")] - public bool Trashed { get; set; } + [DataMember(Name = "trashed")] + public bool Trashed { get; set; } - [DataMember(Name = "udi")] - public GuidUdi? Udi { get; set; } + [DataMember(Name = "udi")] + public GuidUdi? Udi { get; set; } - [DataMember(Name = "url")] - public string? Url { get; set; } - } + [DataMember(Name = "url")] + public string? Url { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ListViewAwareContentItemDisplayBase.cs b/src/Umbraco.Core/Models/ContentEditing/ListViewAwareContentItemDisplayBase.cs index 729a086864..1add8da7d8 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ListViewAwareContentItemDisplayBase.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ListViewAwareContentItemDisplayBase.cs @@ -1,28 +1,27 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// An abstract model representing a content item that can be contained in a list view +/// +/// +public abstract class ListViewAwareContentItemDisplayBase : ContentItemDisplayBase + where T : ContentPropertyBasic { /// - /// An abstract model representing a content item that can be contained in a list view + /// Property indicating if this item is part of a list view parent /// - /// - public abstract class ListViewAwareContentItemDisplayBase : ContentItemDisplayBase - where T : ContentPropertyBasic - { - /// - /// Property indicating if this item is part of a list view parent - /// - [DataMember(Name = "isChildOfListView")] - public bool IsChildOfListView { get; set; } + [DataMember(Name = "isChildOfListView")] + public bool IsChildOfListView { get; set; } - /// - /// Property for the entity's individual tree node URL - /// - /// - /// This is required if the item is a child of a list view since the tree won't actually be loaded, - /// so the app will need to go fetch the individual tree node in order to be able to load it's action list (menu) - /// - [DataMember(Name = "treeNodeUrl")] - public string? TreeNodeUrl { get; set; } - } + /// + /// Property for the entity's individual tree node URL + /// + /// + /// This is required if the item is a child of a list view since the tree won't actually be loaded, + /// so the app will need to go fetch the individual tree node in order to be able to load it's action list (menu) + /// + [DataMember(Name = "treeNodeUrl")] + public string? TreeNodeUrl { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MacroDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MacroDisplay.cs index f794143aab..9919004a50 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MacroDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MacroDisplay.cs @@ -1,67 +1,65 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The macro display model +/// +[DataContract(Name = "dictionary", Namespace = "")] +public class MacroDisplay : EntityBasic, INotificationModel { /// - /// The macro display model + /// Initializes a new instance of the class. /// - [DataContract(Name = "dictionary", Namespace = "")] - public class MacroDisplay : EntityBasic, INotificationModel + public MacroDisplay() { - /// - /// Initializes a new instance of the class. - /// - public MacroDisplay() - { - Notifications = new List(); - Parameters = new List(); - } - - /// - [DataMember(Name = "notifications")] - public List Notifications { get; } - - /// - /// Gets or sets a value indicating whether the macro can be used in a rich text editor. - /// - [DataMember(Name = "useInEditor")] - public bool UseInEditor { get; set; } - - /// - /// Gets or sets a value indicating whether the macro should be rendered a rich text editor. - /// - [DataMember(Name = "renderInEditor")] - public bool RenderInEditor { get; set; } - - /// - /// Gets or sets the cache period. - /// - [DataMember(Name = "cachePeriod")] - public int CachePeriod { get; set; } - - /// - /// Gets or sets a value indicating whether the macro should be cached by page - /// - [DataMember(Name = "cacheByPage")] - public bool CacheByPage { get; set; } - - /// - /// Gets or sets a value indicating whether the macro should be cached by user - /// - [DataMember(Name = "cacheByUser")] - public bool CacheByUser { get; set; } - - /// - /// Gets or sets the view. - /// - [DataMember(Name = "view")] - public string View { get; set; } = null!; - - /// - /// Gets or sets the parameters. - /// - [DataMember(Name = "parameters")] - public IEnumerable Parameters { get; set; } + Notifications = new List(); + Parameters = new List(); } + + /// + /// Gets or sets a value indicating whether the macro can be used in a rich text editor. + /// + [DataMember(Name = "useInEditor")] + public bool UseInEditor { get; set; } + + /// + /// Gets or sets a value indicating whether the macro should be rendered a rich text editor. + /// + [DataMember(Name = "renderInEditor")] + public bool RenderInEditor { get; set; } + + /// + /// Gets or sets the cache period. + /// + [DataMember(Name = "cachePeriod")] + public int CachePeriod { get; set; } + + /// + /// Gets or sets a value indicating whether the macro should be cached by page + /// + [DataMember(Name = "cacheByPage")] + public bool CacheByPage { get; set; } + + /// + /// Gets or sets a value indicating whether the macro should be cached by user + /// + [DataMember(Name = "cacheByUser")] + public bool CacheByUser { get; set; } + + /// + /// Gets or sets the view. + /// + [DataMember(Name = "view")] + public string View { get; set; } = null!; + + /// + /// Gets or sets the parameters. + /// + [DataMember(Name = "parameters")] + public IEnumerable Parameters { get; set; } + + /// + [DataMember(Name = "notifications")] + public List Notifications { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MacroParameter.cs b/src/Umbraco.Core/Models/ContentEditing/MacroParameter.cs index 233a58cd08..3db1cd5820 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MacroParameter.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MacroParameter.cs @@ -1,43 +1,41 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a macro parameter with an editor +/// +[DataContract(Name = "macroParameter", Namespace = "")] +public class MacroParameter { + [DataMember(Name = "alias", IsRequired = true)] + [Required] + public string Alias { get; set; } = null!; + + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + /// - /// Represents a macro parameter with an editor + /// The editor view to render for this parameter /// - [DataContract(Name = "macroParameter", Namespace = "")] - public class MacroParameter - { - [DataMember(Name = "alias", IsRequired = true)] - [Required] - public string Alias { get; set; } = null!; + [DataMember(Name = "view", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string? View { get; set; } - [DataMember(Name = "name")] - public string? Name { get; set; } + /// + /// The configuration for this parameter editor + /// + [DataMember(Name = "config", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public IDictionary? Configuration { get; set; } - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - - /// - /// The editor view to render for this parameter - /// - [DataMember(Name = "view", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public string? View { get; set; } - - /// - /// The configuration for this parameter editor - /// - [DataMember(Name = "config", IsRequired = true)] - [Required(AllowEmptyStrings = false)] - public IDictionary? Configuration { get; set; } - - /// - /// Since we don't post this back this isn't currently really used on the server side - /// - [DataMember(Name = "value")] - public object? Value { get; set; } - } + /// + /// Since we don't post this back this isn't currently really used on the server side + /// + [DataMember(Name = "value")] + public object? Value { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MacroParameterDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MacroParameterDisplay.cs index 8cd630d66f..3a532fcc12 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MacroParameterDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MacroParameterDisplay.cs @@ -1,35 +1,34 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The macro parameter display. +/// +[DataContract(Name = "parameter", Namespace = "")] +public class MacroParameterDisplay { /// - /// The macro parameter display. + /// Gets or sets the key. /// - [DataContract(Name = "parameter", Namespace = "")] - public class MacroParameterDisplay - { - /// - /// Gets or sets the key. - /// - [DataMember(Name = "key")] - public string Key { get; set; } = null!; + [DataMember(Name = "key")] + public string Key { get; set; } = null!; - /// - /// Gets or sets the label. - /// - [DataMember(Name = "label")] - public string? Label { get; set; } + /// + /// Gets or sets the label. + /// + [DataMember(Name = "label")] + public string? Label { get; set; } - /// - /// Gets or sets the editor. - /// - [DataMember(Name = "editor")] - public string Editor { get; set; } = null!; + /// + /// Gets or sets the editor. + /// + [DataMember(Name = "editor")] + public string Editor { get; set; } = null!; - /// - /// Gets or sets the id. - /// - [DataMember(Name = "id")] - public int Id { get; set; } - } + /// + /// Gets or sets the id. + /// + [DataMember(Name = "id")] + public int Id { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MediaItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MediaItemDisplay.cs index a56911f707..784e5510fb 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MediaItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MediaItemDisplay.cs @@ -1,26 +1,21 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a media item to be displayed in the back office +/// +[DataContract(Name = "content", Namespace = "")] +public class MediaItemDisplay : ListViewAwareContentItemDisplayBase { - /// - /// A model representing a media item to be displayed in the back office - /// - [DataContract(Name = "content", Namespace = "")] - public class MediaItemDisplay : ListViewAwareContentItemDisplayBase - { - public MediaItemDisplay() - { - ContentApps = new List(); - } + public MediaItemDisplay() => ContentApps = new List(); - [DataMember(Name = "contentType")] - public ContentTypeBasic? ContentType { get; set; } + [DataMember(Name = "contentType")] + public ContentTypeBasic? ContentType { get; set; } - [DataMember(Name = "mediaLink")] - public string? MediaLink { get; set; } + [DataMember(Name = "mediaLink")] + public string? MediaLink { get; set; } - [DataMember(Name = "apps")] - public IEnumerable ContentApps { get; set; } - } + [DataMember(Name = "apps")] + public IEnumerable ContentApps { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MediaItemSave.cs b/src/Umbraco.Core/Models/ContentEditing/MediaItemSave.cs index 06c201ab67..7bac43b25d 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MediaItemSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MediaItemSave.cs @@ -1,12 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a media item to be saved +/// +[DataContract(Name = "content", Namespace = "")] +public class MediaItemSave : ContentBaseSave { - /// - /// A model representing a media item to be saved - /// - [DataContract(Name = "content", Namespace = "")] - public class MediaItemSave : ContentBaseSave - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MediaTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MediaTypeDisplay.cs index 2c7c50550d..899be95040 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MediaTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MediaTypeDisplay.cs @@ -1,11 +1,10 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentType", Namespace = "")] +public class MediaTypeDisplay : ContentTypeCompositionDisplay { - [DataContract(Name = "contentType", Namespace = "")] - public class MediaTypeDisplay : ContentTypeCompositionDisplay - { - [DataMember(Name = "isSystemMediaType")] - public bool IsSystemMediaType { get; set; } - } + [DataMember(Name = "isSystemMediaType")] + public bool IsSystemMediaType { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MediaTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/MediaTypeSave.cs index 1ef2a1988b..b3fdeea1e2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MediaTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MediaTypeSave.cs @@ -1,12 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Model used to save a media type +/// +[DataContract(Name = "contentType", Namespace = "")] +public class MediaTypeSave : ContentTypeSave { - /// - /// Model used to save a media type - /// - [DataContract(Name = "contentType", Namespace = "")] - public class MediaTypeSave : ContentTypeSave - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberBasic.cs b/src/Umbraco.Core/Models/ContentEditing/MemberBasic.cs index d148d88921..7ef1ce5f72 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberBasic.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Used for basic member information +/// +public class MemberBasic : ContentItemBasic { - /// - /// Used for basic member information - /// - public class MemberBasic : ContentItemBasic + [DataMember(Name = "username")] + public string? Username { get; set; } + + [DataMember(Name = "email")] + public string? Email { get; set; } + + [DataMember(Name = "properties")] + public override IEnumerable Properties { - [DataMember(Name = "username")] - public string? Username { get; set; } - - [DataMember(Name = "email")] - public string? Email { get; set; } - - [DataMember(Name = "properties")] - public override IEnumerable Properties - { - get => base.Properties; - set => base.Properties = value; - } + get => base.Properties; + set => base.Properties = value; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs index 5448c40b1e..161c085d35 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberDisplay.cs @@ -1,51 +1,46 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a member to be displayed in the back office +/// +[DataContract(Name = "content", Namespace = "")] +public class MemberDisplay : ListViewAwareContentItemDisplayBase { - /// - /// A model representing a member to be displayed in the back office - /// - [DataContract(Name = "content", Namespace = "")] - public class MemberDisplay : ListViewAwareContentItemDisplayBase - { - public MemberDisplay() - { - // MemberProviderFieldMapping = new Dictionary(); - ContentApps = new List(); - } + public MemberDisplay() => - [DataMember(Name = "contentType")] - public ContentTypeBasic? ContentType { get; set; } + // MemberProviderFieldMapping = new Dictionary(); + ContentApps = new List(); - [DataMember(Name = "username")] - public string? Username { get; set; } + [DataMember(Name = "contentType")] + public ContentTypeBasic? ContentType { get; set; } - [DataMember(Name = "email")] - public string? Email { get; set; } + [DataMember(Name = "username")] + public string? Username { get; set; } - [DataMember(Name = "isLockedOut")] - public bool IsLockedOut { get; set; } + [DataMember(Name = "email")] + public string? Email { get; set; } - [DataMember(Name = "isApproved")] - public bool IsApproved { get; set; } + [DataMember(Name = "isLockedOut")] + public bool IsLockedOut { get; set; } - //[DataMember(Name = "membershipScenario")] - //public MembershipScenario MembershipScenario { get; set; } + [DataMember(Name = "isApproved")] + public bool IsApproved { get; set; } - // /// - // /// This is used to indicate how to map the membership provider properties to the save model, this mapping - // /// will change if a developer has opted to have custom member property aliases specified in their membership provider config, - // /// or if we are editing a member that is not an Umbraco member (custom provider) - // /// - // [DataMember(Name = "fieldConfig")] - // public IDictionary MemberProviderFieldMapping { get; set; } + // [DataMember(Name = "membershipScenario")] + // public MembershipScenario MembershipScenario { get; set; } - [DataMember(Name = "apps")] - public IEnumerable ContentApps { get; set; } + // /// + // /// This is used to indicate how to map the membership provider properties to the save model, this mapping + // /// will change if a developer has opted to have custom member property aliases specified in their membership provider config, + // /// or if we are editing a member that is not an Umbraco member (custom provider) + // /// + // [DataMember(Name = "fieldConfig")] + // public IDictionary MemberProviderFieldMapping { get; set; } + [DataMember(Name = "apps")] + public IEnumerable ContentApps { get; set; } - - [DataMember(Name = "membershipProperties")] - public IEnumerable? MembershipProperties { get; set; } - } + [DataMember(Name = "membershipProperties")] + public IEnumerable? MembershipProperties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs index 2d930727aa..0804fd53d7 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberGroupDisplay.cs @@ -1,20 +1,15 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - [DataContract(Name = "memberGroup", Namespace = "")] - public class MemberGroupDisplay : EntityBasic, INotificationModel - { - public MemberGroupDisplay() - { - Notifications = new List(); - } +namespace Umbraco.Cms.Core.Models.ContentEditing; - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - } +[DataContract(Name = "memberGroup", Namespace = "")] +public class MemberGroupDisplay : EntityBasic, INotificationModel +{ + public MemberGroupDisplay() => Notifications = new List(); + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberGroupSave.cs b/src/Umbraco.Core/Models/ContentEditing/MemberGroupSave.cs index 2b863a758d..292d410625 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberGroupSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberGroupSave.cs @@ -1,9 +1,8 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "memberGroup", Namespace = "")] +public class MemberGroupSave : EntityBasic { - [DataContract(Name = "memberGroup", Namespace = "")] - public class MemberGroupSave : EntityBasic - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberListDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberListDisplay.cs index c4a5382e84..cd89f46fc6 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberListDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberListDisplay.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a member list to be displayed in the back office +/// +[DataContract(Name = "content", Namespace = "")] +public class MemberListDisplay : ContentItemDisplayBase { - /// - /// A model representing a member list to be displayed in the back office - /// - [DataContract(Name = "content", Namespace = "")] - public class MemberListDisplay : ContentItemDisplayBase - { - [DataMember(Name = "apps")] - public IEnumerable? ContentApps { get; set; } - } + [DataMember(Name = "apps")] + public IEnumerable? ContentApps { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeBasic.cs index b25f2ae5c8..9ef0ebf3b9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeBasic.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Basic member property type +/// +[DataContract(Name = "contentType", Namespace = "")] +public class MemberPropertyTypeBasic : PropertyTypeBasic { - /// - /// Basic member property type - /// - [DataContract(Name = "contentType", Namespace = "")] - public class MemberPropertyTypeBasic : PropertyTypeBasic - { - [DataMember(Name = "showOnMemberProfile")] - public bool MemberCanViewProperty { get; set; } + [DataMember(Name = "showOnMemberProfile")] + public bool MemberCanViewProperty { get; set; } - [DataMember(Name = "memberCanEdit")] - public bool MemberCanEditProperty { get; set; } + [DataMember(Name = "memberCanEdit")] + public bool MemberCanEditProperty { get; set; } - [DataMember(Name = "isSensitiveData")] - public bool IsSensitiveData { get; set; } - } + [DataMember(Name = "isSensitiveData")] + public bool IsSensitiveData { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeDisplay.cs index 873883c8db..1038440974 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberPropertyTypeDisplay.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyType")] +public class MemberPropertyTypeDisplay : PropertyTypeDisplay { - [DataContract(Name = "propertyType")] - public class MemberPropertyTypeDisplay : PropertyTypeDisplay - { - [DataMember(Name = "showOnMemberProfile")] - public bool MemberCanViewProperty { get; set; } + [DataMember(Name = "showOnMemberProfile")] + public bool MemberCanViewProperty { get; set; } - [DataMember(Name = "memberCanEdit")] - public bool MemberCanEditProperty { get; set; } + [DataMember(Name = "memberCanEdit")] + public bool MemberCanEditProperty { get; set; } - [DataMember(Name = "isSensitiveData")] - public bool IsSensitiveData { get; set; } - } + [DataMember(Name = "isSensitiveData")] + public bool IsSensitiveData { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs b/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs index 903c87341a..2963618e1b 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberSave.cs @@ -1,48 +1,48 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Validation; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +public class MemberSave : ContentBaseSave { - /// - public class MemberSave : ContentBaseSave + [DataMember(Name = "username", IsRequired = true)] + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + public string Username { get; set; } = null!; + + [DataMember(Name = "email", IsRequired = true)] + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + [EmailAddress] + public string Email { get; set; } = null!; + + [DataMember(Name = "password")] + public ChangingPasswordModel? Password { get; set; } + + [DataMember(Name = "memberGroups")] + public IEnumerable? Groups { get; set; } + + /// + /// Returns the value from the Comments property + /// + public string? Comments => GetPropertyValue(Constants.Conventions.Member.Comments); + + [DataMember(Name = "isLockedOut")] + public bool IsLockedOut { get; set; } + + [DataMember(Name = "isApproved")] + public bool IsApproved { get; set; } + + private T? GetPropertyValue(string alias) { - - [DataMember(Name = "username", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - public string Username { get; set; } = null!; - - [DataMember(Name = "email", IsRequired = true)] - [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] - [EmailAddress] - public string Email { get; set; } = null!; - - [DataMember(Name = "password")] - public ChangingPasswordModel? Password { get; set; } - - [DataMember(Name = "memberGroups")] - public IEnumerable? Groups { get; set; } - - /// - /// Returns the value from the Comments property - /// - public string? Comments => GetPropertyValue(Constants.Conventions.Member.Comments); - - [DataMember(Name = "isLockedOut")] - public bool IsLockedOut { get; set; } - - [DataMember(Name = "isApproved")] - public bool IsApproved { get; set; } - - private T? GetPropertyValue(string alias) + ContentPropertyBasic? prop = Properties.FirstOrDefault(x => x.Alias == alias); + if (prop == null) { - var prop = Properties.FirstOrDefault(x => x.Alias == alias); - if (prop == null) return default; - var converted = prop.Value.TryConvertTo(); - return converted.Result ?? default; + return default; } + + Attempt converted = prop.Value.TryConvertTo(); + return converted.Result ?? default; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/MemberTypeDisplay.cs index 67e390f378..ea8aa5c1e3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberTypeDisplay.cs @@ -1,9 +1,8 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "contentType", Namespace = "")] +public class MemberTypeDisplay : ContentTypeCompositionDisplay { - [DataContract(Name = "contentType", Namespace = "")] - public class MemberTypeDisplay : ContentTypeCompositionDisplay - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MemberTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/MemberTypeSave.cs index 80ac46ae09..59a6047494 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MemberTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MemberTypeSave.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Model used to save a member type +/// +public class MemberTypeSave : ContentTypeSave { - /// - /// Model used to save a member type - /// - public class MemberTypeSave : ContentTypeSave - { - } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MessagesExtensions.cs b/src/Umbraco.Core/Models/ContentEditing/MessagesExtensions.cs index 5a93ae94c9..5a65111345 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MessagesExtensions.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MessagesExtensions.cs @@ -1,70 +1,80 @@ -using System.Linq; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class MessagesExtensions { - public static class MessagesExtensions + public static void AddNotification(this INotificationModel model, string header, string msg, NotificationStyle type) { - public static void AddNotification(this INotificationModel model, string header, string msg, NotificationStyle type) + if (model.Exists(header, msg, type)) { - if (model.Exists(header, msg, type)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = type - }); + return; } - public static void AddSuccessNotification(this INotificationModel model, string header, string msg) - { - if (model.Exists(header, msg, NotificationStyle.Success)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Success - }); - } - - public static void AddErrorNotification(this INotificationModel model, string? header, string msg) - { - if (model.Exists(header, msg, NotificationStyle.Error)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Error - }); - } - - public static void AddWarningNotification(this INotificationModel model, string header, string msg) - { - if (model.Exists(header, msg, NotificationStyle.Warning)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Warning - }); - } - - public static void AddInfoNotification(this INotificationModel model, string header, string msg) - { - if (model.Exists(header, msg, NotificationStyle.Info)) return; - - model.Notifications?.Add(new BackOfficeNotification() - { - Header = header, - Message = msg, - NotificationType = NotificationStyle.Info - }); - } - - private static bool Exists(this INotificationModel model, string? header, string message, NotificationStyle notificationType) => model.Notifications?.Any(x => (x.Header?.InvariantEquals(header) ?? false) && (x.Message?.InvariantEquals(message) ?? false) && x.NotificationType == notificationType) ?? false; + model.Notifications?.Add(new BackOfficeNotification { Header = header, Message = msg, NotificationType = type }); } + + public static void AddSuccessNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Success)) + { + return; + } + + model.Notifications?.Add(new BackOfficeNotification + { + Header = header, + Message = msg, + NotificationType = NotificationStyle.Success, + }); + } + + public static void AddErrorNotification(this INotificationModel model, string? header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Error)) + { + return; + } + + model.Notifications?.Add(new BackOfficeNotification + { + Header = header, + Message = msg, + NotificationType = NotificationStyle.Error, + }); + } + + public static void AddWarningNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Warning)) + { + return; + } + + model.Notifications?.Add(new BackOfficeNotification + { + Header = header, + Message = msg, + NotificationType = NotificationStyle.Warning, + }); + } + + public static void AddInfoNotification(this INotificationModel model, string header, string msg) + { + if (model.Exists(header, msg, NotificationStyle.Info)) + { + return; + } + + model.Notifications?.Add(new BackOfficeNotification + { + Header = header, + Message = msg, + NotificationType = NotificationStyle.Info, + }); + } + + private static bool Exists(this INotificationModel model, string? header, string message, NotificationStyle notificationType) => model.Notifications?.Any(x => + (x.Header?.InvariantEquals(header) ?? false) && (x.Message?.InvariantEquals(message) ?? false) && + x.NotificationType == notificationType) ?? false; } diff --git a/src/Umbraco.Core/Models/ContentEditing/ModelWithNotifications.cs b/src/Umbraco.Core/Models/ContentEditing/ModelWithNotifications.cs index d79be81725..56275bfb6c 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ModelWithNotifications.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ModelWithNotifications.cs @@ -1,31 +1,30 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A generic model supporting notifications, this is useful for returning any model type to include notifications from +/// api controllers +/// +/// +[DataContract(Name = "model", Namespace = "")] +public class ModelWithNotifications : INotificationModel { - /// - /// A generic model supporting notifications, this is useful for returning any model type to include notifications from api controllers - /// - /// - [DataContract(Name = "model", Namespace = "")] - public class ModelWithNotifications : INotificationModel + public ModelWithNotifications(T value) { - public ModelWithNotifications(T value) - { - Value = value; - Notifications = new List(); - } - - /// - /// The generic value - /// - [DataMember(Name = "value")] - public T Value { get; private set; } - - /// - /// The notifications - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + Value = value; + Notifications = new List(); } + + /// + /// The generic value + /// + [DataMember(Name = "value")] + public T Value { get; private set; } + + /// + /// The notifications + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/MoveOrCopy.cs b/src/Umbraco.Core/Models/ContentEditing/MoveOrCopy.cs index c27cf70ccf..ecbcc027f4 100644 --- a/src/Umbraco.Core/Models/ContentEditing/MoveOrCopy.cs +++ b/src/Umbraco.Core/Models/ContentEditing/MoveOrCopy.cs @@ -1,41 +1,39 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A model representing a model for moving or copying +/// +[DataContract(Name = "content", Namespace = "")] +public class MoveOrCopy { /// - /// A model representing a model for moving or copying + /// The Id of the node to move or copy to /// - [DataContract(Name = "content", Namespace = "")] - public class MoveOrCopy - { - /// - /// The Id of the node to move or copy to - /// - [DataMember(Name = "parentId", IsRequired = true)] - [Required] - public int ParentId { get; set; } + [DataMember(Name = "parentId", IsRequired = true)] + [Required] + public int ParentId { get; set; } - /// - /// The id of the node to move or copy - /// - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int Id { get; set; } + /// + /// The id of the node to move or copy + /// + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } - /// - /// Boolean indicating whether copying the object should create a relation to it's original - /// - [DataMember(Name = "relateToOriginal", IsRequired = true)] - [Required] - public bool RelateToOriginal { get; set; } - - /// - /// Boolean indicating whether copying the object should be recursive - /// - [DataMember(Name = "recursive", IsRequired = true)] - [Required] - public bool Recursive { get; set; } - } + /// + /// Boolean indicating whether copying the object should create a relation to it's original + /// + [DataMember(Name = "relateToOriginal", IsRequired = true)] + [Required] + public bool RelateToOriginal { get; set; } + /// + /// Boolean indicating whether copying the object should be recursive + /// + [DataMember(Name = "recursive", IsRequired = true)] + [Required] + public bool Recursive { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/NotificationStyle.cs b/src/Umbraco.Core/Models/ContentEditing/NotificationStyle.cs index a8c17d1850..1fe9e9b525 100644 --- a/src/Umbraco.Core/Models/ContentEditing/NotificationStyle.cs +++ b/src/Umbraco.Core/Models/ContentEditing/NotificationStyle.cs @@ -1,29 +1,32 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract] +public enum NotificationStyle { - [DataContract] - public enum NotificationStyle - { - /// - /// Save icon - /// - Save = 0, - /// - /// Info icon - /// - Info = 1, - /// - /// Error icon - /// - Error = 2, - /// - /// Success icon - /// - Success = 3, - /// - /// Warning icon - /// - Warning = 4 - } + /// + /// Save icon + /// + Save = 0, + + /// + /// Info icon + /// + Info = 1, + + /// + /// Error icon + /// + Error = 2, + + /// + /// Success icon + /// + Success = 3, + + /// + /// Warning icon + /// + Warning = 4, } diff --git a/src/Umbraco.Core/Models/ContentEditing/NotifySetting.cs b/src/Umbraco.Core/Models/ContentEditing/NotifySetting.cs index ee4029cab3..603ec953b0 100644 --- a/src/Umbraco.Core/Models/ContentEditing/NotifySetting.cs +++ b/src/Umbraco.Core/Models/ContentEditing/NotifySetting.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "notifySetting", Namespace = "")] +public class NotifySetting { - [DataContract(Name = "notifySetting", Namespace = "")] - public class NotifySetting - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "checked")] - public bool Checked { get; set; } + [DataMember(Name = "checked")] + public bool Checked { get; set; } - /// - /// The letter from the IAction - /// - [DataMember(Name = "notifyCode")] - public string? NotifyCode { get; set; } - } + /// + /// The letter from the IAction + /// + [DataMember(Name = "notifyCode")] + public string? NotifyCode { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ObjectType.cs b/src/Umbraco.Core/Models/ContentEditing/ObjectType.cs index 4682b752b9..c2f69218b3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ObjectType.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ObjectType.cs @@ -1,15 +1,13 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - [DataContract(Name = "objectType", Namespace = "")] - public class ObjectType - { - [DataMember(Name = "name")] - public string? Name { get; set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "id")] - public Guid Id { get; set; } - } +[DataContract(Name = "objectType", Namespace = "")] +public class ObjectType +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "id")] + public Guid Id { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/Permission.cs b/src/Umbraco.Core/Models/ContentEditing/Permission.cs index c6e446fc39..9bdb664579 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Permission.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Permission.cs @@ -1,38 +1,33 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "permission", Namespace = "")] +public class Permission : ICloneable { - [DataContract(Name = "permission", Namespace = "")] - public class Permission : ICloneable - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "description")] - public string? Description { get; set; } + [DataMember(Name = "description")] + public string? Description { get; set; } - [DataMember(Name = "checked")] - public bool Checked { get; set; } + [DataMember(Name = "checked")] + public bool Checked { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } + [DataMember(Name = "icon")] + public string? Icon { get; set; } - /// - /// We'll use this to map the categories but it wont' be returned in the json - /// - [IgnoreDataMember] - public string Category { get; set; } = null!; + /// + /// We'll use this to map the categories but it wont' be returned in the json + /// + [IgnoreDataMember] + public string Category { get; set; } = null!; - /// - /// The letter from the IAction - /// - [DataMember(Name = "permissionCode")] - public string? PermissionCode { get; set; } + /// + /// The letter from the IAction + /// + [DataMember(Name = "permissionCode")] + public string? PermissionCode { get; set; } - public object Clone() - { - return this.MemberwiseClone(); - } - } + public object Clone() => MemberwiseClone(); } diff --git a/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs b/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs index 69029c961a..5d71369141 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs @@ -1,24 +1,23 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - /// - /// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the - /// temporary files that were created. - /// - [DataContract] - public class PostedFiles : IHaveUploadedFiles, INotificationModel - { - public PostedFiles() - { - UploadedFiles = new List(); - Notifications = new List(); - } - public List UploadedFiles { get; private set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } +/// +/// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the +/// temporary files that were created. +/// +[DataContract] +public class PostedFiles : IHaveUploadedFiles, INotificationModel +{ + public PostedFiles() + { + UploadedFiles = new List(); + Notifications = new List(); } + + public List UploadedFiles { get; } + + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PostedFolder.cs b/src/Umbraco.Core/Models/ContentEditing/PostedFolder.cs index 56ca1c1907..79769559db 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PostedFolder.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PostedFolder.cs @@ -1,17 +1,16 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing -{ - /// - /// Used to create a folder with the MediaController - /// - [DataContract] - public class PostedFolder - { - [DataMember(Name = "parentId")] - public string? ParentId { get; set; } +namespace Umbraco.Cms.Core.Models.ContentEditing; - [DataMember(Name = "name")] - public string? Name { get; set; } - } +/// +/// Used to create a folder with the MediaController +/// +[DataContract] +public class PostedFolder +{ + [DataMember(Name = "parentId")] + public string? ParentId { get; set; } + + [DataMember(Name = "name")] + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyEditorBasic.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyEditorBasic.cs index b73f2897e7..498537cf1e 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyEditorBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyEditorBasic.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Defines an available property editor to be able to select for a data type +/// +[DataContract(Name = "propertyEditor", Namespace = "")] +public class PropertyEditorBasic { - /// - /// Defines an available property editor to be able to select for a data type - /// - [DataContract(Name = "propertyEditor", Namespace = "")] - public class PropertyEditorBasic - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "icon")] - public string? Icon { get; set; } - } + [DataMember(Name = "icon")] + public string? Icon { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs index 0431fb270f..5b45776a8e 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs @@ -1,66 +1,62 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyGroup", Namespace = "")] +public abstract class PropertyGroupBasic { - [DataContract(Name = "propertyGroup", Namespace = "")] - public abstract class PropertyGroupBasic - { - /// - /// Gets the special generic properties tab identifier. - /// - public const int GenericPropertiesGroupId = -666; + /// + /// Gets the special generic properties tab identifier. + /// + public const int GenericPropertiesGroupId = -666; - /// - /// Gets a value indicating whether this tab is the generic properties tab. - /// - [IgnoreDataMember] - public bool IsGenericProperties => Id == GenericPropertiesGroupId; + /// + /// Gets a value indicating whether this tab is the generic properties tab. + /// + [IgnoreDataMember] + public bool IsGenericProperties => Id == GenericPropertiesGroupId; - /// - /// Gets a value indicating whether the property group is inherited through - /// content types composition. - /// - /// A property group can be inherited and defined on the content type - /// currently being edited, at the same time. Inherited is true when there exists at least - /// one property group higher in the composition, with the same alias. - [DataMember(Name = "inherited")] - public bool Inherited { get; set; } + /// + /// Gets a value indicating whether the property group is inherited through + /// content types composition. + /// + /// + /// A property group can be inherited and defined on the content type + /// currently being edited, at the same time. Inherited is true when there exists at least + /// one property group higher in the composition, with the same alias. + /// + [DataMember(Name = "inherited")] + public bool Inherited { get; set; } - // needed - so we can handle alias renames - [DataMember(Name = "id")] - public int Id { get; set; } + // needed - so we can handle alias renames + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "key")] - public Guid Key { get; set; } + [DataMember(Name = "key")] + public Guid Key { get; set; } - [DataMember(Name = "type")] - public PropertyGroupType Type { get; set; } + [DataMember(Name = "type")] + public PropertyGroupType Type { get; set; } - [Required] - [DataMember(Name = "name")] - public string? Name { get; set; } + [Required] + [DataMember(Name = "name")] + public string? Name { get; set; } - [Required] - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; + [Required] + [DataMember(Name = "alias")] + public string Alias { get; set; } = null!; - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - } - - [DataContract(Name = "propertyGroup", Namespace = "")] - public class PropertyGroupBasic : PropertyGroupBasic - where TPropertyType: PropertyTypeBasic - { - public PropertyGroupBasic() - { - Properties = new List(); - } - - [DataMember(Name = "properties")] - public IEnumerable Properties { get; set; } - } + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } +} + +[DataContract(Name = "propertyGroup", Namespace = "")] +public class PropertyGroupBasic : PropertyGroupBasic + where TPropertyType : PropertyTypeBasic +{ + public PropertyGroupBasic() => Properties = new List(); + + [DataMember(Name = "properties")] + public IEnumerable Properties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasicExtensions.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasicExtensions.cs index 6f1317f3eb..4e3b530f99 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasicExtensions.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasicExtensions.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +internal static class PropertyGroupBasicExtensions { - internal static class PropertyGroupBasicExtensions - { - public static string? GetParentAlias(this PropertyGroupBasic propertyGroup) - => PropertyGroupExtensions.GetParentAlias(propertyGroup.Alias); - } + public static string? GetParentAlias(this PropertyGroupBasic propertyGroup) + => PropertyGroupExtensions.GetParentAlias(propertyGroup.Alias); } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupDisplay.cs index a543d85347..67a200cf65 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupDisplay.cs @@ -1,39 +1,37 @@ -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyGroup", Namespace = "")] +public class PropertyGroupDisplay : PropertyGroupBasic + where TPropertyTypeDisplay : PropertyTypeDisplay { - [DataContract(Name = "propertyGroup", Namespace = "")] - public class PropertyGroupDisplay : PropertyGroupBasic - where TPropertyTypeDisplay : PropertyTypeDisplay + public PropertyGroupDisplay() { - public PropertyGroupDisplay() - { - Properties = new List(); - ParentTabContentTypeNames = new List(); - ParentTabContentTypes = new List(); - } - - /// - /// Gets the context content type. - /// - [DataMember(Name = "contentTypeId")] - [ReadOnly(true)] - public int ContentTypeId { get; set; } - - /// - /// Gets the identifiers of the content types that define this group. - /// - [DataMember(Name = "parentTabContentTypes")] - [ReadOnly(true)] - public IEnumerable ParentTabContentTypes { get; set; } - - /// - /// Gets the name of the content types that define this group. - /// - [DataMember(Name = "parentTabContentTypeNames")] - [ReadOnly(true)] - public IEnumerable ParentTabContentTypeNames { get; set; } + Properties = new List(); + ParentTabContentTypeNames = new List(); + ParentTabContentTypes = new List(); } + + /// + /// Gets the context content type. + /// + [DataMember(Name = "contentTypeId")] + [ReadOnly(true)] + public int ContentTypeId { get; set; } + + /// + /// Gets the identifiers of the content types that define this group. + /// + [DataMember(Name = "parentTabContentTypes")] + [ReadOnly(true)] + public IEnumerable ParentTabContentTypes { get; set; } + + /// + /// Gets the name of the content types that define this group. + /// + [DataMember(Name = "parentTabContentTypeNames")] + [ReadOnly(true)] + public IEnumerable ParentTabContentTypeNames { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs index 0aded31a18..4574e62cde 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeBasic.cs @@ -1,72 +1,72 @@ -using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyType")] +public class PropertyTypeBasic { - [DataContract(Name = "propertyType")] - public class PropertyTypeBasic - { - /// - /// Gets a value indicating whether the property type is inherited through - /// content types composition. - /// - /// Inherited is true when the property is defined by a content type - /// higher in the composition, and not by the content type currently being - /// edited. - [DataMember(Name = "inherited")] - public bool Inherited { get; set; } + /// + /// Gets a value indicating whether the property type is inherited through + /// content types composition. + /// + /// + /// Inherited is true when the property is defined by a content type + /// higher in the composition, and not by the content type currently being + /// edited. + /// + [DataMember(Name = "inherited")] + public bool Inherited { get; set; } - // needed - so we can handle alias renames - [DataMember(Name = "id")] - public int Id { get; set; } + // needed - so we can handle alias renames + [DataMember(Name = "id")] + public int Id { get; set; } - [Required] - [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public string Alias { get; set; } = null!; - [DataMember(Name = "description")] - public string? Description { get; set; } + [DataMember(Name = "description")] + public string? Description { get; set; } - [DataMember(Name = "validation")] - public PropertyTypeValidation? Validation { get; set; } + [DataMember(Name = "validation")] + public PropertyTypeValidation? Validation { get; set; } - [DataMember(Name = "label")] - [Required] - public string Label { get; set; } = null!; + [DataMember(Name = "label")] + [Required] + public string Label { get; set; } = null!; - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } - [DataMember(Name = "dataTypeId")] - [Required] - public int DataTypeId { get; set; } + [DataMember(Name = "dataTypeId")] + [Required] + public int DataTypeId { get; set; } - [DataMember(Name = "dataTypeKey")] - [ReadOnly(true)] - public Guid DataTypeKey { get; set; } + [DataMember(Name = "dataTypeKey")] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } - [DataMember(Name = "dataTypeName")] - [ReadOnly(true)] - public string? DataTypeName { get; set; } + [DataMember(Name = "dataTypeName")] + [ReadOnly(true)] + public string? DataTypeName { get; set; } - [DataMember(Name = "dataTypeIcon")] - [ReadOnly(true)] - public string? DataTypeIcon { get; set; } + [DataMember(Name = "dataTypeIcon")] + [ReadOnly(true)] + public string? DataTypeIcon { get; set; } - //SD: Is this really needed ? - [DataMember(Name = "groupId")] - public int GroupId { get; set; } + // SD: Is this really needed ? + [DataMember(Name = "groupId")] + public int GroupId { get; set; } - [DataMember(Name = "allowCultureVariant")] - public bool AllowCultureVariant { get; set; } + [DataMember(Name = "allowCultureVariant")] + public bool AllowCultureVariant { get; set; } - [DataMember(Name = "allowSegmentVariant")] - public bool AllowSegmentVariant { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } - [DataMember(Name = "labelOnTop")] - public bool LabelOnTop { get; set; } - } + [DataMember(Name = "labelOnTop")] + public bool LabelOnTop { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeDisplay.cs index 5ca3e4de5c..926ea50106 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeDisplay.cs @@ -1,48 +1,47 @@ -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "propertyType")] +public class PropertyTypeDisplay : PropertyTypeBasic { - [DataContract(Name = "propertyType")] - public class PropertyTypeDisplay : PropertyTypeBasic - { - [DataMember(Name = "editor")] - [ReadOnly(true)] - public string? Editor { get; set; } + [DataMember(Name = "editor")] + [ReadOnly(true)] + public string? Editor { get; set; } - [DataMember(Name = "view")] - [ReadOnly(true)] - public string? View { get; set; } + [DataMember(Name = "view")] + [ReadOnly(true)] + public string? View { get; set; } - [DataMember(Name = "config")] - [ReadOnly(true)] - public IDictionary? Config { get; set; } + [DataMember(Name = "config")] + [ReadOnly(true)] + public IDictionary? Config { get; set; } - /// - /// Gets a value indicating whether this property should be locked when editing. - /// - /// This is used for built in properties like the default MemberType - /// properties that should not be editable from the backoffice. - [DataMember(Name = "locked")] - [ReadOnly(true)] - public bool Locked { get; set; } + /// + /// Gets a value indicating whether this property should be locked when editing. + /// + /// + /// This is used for built in properties like the default MemberType + /// properties that should not be editable from the backoffice. + /// + [DataMember(Name = "locked")] + [ReadOnly(true)] + public bool Locked { get; set; } - /// - /// This is required for the UI editor to know if this particular property belongs to - /// an inherited item or the current item. - /// - [DataMember(Name = "contentTypeId")] - [ReadOnly(true)] - public int ContentTypeId { get; set; } + /// + /// This is required for the UI editor to know if this particular property belongs to + /// an inherited item or the current item. + /// + [DataMember(Name = "contentTypeId")] + [ReadOnly(true)] + public int ContentTypeId { get; set; } - /// - /// This is required for the UI editor to know which content type name this property belongs - /// to based on the property inheritance structure - /// - [DataMember(Name = "contentTypeName")] - [ReadOnly(true)] - public string? ContentTypeName { get; set; } - - } + /// + /// This is required for the UI editor to know which content type name this property belongs + /// to based on the property inheritance structure + /// + [DataMember(Name = "contentTypeName")] + [ReadOnly(true)] + public string? ContentTypeName { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs index 5db1ab8139..76e9547c07 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PropertyTypeValidation.cs @@ -1,23 +1,22 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// An object representing the property type validation settings +/// +[DataContract(Name = "propertyValidation", Namespace = "")] +public class PropertyTypeValidation { - /// - /// An object representing the property type validation settings - /// - [DataContract(Name = "propertyValidation", Namespace = "")] - public class PropertyTypeValidation - { - [DataMember(Name = "mandatory")] - public bool Mandatory { get; set; } + [DataMember(Name = "mandatory")] + public bool Mandatory { get; set; } - [DataMember(Name = "mandatoryMessage")] - public string? MandatoryMessage { get; set; } + [DataMember(Name = "mandatoryMessage")] + public string? MandatoryMessage { get; set; } - [DataMember(Name = "pattern")] - public string? Pattern { get; set; } + [DataMember(Name = "pattern")] + public string? Pattern { get; set; } - [DataMember(Name = "patternMessage")] - public string? PatternMessage { get; set; } - } + [DataMember(Name = "patternMessage")] + public string? PatternMessage { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/PublicAccess.cs b/src/Umbraco.Core/Models/ContentEditing/PublicAccess.cs index 199ca34ceb..1c21aec033 100644 --- a/src/Umbraco.Core/Models/ContentEditing/PublicAccess.cs +++ b/src/Umbraco.Core/Models/ContentEditing/PublicAccess.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "publicAccess", Namespace = "")] +public class PublicAccess { - [DataContract(Name = "publicAccess", Namespace = "")] - public class PublicAccess - { - [DataMember(Name = "groups")] - public MemberGroupDisplay[]? Groups { get; set; } + [DataMember(Name = "groups")] + public MemberGroupDisplay[]? Groups { get; set; } - [DataMember(Name = "loginPage")] - public EntityBasic? LoginPage { get; set; } + [DataMember(Name = "loginPage")] + public EntityBasic? LoginPage { get; set; } - [DataMember(Name = "errorPage")] - public EntityBasic? ErrorPage { get; set; } + [DataMember(Name = "errorPage")] + public EntityBasic? ErrorPage { get; set; } - [DataMember(Name = "members")] - public MemberDisplay[]? Members { get; set; } - } + [DataMember(Name = "members")] + public MemberDisplay[]? Members { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RedirectUrlSearchResults.cs b/src/Umbraco.Core/Models/ContentEditing/RedirectUrlSearchResults.cs index e4b026b6eb..8a1a8d91c9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RedirectUrlSearchResults.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RedirectUrlSearchResults.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "redirectUrlSearchResult", Namespace = "")] +public class RedirectUrlSearchResult { - [DataContract(Name = "redirectUrlSearchResult", Namespace = "")] - public class RedirectUrlSearchResult - { - [DataMember(Name = "searchResults")] - public IEnumerable? SearchResults { get; set; } + [DataMember(Name = "searchResults")] + public IEnumerable? SearchResults { get; set; } - [DataMember(Name = "totalCount")] - public long TotalCount { get; set; } + [DataMember(Name = "totalCount")] + public long TotalCount { get; set; } - [DataMember(Name = "pageCount")] - public int PageCount { get; set; } + [DataMember(Name = "pageCount")] + public int PageCount { get; set; } - [DataMember(Name = "currentPage")] - public int CurrentPage { get; set; } - } + [DataMember(Name = "currentPage")] + public int CurrentPage { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/RelationDisplay.cs index 0decb18414..d4cb960251 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationDisplay.cs @@ -1,52 +1,50 @@ -using System; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "relation", Namespace = "")] +public class RelationDisplay { - [DataContract(Name = "relation", Namespace = "")] - public class RelationDisplay - { - /// - /// Gets or sets the Parent Id of the Relation (Source). - /// - [DataMember(Name = "parentId")] - [ReadOnly(true)] - public int ParentId { get; set; } + /// + /// Gets or sets the Parent Id of the Relation (Source). + /// + [DataMember(Name = "parentId")] + [ReadOnly(true)] + public int ParentId { get; set; } - /// - /// Gets or sets the Parent Name of the relation (Source). - /// - [DataMember(Name = "parentName")] - [ReadOnly(true)] - public string? ParentName { get; set; } + /// + /// Gets or sets the Parent Name of the relation (Source). + /// + [DataMember(Name = "parentName")] + [ReadOnly(true)] + public string? ParentName { get; set; } - /// - /// Gets or sets the Child Id of the Relation (Destination). - /// - [DataMember(Name = "childId")] - [ReadOnly(true)] - public int ChildId { get; set; } + /// + /// Gets or sets the Child Id of the Relation (Destination). + /// + [DataMember(Name = "childId")] + [ReadOnly(true)] + public int ChildId { get; set; } - /// - /// Gets or sets the Child Name of the relation (Destination). - /// - [DataMember(Name = "childName")] - [ReadOnly(true)] - public string? ChildName { get; set; } + /// + /// Gets or sets the Child Name of the relation (Destination). + /// + [DataMember(Name = "childName")] + [ReadOnly(true)] + public string? ChildName { get; set; } - /// - /// Gets or sets the date when the Relation was created. - /// - [DataMember(Name = "createDate")] - [ReadOnly(true)] - public DateTime CreateDate { get; set; } + /// + /// Gets or sets the date when the Relation was created. + /// + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } - /// - /// Gets or sets a comment for the Relation. - /// - [DataMember(Name = "comment")] - [ReadOnly(true)] - public string? Comment { get; set; } - } + /// + /// Gets or sets a comment for the Relation. + /// + [DataMember(Name = "comment")] + [ReadOnly(true)] + public string? Comment { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs index 906fdf3a40..b6168e13d5 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs @@ -1,65 +1,59 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "relationType", Namespace = "")] +public class RelationTypeDisplay : EntityBasic, INotificationModel { - [DataContract(Name = "relationType", Namespace = "")] - public class RelationTypeDisplay : EntityBasic, INotificationModel - { - public RelationTypeDisplay() - { - Notifications = new List(); - } + public RelationTypeDisplay() => Notifications = new List(); - [DataMember(Name = "isSystemRelationType")] - public bool IsSystemRelationType { get; set; } + [DataMember(Name = "isSystemRelationType")] + public bool IsSystemRelationType { get; set; } - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember(Name = "isBidirectional", IsRequired = true)] - public bool IsBidirectional { get; set; } + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember(Name = "isBidirectional", IsRequired = true)] + public bool IsBidirectional { get; set; } - /// - /// Gets or sets the Parents object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember(Name = "parentObjectType", IsRequired = true)] - public Guid? ParentObjectType { get; set; } + /// + /// Gets or sets the Parents object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember(Name = "parentObjectType", IsRequired = true)] + public Guid? ParentObjectType { get; set; } - /// - /// Gets or sets the Parent's object type name. - /// - [DataMember(Name = "parentObjectTypeName")] - [ReadOnly(true)] - public string? ParentObjectTypeName { get; set; } + /// + /// Gets or sets the Parent's object type name. + /// + [DataMember(Name = "parentObjectTypeName")] + [ReadOnly(true)] + public string? ParentObjectTypeName { get; set; } - /// - /// Gets or sets the Child's object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember(Name = "childObjectType", IsRequired = true)] - public Guid? ChildObjectType { get; set; } + /// + /// Gets or sets the Child's object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember(Name = "childObjectType", IsRequired = true)] + public Guid? ChildObjectType { get; set; } - /// - /// Gets or sets the Child's object type name. - /// - [DataMember(Name = "childObjectTypeName")] - [ReadOnly(true)] - public string? ChildObjectTypeName { get; set; } + /// + /// Gets or sets the Child's object type name. + /// + [DataMember(Name = "childObjectTypeName")] + [ReadOnly(true)] + public string? ChildObjectTypeName { get; set; } - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember(Name = "isDependency", IsRequired = true)] + public bool IsDependency { get; set; } - /// - /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. - /// - [DataMember(Name = "isDependency", IsRequired = true)] - public bool IsDependency { get; set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs index f541158095..910d2827f7 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs @@ -1,33 +1,31 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "relationType", Namespace = "")] +public class RelationTypeSave : EntityBasic { - [DataContract(Name = "relationType", Namespace = "")] - public class RelationTypeSave : EntityBasic - { - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember(Name = "isBidirectional", IsRequired = true)] - public bool IsBidirectional { get; set; } + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember(Name = "isBidirectional", IsRequired = true)] + public bool IsBidirectional { get; set; } - /// - /// Gets or sets the parent object type ID. - /// - [DataMember(Name = "parentObjectType", IsRequired = false)] - public Guid? ParentObjectType { get; set; } + /// + /// Gets or sets the parent object type ID. + /// + [DataMember(Name = "parentObjectType", IsRequired = false)] + public Guid? ParentObjectType { get; set; } - /// - /// Gets or sets the child object type ID. - /// - [DataMember(Name = "childObjectType", IsRequired = false)] - public Guid? ChildObjectType { get; set; } + /// + /// Gets or sets the child object type ID. + /// + [DataMember(Name = "childObjectType", IsRequired = false)] + public Guid? ChildObjectType { get; set; } - /// - /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. - /// - [DataMember(Name = "isDependency", IsRequired = true)] - public bool IsDependency { get; set; } - } + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember(Name = "isDependency", IsRequired = true)] + public bool IsDependency { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorCommand.cs b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorCommand.cs index 06fcc5d124..782f34c88c 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorCommand.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorCommand.cs @@ -1,24 +1,23 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public enum RichTextEditorCommandMode { - [DataContract(Name = "richtexteditorcommand", Namespace = "")] - public class RichTextEditorCommand - { - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "alias")] - public string? Alias { get; set; } - - [DataMember(Name = "mode")] - public RichTextEditorCommandMode Mode { get; set; } - } - - public enum RichTextEditorCommandMode - { - Insert, - Selection, - All - } + Insert, + Selection, + All, +} + +[DataContract(Name = "richtexteditorcommand", Namespace = "")] +public class RichTextEditorCommand +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "alias")] + public string? Alias { get; set; } + + [DataMember(Name = "mode")] + public RichTextEditorCommandMode Mode { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorConfiguration.cs b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorConfiguration.cs index e80b25f4ae..c621aa8c59 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorConfiguration.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorConfiguration.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "richtexteditorconfiguration", Namespace = "")] +public class RichTextEditorConfiguration { - [DataContract(Name = "richtexteditorconfiguration", Namespace = "")] - public class RichTextEditorConfiguration - { - [DataMember(Name = "plugins")] - public IEnumerable? Plugins { get; set; } + [DataMember(Name = "plugins")] + public IEnumerable? Plugins { get; set; } - [DataMember(Name = "commands")] - public IEnumerable? Commands { get; set; } + [DataMember(Name = "commands")] + public IEnumerable? Commands { get; set; } - [DataMember(Name = "validElements")] - public string? ValidElements { get; set; } + [DataMember(Name = "validElements")] + public string? ValidElements { get; set; } - [DataMember(Name = "inValidElements")] - public string? InvalidElements { get; set; } + [DataMember(Name = "inValidElements")] + public string? InvalidElements { get; set; } - [DataMember(Name = "customConfig")] - public IDictionary? CustomConfig { get; set; } - } + [DataMember(Name = "customConfig")] + public IDictionary? CustomConfig { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorPlugin.cs b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorPlugin.cs index 3740f47fc6..c35eb1e18c 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RichTextEditorPlugin.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RichTextEditorPlugin.cs @@ -1,11 +1,10 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "richtexteditorplugin", Namespace = "")] +public class RichTextEditorPlugin { - [DataContract(Name = "richtexteditorplugin", Namespace = "")] - public class RichTextEditorPlugin - { - [DataMember(Name = "name")] - public string? Name { get; set; } - } + [DataMember(Name = "name")] + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RollbackVersion.cs b/src/Umbraco.Core/Models/ContentEditing/RollbackVersion.cs index ca0e3ff9af..dfd4511aa1 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RollbackVersion.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RollbackVersion.cs @@ -1,21 +1,19 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "rollbackVersion", Namespace = "")] +public class RollbackVersion { - [DataContract(Name = "rollbackVersion", Namespace = "")] - public class RollbackVersion - { - [DataMember(Name = "versionId")] - public int VersionId { get; set; } + [DataMember(Name = "versionId")] + public int VersionId { get; set; } - [DataMember(Name = "versionDate")] - public DateTime? VersionDate { get; set; } + [DataMember(Name = "versionDate")] + public DateTime? VersionDate { get; set; } - [DataMember(Name = "versionAuthorId")] - public int VersionAuthorId { get; set; } + [DataMember(Name = "versionAuthorId")] + public int VersionAuthorId { get; set; } - [DataMember(Name = "versionAuthorName")] - public string? VersionAuthorName { get; set; } - } + [DataMember(Name = "versionAuthorName")] + public string? VersionAuthorName { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SearchResult.cs b/src/Umbraco.Core/Models/ContentEditing/SearchResult.cs index 53facfe990..8a7fc53605 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SearchResult.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SearchResult.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "result", Namespace = "")] +public class SearchResult { - [DataContract(Name = "result", Namespace = "")] - public class SearchResult - { - [DataMember(Name = "id")] - public string? Id { get; set; } + [DataMember(Name = "id")] + public string? Id { get; set; } - [DataMember(Name = "score")] - public float Score { get; set; } + [DataMember(Name = "score")] + public float Score { get; set; } - [DataMember(Name = "fieldCount")] - public int FieldCount => Values?.Count ?? 0; + [DataMember(Name = "fieldCount")] + public int FieldCount => Values?.Count ?? 0; - [DataMember(Name = "values")] - public IReadOnlyDictionary>? Values { get; set; } - } + [DataMember(Name = "values")] + public IReadOnlyDictionary>? Values { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SearchResultEntity.cs b/src/Umbraco.Core/Models/ContentEditing/SearchResultEntity.cs index e2fc1ff2d7..f86ffc232a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SearchResultEntity.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SearchResultEntity.cs @@ -1,16 +1,13 @@ -using System.Collections; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "searchResult", Namespace = "")] +public class SearchResultEntity : EntityBasic { - [DataContract(Name = "searchResult", Namespace = "")] - public class SearchResultEntity : EntityBasic - { - /// - /// The score of the search result - /// - [DataMember(Name = "score")] - public float Score { get; set; } - } + /// + /// The score of the search result + /// + [DataMember(Name = "score")] + public float Score { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SearchResults.cs b/src/Umbraco.Core/Models/ContentEditing/SearchResults.cs index 2d550a4457..fb7b0fc101 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SearchResults.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SearchResults.cs @@ -1,22 +1,15 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "results", Namespace = "")] +public class SearchResults { - [DataContract(Name = "results", Namespace = "")] - public class SearchResults - { - public static SearchResults Empty() => new SearchResults - { - Results = Enumerable.Empty(), - TotalRecords = 0 - }; + [DataMember(Name = "totalRecords")] + public long TotalRecords { get; set; } - [DataMember(Name = "totalRecords")] - public long TotalRecords { get; set; } + [DataMember(Name = "results")] + public IEnumerable? Results { get; set; } - [DataMember(Name = "results")] - public IEnumerable? Results { get; set; } - } + public static SearchResults Empty() => new() { Results = Enumerable.Empty(), TotalRecords = 0 }; } diff --git a/src/Umbraco.Core/Models/ContentEditing/Section.cs b/src/Umbraco.Core/Models/ContentEditing/Section.cs index 558d73b49b..68d34822c3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Section.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Section.cs @@ -1,24 +1,23 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a section (application) in the back office +/// +[DataContract(Name = "section", Namespace = "")] +public class Section { + [DataMember(Name = "name")] + public string Name { get; set; } = null!; + + [DataMember(Name = "alias")] + public string Alias { get; set; } = null!; + /// - /// Represents a section (application) in the back office + /// In some cases a custom route path can be specified so that when clicking on a section it goes to this + /// path instead of the normal dashboard path /// - [DataContract(Name = "section", Namespace = "")] - public class Section - { - [DataMember(Name = "name")] - public string Name { get; set; } = null!; - - [DataMember(Name = "alias")] - public string Alias { get; set; } = null!; - - /// - /// In some cases a custom route path can be specified so that when clicking on a section it goes to this - /// path instead of the normal dashboard path - /// - [DataMember(Name = "routePath")] - public string? RoutePath { get; set; } - } + [DataMember(Name = "routePath")] + public string? RoutePath { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs b/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs index e6db2b933a..9fe429cf3f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SimpleNotificationModel.cs @@ -1,31 +1,24 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "notificationModel", Namespace = "")] +public class SimpleNotificationModel : INotificationModel { - [DataContract(Name = "notificationModel", Namespace = "")] - public class SimpleNotificationModel : INotificationModel - { - public SimpleNotificationModel() - { - Notifications = new List(); - } + public SimpleNotificationModel() => Notifications = new List(); - public SimpleNotificationModel(params BackOfficeNotification[] notifications) - { - Notifications = new List(notifications); - } + public SimpleNotificationModel(params BackOfficeNotification[] notifications) => + Notifications = new List(notifications); - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + /// + /// A default message + /// + [DataMember(Name = "message")] + public string? Message { get; set; } - /// - /// A default message - /// - [DataMember(Name = "message")] - public string? Message { get; set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/SnippetDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/SnippetDisplay.cs index 39e2027b27..48b3d71cac 100644 --- a/src/Umbraco.Core/Models/ContentEditing/SnippetDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/SnippetDisplay.cs @@ -1,14 +1,13 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "scriptFile", Namespace = "")] +public class SnippetDisplay { - [DataContract(Name = "scriptFile", Namespace = "")] - public class SnippetDisplay - { - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } - [DataMember(Name = "fileName", IsRequired = true)] - public string? FileName { get; set; } - } + [DataMember(Name = "fileName", IsRequired = true)] + public string? FileName { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/StyleSheet.cs b/src/Umbraco.Core/Models/ContentEditing/StyleSheet.cs index 11d3b814c1..6a8d7c14fe 100644 --- a/src/Umbraco.Core/Models/ContentEditing/StyleSheet.cs +++ b/src/Umbraco.Core/Models/ContentEditing/StyleSheet.cs @@ -1,14 +1,13 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "stylesheet", Namespace = "")] +public class Stylesheet { - [DataContract(Name = "stylesheet", Namespace = "")] - public class Stylesheet - { - [DataMember(Name="name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "path")] - public string? Path { get; set; } - } + [DataMember(Name = "path")] + public string? Path { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/StylesheetRule.cs b/src/Umbraco.Core/Models/ContentEditing/StylesheetRule.cs index c5f827300a..f7af3d984f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/StylesheetRule.cs +++ b/src/Umbraco.Core/Models/ContentEditing/StylesheetRule.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "stylesheetRule", Namespace = "")] +public class StylesheetRule { - [DataContract(Name = "stylesheetRule", Namespace = "")] - public class StylesheetRule - { - [DataMember(Name = "name")] - public string Name { get; set; } = null!; + [DataMember(Name = "name")] + public string Name { get; set; } = null!; - [DataMember(Name = "selector")] - public string Selector { get; set; } = null!; + [DataMember(Name = "selector")] + public string Selector { get; set; } = null!; - [DataMember(Name = "styles")] - public string Styles { get; set; } = null!; - } + [DataMember(Name = "styles")] + public string Styles { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/ContentEditing/Tab.cs b/src/Umbraco.Core/Models/ContentEditing/Tab.cs index 4bcd824670..ab1e92d340 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Tab.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Tab.cs @@ -1,40 +1,37 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a tab in the UI +/// +[DataContract(Name = "tab", Namespace = "")] +public class Tab { + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "type")] + public string? Type { get; set; } + + [DataMember(Name = "active")] + public bool IsActive { get; set; } + + [DataMember(Name = "label")] + public string? Label { get; set; } + + [DataMember(Name = "alias")] + public string? Alias { get; set; } + /// - /// Represents a tab in the UI + /// The expanded state of the tab /// - [DataContract(Name = "tab", Namespace = "")] - public class Tab - { - [DataMember(Name = "id")] - public int Id { get; set; } + [DataMember(Name = "open")] + public bool Expanded { get; set; } = true; - [DataMember(Name = "key")] - public Guid Key { get; set; } - - [DataMember(Name = "type")] - public string? Type { get; set; } - - [DataMember(Name = "active")] - public bool IsActive { get; set; } - - [DataMember(Name = "label")] - public string? Label { get; set; } - - [DataMember(Name = "alias")] - public string? Alias { get; set; } - - /// - /// The expanded state of the tab - /// - [DataMember(Name = "open")] - public bool Expanded { get; set; } = true; - - [DataMember(Name = "properties")] - public IEnumerable? Properties { get; set; } - } + [DataMember(Name = "properties")] + public IEnumerable? Properties { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/TabbedContentItem.cs b/src/Umbraco.Core/Models/ContentEditing/TabbedContentItem.cs index afc64e7faf..c47424cdf0 100644 --- a/src/Umbraco.Core/Models/ContentEditing/TabbedContentItem.cs +++ b/src/Umbraco.Core/Models/ContentEditing/TabbedContentItem.cs @@ -1,35 +1,29 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public abstract class TabbedContentItem : ContentItemBasic, ITabbedContent + where T : ContentPropertyBasic { - public abstract class TabbedContentItem : ContentItemBasic, ITabbedContent where T : ContentPropertyBasic + protected TabbedContentItem() => Tabs = new List>(); + + /// + /// Override the properties property to ensure we don't serialize this + /// and to simply return the properties based on the properties in the tabs collection + /// + /// + /// This property cannot be set + /// + [IgnoreDataMember] + public override IEnumerable Properties { - protected TabbedContentItem() - { - Tabs = new List>(); - } - - /// - /// Defines the tabs containing display properties - /// - [DataMember(Name = "tabs")] - public IEnumerable> Tabs { get; set; } - - /// - /// Override the properties property to ensure we don't serialize this - /// and to simply return the properties based on the properties in the tabs collection - /// - /// - /// This property cannot be set - /// - [IgnoreDataMember] - public override IEnumerable Properties - { - get => Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); - set => throw new NotImplementedException(); - } + get => Tabs.Where(x => x.Properties is not null).SelectMany(x => x.Properties!); + set => throw new NotImplementedException(); } + + /// + /// Defines the tabs containing display properties + /// + [DataMember(Name = "tabs")] + public IEnumerable> Tabs { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/TemplateDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/TemplateDisplay.cs index fd67d55936..b6dadcdc2a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/TemplateDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/TemplateDisplay.cs @@ -1,47 +1,43 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "template", Namespace = "")] +public class TemplateDisplay : INotificationModel { - [DataContract(Name = "template", Namespace = "")] - public class TemplateDisplay : INotificationModel - { + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "id")] - public int Id { get; set; } + [Required] + [DataMember(Name = "name")] + public string? Name { get; set; } - [Required] - [DataMember(Name = "name")] - public string? Name { get; set; } + [Required] + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; - [Required] - [DataMember(Name = "alias")] - public string Alias { get; set; } = string.Empty; + [DataMember(Name = "key")] + public Guid Key { get; set; } - [DataMember(Name = "key")] - public Guid Key { get; set; } + [DataMember(Name = "content")] + public string? Content { get; set; } - [DataMember(Name = "content")] - public string? Content { get; set; } + [DataMember(Name = "path")] + public string? Path { get; set; } - [DataMember(Name = "path")] - public string? Path { get; set; } + [DataMember(Name = "virtualPath")] + public string? VirtualPath { get; set; } - [DataMember(Name = "virtualPath")] - public string? VirtualPath { get; set; } + [DataMember(Name = "masterTemplateAlias")] + public string? MasterTemplateAlias { get; set; } - [DataMember(Name = "masterTemplateAlias")] - public string? MasterTemplateAlias { get; set; } + [DataMember(Name = "isMasterTemplate")] + public bool IsMasterTemplate { get; set; } - [DataMember(Name = "isMasterTemplate")] - public bool IsMasterTemplate { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List? Notifications { get; private set; } - } + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List? Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/TreeSearchResult.cs b/src/Umbraco.Core/Models/ContentEditing/TreeSearchResult.cs index 99533facc8..f1b3dea9b2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/TreeSearchResult.cs +++ b/src/Umbraco.Core/Models/ContentEditing/TreeSearchResult.cs @@ -1,34 +1,32 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a search result by entity type +/// +[DataContract(Name = "searchResult", Namespace = "")] +public class TreeSearchResult { + [DataMember(Name = "appAlias")] + public string? AppAlias { get; set; } + + [DataMember(Name = "treeAlias")] + public string? TreeAlias { get; set; } + /// - /// Represents a search result by entity type + /// This is optional but if specified should be the name of an angular service to format the search result. /// - [DataContract(Name = "searchResult", Namespace = "")] - public class TreeSearchResult - { - [DataMember(Name = "appAlias")] - public string? AppAlias { get; set; } + [DataMember(Name = "jsSvc")] + public string? JsFormatterService { get; set; } - [DataMember(Name = "treeAlias")] - public string? TreeAlias { get; set; } + /// + /// This is optional but if specified should be the name of a method on the jsSvc angular service to use, if not + /// specified than it will expect the method to be called `format(searchResult, appAlias, treeAlias)` + /// + [DataMember(Name = "jsMethod")] + public string? JsFormatterMethod { get; set; } - /// - /// This is optional but if specified should be the name of an angular service to format the search result. - /// - [DataMember(Name = "jsSvc")] - public string? JsFormatterService { get; set; } - - /// - /// This is optional but if specified should be the name of a method on the jsSvc angular service to use, if not - /// specified than it will expect the method to be called `format(searchResult, appAlias, treeAlias)` - /// - [DataMember(Name = "jsMethod")] - public string? JsFormatterMethod { get; set; } - - [DataMember(Name = "results")] - public IEnumerable? Results { get; set; } - } + [DataMember(Name = "results")] + public IEnumerable? Results { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs b/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs index c77500c531..e089093174 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs @@ -1,98 +1,97 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents the type's of Umbraco entities that can be resolved from the EntityController +/// +public enum UmbracoEntityTypes { /// - /// Represents the type's of Umbraco entities that can be resolved from the EntityController + /// Language /// - public enum UmbracoEntityTypes - { - /// - /// Language - /// - Language, + Language, - /// - /// User - /// - User, + /// + /// User + /// + User, - /// - /// Macro - /// - Macro, + /// + /// Macro + /// + Macro, - /// - /// Document - /// - Document, + /// + /// Document + /// + Document, - /// - /// Media - /// - Media, + /// + /// Media + /// + Media, - /// - /// Member Type - /// - MemberType, + /// + /// Member Type + /// + MemberType, - /// - /// Template - /// - Template, + /// + /// Template + /// + Template, - /// - /// Member Group - /// - MemberGroup, + /// + /// Member Group + /// + MemberGroup, - /// - /// "Media Type - /// - MediaType, + /// + /// "Media Type + /// + MediaType, - /// - /// Document Type - /// - DocumentType, + /// + /// Document Type + /// + DocumentType, - /// - /// Stylesheet - /// - Stylesheet, + /// + /// Stylesheet + /// + Stylesheet, - /// - /// Script - /// - Script, + /// + /// Script + /// + Script, - /// - /// Partial View - /// - PartialView, + /// + /// Partial View + /// + PartialView, - /// - /// Member - /// - Member, + /// + /// Member + /// + Member, - /// - /// Data Type - /// - DataType, + /// + /// Data Type + /// + DataType, - /// - /// Property Type - /// - PropertyType, + /// + /// Property Type + /// + PropertyType, - /// - /// Property Group - /// - PropertyGroup, + /// + /// Property Group + /// + PropertyGroup, - /// - /// Dictionary Item - /// - DictionaryItem - } + /// + /// Dictionary Item + /// + DictionaryItem, } diff --git a/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs b/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs index 7a4e6d28d8..cc77bf5dbf 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UnpublishContent.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Used to unpublish content and variants +/// +[DataContract(Name = "unpublish", Namespace = "")] +public class UnpublishContent { - /// - /// Used to unpublish content and variants - /// - [DataContract(Name = "unpublish", Namespace = "")] - public class UnpublishContent - { - [DataMember(Name = "id")] - public int Id { get; set; } + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "cultures")] - public string[]? Cultures { get; set; } - } + [DataMember(Name = "cultures")] + public string[]? Cultures { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UrlAndAnchors.cs b/src/Umbraco.Core/Models/ContentEditing/UrlAndAnchors.cs index 0e8c711e83..1a732ed017 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UrlAndAnchors.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UrlAndAnchors.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "urlAndAnchors", Namespace = "")] +public class UrlAndAnchors { - [DataContract(Name = "urlAndAnchors", Namespace = "")] - public class UrlAndAnchors + public UrlAndAnchors(string url, IEnumerable anchorValues) { - public UrlAndAnchors(string url, IEnumerable anchorValues) - { - Url = url; - AnchorValues = anchorValues; - } - - [DataMember(Name = "url")] - public string Url { get; } - - [DataMember(Name = "anchorValues")] - public IEnumerable AnchorValues { get; } + Url = url; + AnchorValues = anchorValues; } + + [DataMember(Name = "url")] + public string Url { get; } + + [DataMember(Name = "anchorValues")] + public IEnumerable AnchorValues { get; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserBasic.cs b/src/Umbraco.Core/Models/ContentEditing/UserBasic.cs index b2dc4ceb4a..6d20e54bfa 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserBasic.cs @@ -1,68 +1,65 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// The user model used for paging and listing users in the UI +/// +[DataContract(Name = "user", Namespace = "")] +[ReadOnly(true)] +public class UserBasic : EntityBasic, INotificationModel { - /// - /// The user model used for paging and listing users in the UI - /// - [DataContract(Name = "user", Namespace = "")] - [ReadOnly(true)] - public class UserBasic : EntityBasic, INotificationModel + public UserBasic() { - public UserBasic() - { - Notifications = new List(); - UserGroups = new List(); - } - - [DataMember(Name = "username")] - public string? Username { get; set; } - - /// - /// The MD5 lowercase hash of the email which can be used by gravatar - /// - [DataMember(Name = "emailHash")] - public string? EmailHash { get; set; } - - [DataMember(Name = "lastLoginDate")] - public DateTime? LastLoginDate { get; set; } - - /// - /// Returns a list of different size avatars - /// - [DataMember(Name = "avatars")] - public string[]? Avatars { get; set; } - - [DataMember(Name = "userState")] - public UserState UserState { get; set; } - - [DataMember(Name = "culture", IsRequired = true)] - public string? Culture { get; set; } - - [DataMember(Name = "email", IsRequired = true)] - public string? Email { get; set; } - - /// - /// The list of group aliases assigned to the user - /// - [DataMember(Name = "userGroups")] - public IEnumerable UserGroups { get; set; } - - /// - /// This is an info flag to denote if this object is the equivalent of the currently logged in user - /// - [DataMember(Name = "isCurrentUser")] - [ReadOnly(true)] - public bool IsCurrentUser { get; set; } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } + Notifications = new List(); + UserGroups = new List(); } + + [DataMember(Name = "username")] + public string? Username { get; set; } + + /// + /// The MD5 lowercase hash of the email which can be used by gravatar + /// + [DataMember(Name = "emailHash")] + public string? EmailHash { get; set; } + + [DataMember(Name = "lastLoginDate")] + public DateTime? LastLoginDate { get; set; } + + /// + /// Returns a list of different size avatars + /// + [DataMember(Name = "avatars")] + public string[]? Avatars { get; set; } + + [DataMember(Name = "userState")] + public UserState UserState { get; set; } + + [DataMember(Name = "culture", IsRequired = true)] + public string? Culture { get; set; } + + [DataMember(Name = "email", IsRequired = true)] + public string? Email { get; set; } + + /// + /// The list of group aliases assigned to the user + /// + [DataMember(Name = "userGroups")] + public IEnumerable UserGroups { get; set; } + + /// + /// This is an info flag to denote if this object is the equivalent of the currently logged in user + /// + [DataMember(Name = "isCurrentUser")] + [ReadOnly(true)] + public bool IsCurrentUser { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Core/Models/ContentEditing/UserDetail.cs index 01c2bcb70c..88b31ee4a2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserDetail.cs @@ -1,62 +1,62 @@ -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents information for the current user +/// +[DataContract(Name = "user", Namespace = "")] +public class UserDetail : UserProfile { + [DataMember(Name = "email", IsRequired = true)] + [Required] + public string? Email { get; set; } + + [DataMember(Name = "locale", IsRequired = true)] + [Required] + public string? Culture { get; set; } + /// - /// Represents information for the current user + /// The MD5 lowercase hash of the email which can be used by gravatar /// - [DataContract(Name = "user", Namespace = "")] - public class UserDetail : UserProfile - { - [DataMember(Name = "email", IsRequired = true)] - [Required] - public string? Email { get; set; } + [DataMember(Name = "emailHash")] + public string? EmailHash { get; set; } - [DataMember(Name = "locale", IsRequired = true)] - [Required] - public string? Culture { get; set; } + [ReadOnly(true)] + [DataMember(Name = "userGroups")] + public string?[]? UserGroups { get; set; } - /// - /// The MD5 lowercase hash of the email which can be used by gravatar - /// - [DataMember(Name = "emailHash")] - public string? EmailHash { get; set; } + /// + /// Gets/sets the number of seconds for the user's auth ticket to expire + /// + [DataMember(Name = "remainingAuthSeconds")] + public double SecondsUntilTimeout { get; set; } - [ReadOnly(true)] - [DataMember(Name = "userGroups")] - public string?[]? UserGroups { get; set; } + /// + /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups + /// they're assigned to + /// + [DataMember(Name = "startContentIds")] + public int[]? StartContentIds { get; set; } - /// - /// Gets/sets the number of seconds for the user's auth ticket to expire - /// - [DataMember(Name = "remainingAuthSeconds")] - public double SecondsUntilTimeout { get; set; } + /// + /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups + /// they're assigned to + /// + [DataMember(Name = "startMediaIds")] + public int[]? StartMediaIds { get; set; } - /// - /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups they're assigned to - /// - [DataMember(Name = "startContentIds")] - public int[]? StartContentIds { get; set; } + /// + /// Returns a list of different size avatars + /// + [DataMember(Name = "avatars")] + public string[]? Avatars { get; set; } - /// - /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups they're assigned to - /// - [DataMember(Name = "startMediaIds")] - public int[]? StartMediaIds { get; set; } - - /// - /// Returns a list of different size avatars - /// - [DataMember(Name = "avatars")] - public string[]? Avatars { get; set; } - - /// - /// A list of sections the user is allowed to view. - /// - [DataMember(Name = "allowedSections")] - public IEnumerable? AllowedSections { get; set; } - } + /// + /// A list of sections the user is allowed to view. + /// + [DataMember(Name = "allowedSections")] + public IEnumerable? AllowedSections { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/UserDisplay.cs index 20e517cefc..4b300c17a9 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserDisplay.cs @@ -1,81 +1,78 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents a user that is being edited +/// +[DataContract(Name = "user", Namespace = "")] +[ReadOnly(true)] +public class UserDisplay : UserBasic { - /// - /// Represents a user that is being edited - /// - [DataContract(Name = "user", Namespace = "")] - [ReadOnly(true)] - public class UserDisplay : UserBasic + public UserDisplay() { - public UserDisplay() - { - AvailableCultures = new Dictionary(); - StartContentIds = new List(); - StartMediaIds = new List(); - Navigation = new List(); - } - - [DataMember(Name = "navigation")] - [ReadOnly(true)] - public IEnumerable Navigation { get; set; } - - /// - /// Gets the available cultures (i.e. to populate a drop down) - /// The key is the culture stored in the database, the value is the Name - /// - [DataMember(Name = "availableCultures")] - public IDictionary AvailableCultures { get; set; } - - [DataMember(Name = "startContentIds")] - public IEnumerable StartContentIds { get; set; } - - [DataMember(Name = "startMediaIds")] - public IEnumerable StartMediaIds { get; set; } - - /// - /// If the password is reset on save, this value will be populated - /// - [DataMember(Name = "resetPasswordValue")] - [ReadOnly(true)] - public string? ResetPasswordValue { get; set; } - - /// - /// A readonly value showing the user's current calculated start content ids - /// - [DataMember(Name = "calculatedStartContentIds")] - [ReadOnly(true)] - public IEnumerable? CalculatedStartContentIds { get; set; } - - /// - /// A readonly value showing the user's current calculated start media ids - /// - [DataMember(Name = "calculatedStartMediaIds")] - [ReadOnly(true)] - public IEnumerable? CalculatedStartMediaIds { get; set; } - - [DataMember(Name = "failedPasswordAttempts")] - [ReadOnly(true)] - public int FailedPasswordAttempts { get; set; } - - [DataMember(Name = "lastLockoutDate")] - [ReadOnly(true)] - public DateTime? LastLockoutDate { get; set; } - - [DataMember(Name = "lastPasswordChangeDate")] - [ReadOnly(true)] - public DateTime? LastPasswordChangeDate { get; set; } - - [DataMember(Name = "createDate")] - [ReadOnly(true)] - public DateTime CreateDate { get; set; } - - [DataMember(Name = "updateDate")] - [ReadOnly(true)] - public DateTime UpdateDate { get; set; } + AvailableCultures = new Dictionary(); + StartContentIds = new List(); + StartMediaIds = new List(); + Navigation = new List(); } + + [DataMember(Name = "navigation")] + [ReadOnly(true)] + public IEnumerable Navigation { get; set; } + + /// + /// Gets the available cultures (i.e. to populate a drop down) + /// The key is the culture stored in the database, the value is the Name + /// + [DataMember(Name = "availableCultures")] + public IDictionary AvailableCultures { get; set; } + + [DataMember(Name = "startContentIds")] + public IEnumerable StartContentIds { get; set; } + + [DataMember(Name = "startMediaIds")] + public IEnumerable StartMediaIds { get; set; } + + /// + /// If the password is reset on save, this value will be populated + /// + [DataMember(Name = "resetPasswordValue")] + [ReadOnly(true)] + public string? ResetPasswordValue { get; set; } + + /// + /// A readonly value showing the user's current calculated start content ids + /// + [DataMember(Name = "calculatedStartContentIds")] + [ReadOnly(true)] + public IEnumerable? CalculatedStartContentIds { get; set; } + + /// + /// A readonly value showing the user's current calculated start media ids + /// + [DataMember(Name = "calculatedStartMediaIds")] + [ReadOnly(true)] + public IEnumerable? CalculatedStartMediaIds { get; set; } + + [DataMember(Name = "failedPasswordAttempts")] + [ReadOnly(true)] + public int FailedPasswordAttempts { get; set; } + + [DataMember(Name = "lastLockoutDate")] + [ReadOnly(true)] + public DateTime? LastLockoutDate { get; set; } + + [DataMember(Name = "lastPasswordChangeDate")] + [ReadOnly(true)] + public DateTime? LastPasswordChangeDate { get; set; } + + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + [DataMember(Name = "updateDate")] + [ReadOnly(true)] + public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupBasic.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupBasic.cs index ffcfde8368..036694f2b6 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupBasic.cs @@ -1,43 +1,40 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "userGroup", Namespace = "")] +public class UserGroupBasic : EntityBasic, INotificationModel { - [DataContract(Name = "userGroup", Namespace = "")] - public class UserGroupBasic : EntityBasic, INotificationModel + public UserGroupBasic() { - public UserGroupBasic() - { - Notifications = new List(); - Sections = Enumerable.Empty
(); - } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - - [DataMember(Name = "sections")] - public IEnumerable
Sections { get; set; } - - [DataMember(Name = "contentStartNode")] - public EntityBasic? ContentStartNode { get; set; } - - [DataMember(Name = "mediaStartNode")] - public EntityBasic? MediaStartNode { get; set; } - - /// - /// The number of users assigned to this group - /// - [DataMember(Name = "userCount")] - public int UserCount { get; set; } - - /// - /// Is the user group a system group e.g. "Administrators", "Sensitive data" or "Translators" - /// - [DataMember(Name = "isSystemUserGroup")] - public bool IsSystemUserGroup { get; set; } + Notifications = new List(); + Sections = Enumerable.Empty
(); } + + [DataMember(Name = "sections")] + public IEnumerable
Sections { get; set; } + + [DataMember(Name = "contentStartNode")] + public EntityBasic? ContentStartNode { get; set; } + + [DataMember(Name = "mediaStartNode")] + public EntityBasic? MediaStartNode { get; set; } + + /// + /// The number of users assigned to this group + /// + [DataMember(Name = "userCount")] + public int UserCount { get; set; } + + /// + /// Is the user group a system group e.g. "Administrators", "Sensitive data" or "Translators" + /// + [DataMember(Name = "isSystemUserGroup")] + public bool IsSystemUserGroup { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupDisplay.cs index 697a0a2100..30cca62c4a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupDisplay.cs @@ -1,31 +1,28 @@ -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "userGroup", Namespace = "")] +public class UserGroupDisplay : UserGroupBasic { - [DataContract(Name = "userGroup", Namespace = "")] - public class UserGroupDisplay : UserGroupBasic + public UserGroupDisplay() { - public UserGroupDisplay() - { - Users = Enumerable.Empty(); - AssignedPermissions = Enumerable.Empty(); - } - - [DataMember(Name = "users")] - public IEnumerable Users { get; set; } - - /// - /// The default permissions for the user group organized by permission group name - /// - [DataMember(Name = "defaultPermissions")] - public IDictionary>? DefaultPermissions { get; set; } - - /// - /// The assigned permissions for the user group organized by permission group name - /// - [DataMember(Name = "assignedPermissions")] - public IEnumerable AssignedPermissions { get; set; } + Users = Enumerable.Empty(); + AssignedPermissions = Enumerable.Empty(); } + + [DataMember(Name = "users")] + public IEnumerable Users { get; set; } + + /// + /// The default permissions for the user group organized by permission group name + /// + [DataMember(Name = "defaultPermissions")] + public IDictionary>? DefaultPermissions { get; set; } + + /// + /// The assigned permissions for the user group organized by permission group name + /// + [DataMember(Name = "assignedPermissions")] + public IEnumerable AssignedPermissions { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs index e782d69635..1c04496e04 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs @@ -1,42 +1,34 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Used to assign user group permissions to a content node +/// +[DataContract(Name = "contentPermission", Namespace = "")] +public class UserGroupPermissionsSave { + public UserGroupPermissionsSave() => AssignedPermissions = new Dictionary>(); + + // TODO: we should have an option to clear the permissions assigned to this node and instead just have them inherit - yes once we actually have inheritance! + [DataMember(Name = "contentId", IsRequired = true)] + [Required] + public int ContentId { get; set; } + /// - /// Used to assign user group permissions to a content node + /// A dictionary of permissions to assign, the key is the user group id /// - [DataContract(Name = "contentPermission", Namespace = "")] - public class UserGroupPermissionsSave + [DataMember(Name = "permissions")] + public IDictionary> AssignedPermissions { get; set; } + + [Obsolete("This is not used and will be removed in Umbraco 10")] + public IEnumerable Validate(ValidationContext validationContext) { - public UserGroupPermissionsSave() + if (AssignedPermissions.SelectMany(x => x.Value).Any(x => x.IsNullOrWhiteSpace())) { - AssignedPermissions = new Dictionary>(); - } - - // TODO: we should have an option to clear the permissions assigned to this node and instead just have them inherit - yes once we actually have inheritance! - - [DataMember(Name = "contentId", IsRequired = true)] - [Required] - public int ContentId { get; set; } - - /// - /// A dictionary of permissions to assign, the key is the user group id - /// - [DataMember(Name = "permissions")] - public IDictionary> AssignedPermissions { get; set; } - - [Obsolete("This is not used and will be removed in Umbraco 10")] - public IEnumerable Validate(ValidationContext validationContext) - { - if (AssignedPermissions.SelectMany(x => x.Value).Any(x => x.IsNullOrWhiteSpace())) - { - yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" }); - } + yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs index 1bf7923817..ef49a6ab28 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs @@ -1,77 +1,78 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +[DataContract(Name = "userGroup", Namespace = "")] +public class UserGroupSave : EntityBasic, IValidatableObject { - [DataContract(Name = "userGroup", Namespace = "")] - public class UserGroupSave : EntityBasic, IValidatableObject + /// + /// The action to perform when saving this user group + /// + /// + /// If either of the Publish actions are specified an exception will be thrown. + /// + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } + + [DataMember(Name = "alias", IsRequired = true)] + [Required] + public override string Alias { get; set; } = string.Empty; + + [DataMember(Name = "sections")] + public IEnumerable? Sections { get; set; } + + [DataMember(Name = "users")] + public IEnumerable? Users { get; set; } + + [DataMember(Name = "startContentId")] + public int? StartContentId { get; set; } + + [DataMember(Name = "startMediaId")] + public int? StartMediaId { get; set; } + + /// + /// The list of letters (permission codes) to assign as the default for the user group + /// + [DataMember(Name = "defaultPermissions")] + public IEnumerable? DefaultPermissions { get; set; } + + /// + /// The assigned permissions for content + /// + /// + /// The key is the content id and the list is the list of letters (permission codes) to assign + /// + [DataMember(Name = "assignedPermissions")] + public IDictionary>? AssignedPermissions { get; set; } + + /// + /// The real persisted user group + /// + [IgnoreDataMember] + public IUserGroup? PersistedUserGroup { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) { - /// - /// The action to perform when saving this user group - /// - /// - /// If either of the Publish actions are specified an exception will be thrown. - /// - [DataMember(Name = "action", IsRequired = true)] - [Required] - public ContentSaveAction Action { get; set; } - - [DataMember(Name = "alias", IsRequired = true)] - [Required] - public override string Alias { get; set; } = string.Empty; - - [DataMember(Name = "sections")] - public IEnumerable? Sections { get; set; } - - [DataMember(Name = "users")] - public IEnumerable? Users { get; set; } - - [DataMember(Name = "startContentId")] - public int? StartContentId { get; set; } - - [DataMember(Name = "startMediaId")] - public int? StartMediaId { get; set; } - - /// - /// The list of letters (permission codes) to assign as the default for the user group - /// - [DataMember(Name = "defaultPermissions")] - public IEnumerable? DefaultPermissions { get; set; } - - /// - /// The assigned permissions for content - /// - /// - /// The key is the content id and the list is the list of letters (permission codes) to assign - /// - [DataMember(Name = "assignedPermissions")] - public IDictionary>? AssignedPermissions { get; set; } - - /// - /// The real persisted user group - /// - [IgnoreDataMember] - public IUserGroup? PersistedUserGroup { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) + if (DefaultPermissions?.Any(x => x.IsNullOrWhiteSpace()) ?? false) { - if (DefaultPermissions?.Any(x => x.IsNullOrWhiteSpace()) ?? false) - { - yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" }); - } + yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" }); + } - if (AssignedPermissions is not null) + if (AssignedPermissions is not null) + { + foreach (KeyValuePair> assignedPermission in AssignedPermissions) { - foreach (var assignedPermission in AssignedPermissions) + foreach (var permission in assignedPermission.Value) { - foreach (var permission in assignedPermission.Value) + if (permission.IsNullOrWhiteSpace()) { - if (permission.IsNullOrWhiteSpace()) - yield return new ValidationResult("A permission value cannot be null or empty", new[] { "AssignedPermissions" }); + yield return new ValidationResult( + "A permission value cannot be null or empty", + new[] { "AssignedPermissions" }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserInvite.cs b/src/Umbraco.Core/Models/ContentEditing/UserInvite.cs index 7b3014369a..02a10b45af 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserInvite.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserInvite.cs @@ -1,44 +1,48 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents the data used to invite a user +/// +[DataContract(Name = "user", Namespace = "")] +public class UserInvite : EntityBasic, IValidatableObject { - /// - /// Represents the data used to invite a user - /// - [DataContract(Name = "user", Namespace = "")] - public class UserInvite : EntityBasic, IValidatableObject + [DataMember(Name = "userGroups")] + [Required] + public IEnumerable UserGroups { get; set; } = null!; + + [DataMember(Name = "email", IsRequired = true)] + [Required] + [EmailAddress] + public string Email { get; set; } = null!; + + [DataMember(Name = "username")] + public string? Username { get; set; } + + [DataMember(Name = "message")] + public string? Message { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) { - [DataMember(Name = "userGroups")] - [Required] - public IEnumerable UserGroups { get; set; } = null!; - - [DataMember(Name = "email", IsRequired = true)] - [Required] - [EmailAddress] - public string Email { get; set; } = null!; - - [DataMember(Name = "username")] - public string? Username { get; set; } - - [DataMember(Name = "message")] - public string? Message { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) + if (UserGroups.Any() == false) { - if (UserGroups.Any() == false) - yield return new ValidationResult("A user must be assigned to at least one group", new[] { nameof(UserGroups) }); + yield return new ValidationResult( + "A user must be assigned to at least one group", + new[] { nameof(UserGroups) }); + } - var securitySettings = validationContext.GetRequiredService>(); + IOptionsSnapshot securitySettings = + validationContext.GetRequiredService>(); - if (securitySettings.Value.UsernameIsEmail == false && Username.IsNullOrWhiteSpace()) - yield return new ValidationResult("A username cannot be empty", new[] { nameof(Username) }); + if (securitySettings.Value.UsernameIsEmail == false && Username.IsNullOrWhiteSpace()) + { + yield return new ValidationResult("A username cannot be empty", new[] { nameof(Username) }); } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserProfile.cs b/src/Umbraco.Core/Models/ContentEditing/UserProfile.cs index 9ade7735e7..441972e8bc 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserProfile.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserProfile.cs @@ -1,27 +1,21 @@ -using System; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// A bare minimum structure that represents a user, usually attached to other objects +/// +[DataContract(Name = "user", Namespace = "")] +public class UserProfile : IComparable { - /// - /// A bare minimum structure that represents a user, usually attached to other objects - /// - [DataContract(Name = "user", Namespace = "")] - public class UserProfile : IComparable - { - [DataMember(Name = "id", IsRequired = true)] - [Required] - public int UserId { get; set; } + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int UserId { get; set; } - [DataMember(Name = "name", IsRequired = true)] - [Required] - public string? Name { get; set; } + [DataMember(Name = "name", IsRequired = true)] + [Required] + public string? Name { get; set; } - - int IComparable.CompareTo(object? obj) - { - return String.Compare(Name, ((UserProfile?)obj)?.Name, StringComparison.Ordinal); - } - } + int IComparable.CompareTo(object? obj) => string.Compare(Name, ((UserProfile?)obj)?.Name, StringComparison.Ordinal); } diff --git a/src/Umbraco.Core/Models/ContentEditing/UserSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserSave.cs index 6e03248a31..e0a3d41d4f 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserSave.cs @@ -1,55 +1,56 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing; + +/// +/// Represents the data used to persist a user +/// +/// +/// This will be different from the model used to display a user and we don't want to "Overpost" data back to the +/// server, +/// and there will most likely be different bits of data required for updating passwords which will be different from +/// the +/// data used to display vs save +/// +[DataContract(Name = "user", Namespace = "")] +public class UserSave : EntityBasic, IValidatableObject { - /// - /// Represents the data used to persist a user - /// - /// - /// This will be different from the model used to display a user and we don't want to "Overpost" data back to the server, - /// and there will most likely be different bits of data required for updating passwords which will be different from the - /// data used to display vs save - /// - [DataContract(Name = "user", Namespace = "")] - public class UserSave : EntityBasic, IValidatableObject + [DataMember(Name = "changePassword", IsRequired = true)] + public ChangingPasswordModel? ChangePassword { get; set; } + + [DataMember(Name = "id", IsRequired = true)] + [Required] + public new int Id { get; set; } + + [DataMember(Name = "username", IsRequired = true)] + [Required] + public string Username { get; set; } = null!; + + [DataMember(Name = "culture", IsRequired = true)] + [Required] + public string Culture { get; set; } = null!; + + [DataMember(Name = "email", IsRequired = true)] + [Required] + [EmailAddress] + public string Email { get; set; } = null!; + + [DataMember(Name = "userGroups")] + [Required] + public IEnumerable UserGroups { get; set; } = null!; + + [DataMember(Name = "startContentIds")] + public int[]? StartContentIds { get; set; } + + [DataMember(Name = "startMediaIds")] + public int[]? StartMediaIds { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) { - [DataMember(Name = "changePassword", IsRequired = true)] - public ChangingPasswordModel? ChangePassword { get; set; } - - [DataMember(Name = "id", IsRequired = true)] - [Required] - public new int Id { get; set; } - - [DataMember(Name = "username", IsRequired = true)] - [Required] - public string Username { get; set; } = null!; - - [DataMember(Name = "culture", IsRequired = true)] - [Required] - public string Culture { get; set; } = null!; - - [DataMember(Name = "email", IsRequired = true)] - [Required] - [EmailAddress] - public string Email { get; set; } = null!; - - [DataMember(Name = "userGroups")] - [Required] - public IEnumerable UserGroups { get; set; } = null!; - - [DataMember(Name = "startContentIds")] - public int[]? StartContentIds { get; set; } - - [DataMember(Name = "startMediaIds")] - public int[]? StartMediaIds { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) + if (UserGroups.Any() == false) { - if (UserGroups.Any() == false) - yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); + yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); } } } diff --git a/src/Umbraco.Core/Models/ContentModel.cs b/src/Umbraco.Core/Models/ContentModel.cs index cead39f019..5d81ea367e 100644 --- a/src/Umbraco.Core/Models/ContentModel.cs +++ b/src/Umbraco.Core/Models/ContentModel.cs @@ -1,21 +1,20 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the model for the current Umbraco view. +/// +public class ContentModel : IContentModel { /// - /// Represents the model for the current Umbraco view. + /// Initializes a new instance of the class with a content. /// - public class ContentModel : IContentModel - { - /// - /// Initializes a new instance of the class with a content. - /// - public ContentModel(IPublishedContent? content) => Content = content ?? throw new ArgumentNullException(nameof(content)); + public ContentModel(IPublishedContent? content) => + Content = content ?? throw new ArgumentNullException(nameof(content)); - /// - /// Gets the content. - /// - public IPublishedContent Content { get; } - } + /// + /// Gets the content. + /// + public IPublishedContent Content { get; } } diff --git a/src/Umbraco.Core/Models/ContentModelOfTContent.cs b/src/Umbraco.Core/Models/ContentModelOfTContent.cs index ab882342b5..32889331e0 100644 --- a/src/Umbraco.Core/Models/ContentModelOfTContent.cs +++ b/src/Umbraco.Core/Models/ContentModelOfTContent.cs @@ -1,19 +1,18 @@ using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models -{ - public class ContentModel : ContentModel - where TContent : IPublishedContent - { - /// - /// Initializes a new instance of the class with a content. - /// - public ContentModel(TContent content) - : base(content) => Content = content; +namespace Umbraco.Cms.Core.Models; - /// - /// Gets the content. - /// - public new TContent Content { get; } - } +public class ContentModel : ContentModel + where TContent : IPublishedContent +{ + /// + /// Initializes a new instance of the class with a content. + /// + public ContentModel(TContent content) + : base(content) => Content = content; + + /// + /// Gets the content. + /// + public new TContent Content { get; } } diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 4ab39f1669..bf0879f8dd 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -1,353 +1,440 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods used to manipulate content variations by the document repository +/// +public static class ContentRepositoryExtensions { - /// - /// Extension methods used to manipulate content variations by the document repository - /// - public static class ContentRepositoryExtensions + public static void SetCultureInfo(this IContentBase content, string? culture, string? name, DateTime date) { - public static void SetCultureInfo(this IContentBase content, string? culture, string? name, DateTime date) + if (name == null) { - 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 (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - content.CultureInfos?.AddOrUpdate(culture, name, date); + throw new ArgumentNullException(nameof(name)); } - /// - /// Updates a culture date, if the culture exists. - /// - public static void TouchCulture(this IContentBase content, string? culture) + if (string.IsNullOrWhiteSpace(name)) { - if (culture.IsNullOrWhiteSpace() || content.CultureInfos is null) - { - return; - } - - if (!content.CultureInfos.TryGetValue(culture!, out var infos)) - { - return; - } - - content.CultureInfos?.AddOrUpdate(culture!, infos.Name, DateTime.Now); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Used to synchronize all culture dates to the same date if they've been modified - /// - /// - /// - /// - /// This is so that in an operation where (for example) 2 languages are updates like french and english, it is possible that - /// these dates assigned to them differ by a couple of Ticks, but we need to ensure they are persisted at the exact same time. - /// - public static void AdjustDates(this IContent content, DateTime date, bool publishing) + if (culture == null) { - if (content.EditedCultures is not null) + throw new ArgumentNullException(nameof(culture)); + } + + if (string.IsNullOrWhiteSpace(culture)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); + } + + content.CultureInfos?.AddOrUpdate(culture, name, date); + } + + /// + /// Updates a culture date, if the culture exists. + /// + public static void TouchCulture(this IContentBase content, string? culture) + { + if (culture.IsNullOrWhiteSpace() || content.CultureInfos is null) + { + return; + } + + if (!content.CultureInfos.TryGetValue(culture!, out ContentCultureInfos infos)) + { + return; + } + + content.CultureInfos?.AddOrUpdate(culture!, infos.Name, DateTime.Now); + } + + /// + /// Used to synchronize all culture dates to the same date if they've been modified + /// + /// + /// + /// + /// This is so that in an operation where (for example) 2 languages are updates like french and english, it is possible + /// that + /// these dates assigned to them differ by a couple of Ticks, but we need to ensure they are persisted at the exact + /// same time. + /// + public static void AdjustDates(this IContent content, DateTime date, bool publishing) + { + if (content.EditedCultures is not null) + { + foreach (var culture in content.EditedCultures.ToList()) { - foreach(var culture in content.EditedCultures.ToList()) - { - if (content.CultureInfos is null) - { - continue; - } - - if (!content.CultureInfos.TryGetValue(culture, out var editedInfos)) - { - continue; - } - - // if it's not dirty, it means it hasn't changed so there's nothing to adjust - if (!editedInfos.IsDirty()) - { - continue; - } - - content.CultureInfos?.AddOrUpdate(culture, editedInfos?.Name, date); - } - } - - - if (!publishing) - { - return; - } - - foreach (var culture in content.PublishedCultures.ToList()) - { - if (content.PublishCultureInfos is null) + if (content.CultureInfos is null) { continue; } - if (!content.PublishCultureInfos.TryGetValue(culture, out ContentCultureInfos publishInfos)) + + if (!content.CultureInfos.TryGetValue(culture, out ContentCultureInfos editedInfos)) { continue; } // if it's not dirty, it means it hasn't changed so there's nothing to adjust - if (!publishInfos.IsDirty()) + if (!editedInfos.IsDirty()) { continue; } - content.PublishCultureInfos.AddOrUpdate(culture, publishInfos.Name, date); + content.CultureInfos?.AddOrUpdate(culture, editedInfos?.Name, date); + } + } - if (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos infos) ?? false) + if (!publishing) + { + return; + } + + foreach (var culture in content.PublishedCultures.ToList()) + { + if (content.PublishCultureInfos is null) + { + continue; + } + + if (!content.PublishCultureInfos.TryGetValue(culture, out ContentCultureInfos publishInfos)) + { + continue; + } + + // if it's not dirty, it means it hasn't changed so there's nothing to adjust + if (!publishInfos.IsDirty()) + { + continue; + } + + content.PublishCultureInfos.AddOrUpdate(culture, publishInfos.Name, date); + + if (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos infos) ?? false) + { + SetCultureInfo(content, culture, infos.Name, date); + } + } + } + + /// + /// Gets the cultures that have been flagged for unpublishing. + /// + /// Gets cultures for which content.UnpublishCulture() has been invoked. + public static IReadOnlyList? GetCulturesUnpublishing(this IContent content) + { + if (!content.Published || !content.ContentType.VariesByCulture() || + !content.IsPropertyDirty("PublishCultureInfos")) + { + return Array.Empty(); + } + + IEnumerable? culturesUnpublishing = content.CultureInfos?.Values + .Where(x => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + x.Culture)) + .Select(x => x.Culture); + + return culturesUnpublishing?.ToList(); + } + + /// + /// Copies values from another document. + /// + public static void CopyFrom(this IContent content, IContent other, string? culture = "*") + { + if (other.ContentTypeId != content.ContentTypeId) + { + throw new InvalidOperationException("Cannot copy values from a different content type."); + } + + culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) + { + throw new NotSupportedException( + $"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + } + + // copying from the same Id and VersionPk + var copyingFromSelf = content.Id == other.Id && content.VersionId == other.VersionId; + var published = copyingFromSelf; + + // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails + + // clear all existing properties for the specified culture + foreach (IProperty property in content.Properties) + { + // each property type may or may not support the variation + if (!property.PropertyType?.SupportsVariation(culture, "*", true) ?? false) + { + continue; + } + + foreach (IPropertyValue pvalue in property.Values) + { + if ((property.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) && + (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) { - SetCultureInfo(content, culture, infos.Name, date); + property.SetValue(null, pvalue.Culture, pvalue.Segment); } } } - /// - /// Gets the cultures that have been flagged for unpublishing. - /// - /// Gets cultures for which content.UnpublishCulture() has been invoked. - public static IReadOnlyList? GetCulturesUnpublishing(this IContent content) + // copy properties from 'other' + IPropertyCollection otherProperties = other.Properties; + foreach (IProperty otherProperty in otherProperties) { - if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos")) - return Array.Empty(); - - var culturesUnpublishing = content.CultureInfos?.Values - .Where(x => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + x.Culture)) - .Select(x => x.Culture); - - return culturesUnpublishing?.ToList(); - } - - /// - /// Copies values from another document. - /// - public static void CopyFrom(this IContent content, IContent other, string? culture = "*") - { - if (other.ContentTypeId != content.ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); - - // the variation should be supported by the content type properties - // if the content type is invariant, only '*' and 'null' is ok - // if the content type varies, everything is ok because some properties may be invariant - if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) - throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); - - // copying from the same Id and VersionPk - var copyingFromSelf = content.Id == other.Id && content.VersionId == other.VersionId; - var published = copyingFromSelf; - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties for the specified culture - foreach (var property in content.Properties) + if (!otherProperty?.PropertyType?.SupportsVariation(culture, "*", true) ?? true) { - // each property type may or may not support the variation - if (!property.PropertyType?.SupportsVariation(culture, "*", wildcards: true) ?? false) - continue; + continue; + } - foreach (var pvalue in property.Values) - if ((property.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) ?? false) && + var alias = otherProperty?.PropertyType.Alias; + if (otherProperty is not null && alias is not null) + { + foreach (IPropertyValue pvalue in otherProperty.Values) + { + if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, true) && (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) { - property.SetValue(null, pvalue.Culture, pvalue.Segment); - } - } - - // copy properties from 'other' - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - if (!otherProperty?.PropertyType?.SupportsVariation(culture, "*", wildcards: true) ?? true) - continue; - - var alias = otherProperty?.PropertyType.Alias; - if (otherProperty is not null && alias is not null) - { - foreach (var pvalue in otherProperty.Values) - { - if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && - (culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) - { - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - content.SetValue(alias, value, pvalue.Culture, pvalue.Segment); - } + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + content.SetValue(alias, value, pvalue.Culture, pvalue.Segment); } } } + } - // copy names, too + // copy names, too + if (culture == "*") + { + content.CultureInfos?.Clear(); + content.CultureInfos = null; + } - if (culture == "*") + if (culture == null || culture == "*") + { + content.Name = other.Name; + } + + // ReSharper disable once UseDeconstruction + if (other.CultureInfos is not null) + { + foreach (ContentCultureInfos cultureInfo in other.CultureInfos) { - content.CultureInfos?.Clear(); - content.CultureInfos = null; - } - - if (culture == null || culture == "*") - content.Name = other.Name; - - // ReSharper disable once UseDeconstruction - if (other.CultureInfos is not null) - { - foreach (var cultureInfo in other.CultureInfos) + if (culture == "*" || culture == cultureInfo.Culture) { - if (culture == "*" || culture == cultureInfo.Culture) - content.SetCultureName(cultureInfo.Name, cultureInfo.Culture); + content.SetCultureName(cultureInfo.Name, cultureInfo.Culture); } } } + } - public static void SetPublishInfo(this IContent content, string? culture, string? name, DateTime date) + public static void SetPublishInfo(this IContent content, string? culture, string? name, DateTime date) + { + if (name == null) { - 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 (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - content.PublishCultureInfos?.AddOrUpdate(culture, name, date); + throw new ArgumentNullException(nameof(name)); } - // sets the edited cultures on the content - public static void SetCultureEdited(this IContent content, IEnumerable? cultures) + if (string.IsNullOrWhiteSpace(name)) { - if (cultures == null) - content.EditedCultures = null; - else - { - var editedCultures = new HashSet(cultures.Where(x => !x.IsNullOrWhiteSpace())!, StringComparer.OrdinalIgnoreCase); - content.EditedCultures = editedCultures.Count > 0 ? editedCultures : null; - } + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Sets the publishing values for names and properties. - /// - /// - /// - /// A value indicating whether it was possible to publish the names and values for the specified - /// culture(s). The method may fail if required names are not set, but it does NOT validate property data - public static bool PublishCulture(this IContent content, CultureImpact? impact) + if (culture == null) { - if (impact == null) throw new ArgumentNullException(nameof(impact)); + throw new ArgumentNullException(nameof(culture)); + } - // the variation should be supported by the content type properties - // if the content type is invariant, only '*' and 'null' is ok - // if the content type varies, everything is ok because some properties may be invariant - if (!content.ContentType.SupportsPropertyVariation(impact.Culture, "*", true)) - throw new NotSupportedException($"Culture \"{impact.Culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + if (string.IsNullOrWhiteSpace(culture)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); + } - // set names - if (impact.ImpactsAllCultures) + content.PublishCultureInfos?.AddOrUpdate(culture, name, date); + } + + // sets the edited cultures on the content + public static void SetCultureEdited(this IContent content, IEnumerable? cultures) + { + if (cultures == null) + { + content.EditedCultures = null; + } + else + { + var editedCultures = new HashSet( + cultures.Where(x => !x.IsNullOrWhiteSpace())!, + StringComparer.OrdinalIgnoreCase); + content.EditedCultures = editedCultures.Count > 0 ? editedCultures : null; + } + } + + /// + /// Sets the publishing values for names and properties. + /// + /// + /// + /// + /// A value indicating whether it was possible to publish the names and values for the specified + /// culture(s). The method may fail if required names are not set, but it does NOT validate property data + /// + public static bool PublishCulture(this IContent content, CultureImpact? impact) + { + if (impact == null) + { + throw new ArgumentNullException(nameof(impact)); + } + + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!content.ContentType.SupportsPropertyVariation(impact.Culture, "*", true)) + { + throw new NotSupportedException( + $"Culture \"{impact.Culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + } + + // set names + if (impact.ImpactsAllCultures) + { + // does NOT contain the invariant culture + foreach (var c in content.AvailableCultures) { - foreach (var c in content.AvailableCultures) // does NOT contain the invariant culture - { - var name = content.GetCultureName(c); - if (string.IsNullOrWhiteSpace(name)) - return false; - content.SetPublishInfo(c, name, DateTime.Now); - } - } - else if (impact.ImpactsOnlyInvariantCulture) - { - if (string.IsNullOrWhiteSpace(content.Name)) - return false; - // PublishName set by repository - nothing to do here - } - else if (impact.ImpactsExplicitCulture) - { - var name = content.GetCultureName(impact.Culture); + var name = content.GetCultureName(c); if (string.IsNullOrWhiteSpace(name)) + { return false; - content.SetPublishInfo(impact.Culture, name, DateTime.Now); + } + + content.SetPublishInfo(c, name, DateTime.Now); + } + } + else if (impact.ImpactsOnlyInvariantCulture) + { + if (string.IsNullOrWhiteSpace(content.Name)) + { + return false; } - // set values - // property.PublishValues only publishes what is valid, variation-wise, - // but accepts any culture arg: null, all, specific - foreach (var property in content.Properties) + // PublishName set by repository - nothing to do here + } + else if (impact.ImpactsExplicitCulture) + { + var name = content.GetCultureName(impact.Culture); + if (string.IsNullOrWhiteSpace(name)) { - // for the specified culture (null or all or specific) - property.PublishValues(impact.Culture); + return false; + } - // maybe the specified culture did not impact the invariant culture, so PublishValues - // above would skip it, yet it *also* impacts invariant properties - if (impact.ImpactsAlsoInvariantProperties) - property.PublishValues(null); + content.SetPublishInfo(impact.Culture, name, DateTime.Now); + } + + // set values + // property.PublishValues only publishes what is valid, variation-wise, + // but accepts any culture arg: null, all, specific + foreach (IProperty property in content.Properties) + { + // for the specified culture (null or all or specific) + property.PublishValues(impact.Culture); + + // maybe the specified culture did not impact the invariant culture, so PublishValues + // above would skip it, yet it *also* impacts invariant properties + if (impact.ImpactsAlsoInvariantProperties) + { + property.PublishValues(null); + } + } + + content.PublishedState = PublishedState.Publishing; + return true; + } + + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool UnpublishCulture(this IContent content, string? culture = "*") + { + culture = culture?.NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) + { + throw new NotSupportedException( + $"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); + } + + var keepProcessing = true; + + if (culture == "*") + { + // all cultures + content.ClearPublishInfos(); + } + else + { + // one single culture + keepProcessing = content.ClearPublishInfo(culture); + } + + if (keepProcessing) + { + // property.PublishValues only publishes what is valid, variation-wise + foreach (IProperty property in content.Properties) + { + property.UnpublishValues(culture); } content.PublishedState = PublishedState.Publishing; - return true; } - /// - /// Returns false if the culture is already unpublished - /// - /// - /// - /// - public static bool UnpublishCulture(this IContent content, string? culture = "*") + return keepProcessing; + } + + public static void ClearPublishInfos(this IContent content) => content.PublishCultureInfos = null; + + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool ClearPublishInfo(this IContent content, string? culture) + { + if (culture == null) { - culture = culture?.NullOrWhiteSpaceAsNull(); - - // the variation should be supported by the content type properties - if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) - throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); - - var keepProcessing = true; - - if (culture == "*") - { - // all cultures - content.ClearPublishInfos(); - } - else - { - // one single culture - keepProcessing = content.ClearPublishInfo(culture); - } - - if (keepProcessing) - { - // property.PublishValues only publishes what is valid, variation-wise - foreach (var property in content.Properties) - property.UnpublishValues(culture); - - content.PublishedState = PublishedState.Publishing; - } - - return keepProcessing; + throw new ArgumentNullException(nameof(culture)); } - public static void ClearPublishInfos(this IContent content) + if (string.IsNullOrWhiteSpace(culture)) { - content.PublishCultureInfos = null; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(culture)); } - /// - /// Returns false if the culture is already unpublished - /// - /// - /// - /// - public static bool ClearPublishInfo(this IContent content, string? culture) + var removed = content.PublishCultureInfos?.Remove(culture); + if (removed ?? false) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - - var removed = content.PublishCultureInfos?.Remove(culture); - if (removed ?? false) - { - // set the culture to be dirty - it's been modified - content.TouchCulture(culture); - } - return removed ?? false; + // set the culture to be dirty - it's been modified + content.TouchCulture(culture); } + + return removed ?? false; } } diff --git a/src/Umbraco.Core/Models/ContentSchedule.cs b/src/Umbraco.Core/Models/ContentSchedule.cs index 77526f254a..18d254a9aa 100644 --- a/src/Umbraco.Core/Models/ContentSchedule.cs +++ b/src/Umbraco.Core/Models/ContentSchedule.cs @@ -1,78 +1,74 @@ -using System; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a scheduled action for a document. +/// +[Serializable] +[DataContract(IsReference = true)] +public class ContentSchedule : IDeepCloneable { /// - /// Represents a scheduled action for a document. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class ContentSchedule : IDeepCloneable + public ContentSchedule(string culture, DateTime date, ContentScheduleAction action) { - /// - /// Initializes a new instance of the class. - /// - public ContentSchedule(string culture, DateTime date, ContentScheduleAction action) - { - Id = Guid.Empty; // will be assigned by document repository - Culture = culture; - Date = date; - Action = action; - } - - /// - /// Initializes a new instance of the class. - /// - public ContentSchedule(Guid id, string culture, DateTime date, ContentScheduleAction action) - { - Id = id; - Culture = culture; - Date = date; - Action = action; - } - - /// - /// Gets the unique identifier of the document targeted by the scheduled action. - /// - [DataMember] - public Guid Id { get; set; } - - /// - /// Gets the culture of the scheduled action. - /// - /// - /// string.Empty represents the invariant culture. - /// - [DataMember] - public string Culture { get; } - - /// - /// Gets the date of the scheduled action. - /// - [DataMember] - public DateTime Date { get; } - - /// - /// Gets the action to take. - /// - [DataMember] - public ContentScheduleAction Action { get; } - - public override bool Equals(object? obj) - => obj is ContentSchedule other && Equals(other); - - public bool Equals(ContentSchedule other) - { - // don't compare Ids, two ContentSchedule are equal if they are for the same change - // for the same culture, on the same date - and the collection deals w/duplicates - return Culture.InvariantEquals(other.Culture) && Date == other.Date && Action == other.Action; - } - - public object DeepClone() - { - return new ContentSchedule(Id, Culture, Date, Action); - } + Id = Guid.Empty; // will be assigned by document repository + Culture = culture; + Date = date; + Action = action; } + + /// + /// Initializes a new instance of the class. + /// + public ContentSchedule(Guid id, string culture, DateTime date, ContentScheduleAction action) + { + Id = id; + Culture = culture; + Date = date; + Action = action; + } + + /// + /// Gets the unique identifier of the document targeted by the scheduled action. + /// + [DataMember] + public Guid Id { get; set; } + + /// + /// Gets the culture of the scheduled action. + /// + /// + /// string.Empty represents the invariant culture. + /// + [DataMember] + public string Culture { get; } + + /// + /// Gets the date of the scheduled action. + /// + [DataMember] + public DateTime Date { get; } + + /// + /// Gets the action to take. + /// + [DataMember] + public ContentScheduleAction Action { get; } + + public object DeepClone() => new ContentSchedule(Id, Culture, Date, Action); + + public override bool Equals(object? obj) + => obj is ContentSchedule other && Equals(other); + + public bool Equals(ContentSchedule other) => + + // don't compare Ids, two ContentSchedule are equal if they are for the same change + // for the same culture, on the same date - and the collection deals w/duplicates + Culture.InvariantEquals(other.Culture) && Date == other.Date && Action == other.Action; + + public override int GetHashCode() => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Models/ContentScheduleAction.cs b/src/Umbraco.Core/Models/ContentScheduleAction.cs index 03be526814..d6a50b994b 100644 --- a/src/Umbraco.Core/Models/ContentScheduleAction.cs +++ b/src/Umbraco.Core/Models/ContentScheduleAction.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines scheduled actions for documents. +/// +public enum ContentScheduleAction { /// - /// Defines scheduled actions for documents. + /// Release the document. /// - public enum ContentScheduleAction - { - /// - /// Release the document. - /// - Release, + Release, - /// - /// Expire the document. - /// - Expire - } + /// + /// Expire the document. + /// + Expire, } diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs index 12a53fd103..4fb90779de 100644 --- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs +++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs @@ -1,242 +1,261 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable, IEquatable { - public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable, IEquatable + // underlying storage for the collection backed by a sorted list so that the schedule is always in order of date and that duplicate dates per culture are not allowed + private readonly Dictionary> _schedule + = new(StringComparer.InvariantCultureIgnoreCase); + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + /// + /// Returns all schedules registered + /// + /// + public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); + + public object DeepClone() { - //underlying storage for the collection backed by a sorted list so that the schedule is always in order of date and that duplicate dates per culture are not allowed - private readonly Dictionary> _schedule - = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); - - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; - - private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + var clone = new ContentScheduleCollection(); + foreach (KeyValuePair> cultureSched in _schedule) { - CollectionChanged?.Invoke(this, args); - } - - /// - /// Add an existing schedule - /// - /// - public void Add(ContentSchedule schedule) - { - if (!_schedule.TryGetValue(schedule.Culture, out var changes)) + var list = new SortedList(); + foreach (KeyValuePair schedEntry in cultureSched.Value) { - changes = new SortedList(); - _schedule[schedule.Culture] = changes; + list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); } - // TODO: Below will throw if there are duplicate dates added, validate/return bool? - changes.Add(schedule.Date, schedule); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule)); + clone._schedule[cultureSched.Key] = list; } - /// - /// Adds a new schedule for invariant content - /// - /// - /// - public bool Add(DateTime? releaseDate, DateTime? expireDate) + return clone; + } + + public bool Equals(ContentScheduleCollection? other) + { + if (other == null) { - return Add(string.Empty, releaseDate, expireDate); + return false; } - /// - /// Adds a new schedule for a culture - /// - /// - /// - /// - /// true if successfully added, false if validation fails - public bool Add(string? culture, DateTime? releaseDate, DateTime? expireDate) + Dictionary> thisSched = _schedule; + Dictionary> thatSched = other._schedule; + + if (thisSched.Count != thatSched.Count) { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) + return false; + } + + foreach ((var culture, SortedList thisList) in thisSched) + { + // if culture is missing, or actions differ, false + if (!thatSched.TryGetValue(culture, out SortedList? thatList) || + !thatList.SequenceEqual(thisList)) + { return false; - - if (!releaseDate.HasValue && !expireDate.HasValue) return false; - - // TODO: Do we allow passing in a release or expiry date that is before now? - - if (!_schedule.TryGetValue(culture, out var changes)) - { - changes = new SortedList(); - _schedule[culture] = changes; } - - // TODO: Below will throw if there are duplicate dates added, should validate/return bool? - // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end? - - if (releaseDate.HasValue) - { - var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); - changes.Add(releaseDate.Value, entry); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); - } - - if (expireDate.HasValue) - { - var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); - changes.Add(expireDate.Value, entry); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); - } - - return true; } - /// - /// Remove a scheduled change - /// - /// - public void Remove(ContentSchedule change) + return true; + } + + public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) + { + var schedule = new ContentScheduleCollection(); + schedule.Add(string.Empty, release, expire); + return schedule; + } + + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => CollectionChanged = null; + + /// + /// Add an existing schedule + /// + /// + public void Add(ContentSchedule schedule) + { + if (!_schedule.TryGetValue(schedule.Culture, out SortedList? changes)) { - if (_schedule.TryGetValue(change.Culture, out var s)) + changes = new SortedList(); + _schedule[schedule.Culture] = changes; + } + + // TODO: Below will throw if there are duplicate dates added, validate/return bool? + changes.Add(schedule.Date, schedule); + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule)); + } + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); + + /// + /// Adds a new schedule for invariant content + /// + /// + /// + public bool Add(DateTime? releaseDate, DateTime? expireDate) => Add(string.Empty, releaseDate, expireDate); + + /// + /// Adds a new schedule for a culture + /// + /// + /// + /// + /// true if successfully added, false if validation fails + public bool Add(string? culture, DateTime? releaseDate, DateTime? expireDate) + { + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); + } + + if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate) + { + return false; + } + + if (!releaseDate.HasValue && !expireDate.HasValue) + { + return false; + } + + // TODO: Do we allow passing in a release or expiry date that is before now? + if (!_schedule.TryGetValue(culture, out SortedList? changes)) + { + changes = new SortedList(); + _schedule[culture] = changes; + } + + // TODO: Below will throw if there are duplicate dates added, should validate/return bool? + // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end? + if (releaseDate.HasValue) + { + var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release); + changes.Add(releaseDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); + } + + if (expireDate.HasValue) + { + var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire); + changes.Add(expireDate.Value, entry); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry)); + } + + return true; + } + + /// + /// Remove a scheduled change + /// + /// + public void Remove(ContentSchedule change) + { + if (_schedule.TryGetValue(change.Culture, out SortedList? s)) + { + var removed = s.Remove(change.Date); + if (removed) { - var removed = s.Remove(change.Date); - if (removed) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); + if (s.Count == 0) { - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change)); - if (s.Count == 0) - _schedule.Remove(change.Culture); + _schedule.Remove(change.Culture); } - } } - - /// - /// Clear all of the scheduled change type for invariant content - /// - /// - /// If specified, will clear all entries with dates less than or equal to the value - public void Clear(ContentScheduleAction action, DateTime? changeDate = null) - { - Clear(string.Empty, action, changeDate); - } - - /// - /// Clear all of the scheduled change type for the culture - /// - /// - /// - /// If specified, will clear all entries with dates less than or equal to the value - public void Clear(string? culture, ContentScheduleAction action, DateTime? date = null) - { - if (culture is null || !_schedule.TryGetValue(culture, out var schedules)) - return; - - var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)).ToList(); - - foreach (var remove in removes) - { - var removed = schedules.Remove(remove.Value.Date); - if (!removed) - continue; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value)); - } - - if (schedules.Count == 0) - _schedule.Remove(culture); - } - - /// - /// Returns all pending schedules based on the date and type provided - /// - /// - /// - /// - public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date) - { - return _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); - } - - /// - /// Gets the schedule for invariant content - /// - /// - public IEnumerable GetSchedule(ContentScheduleAction? action = null) - { - return GetSchedule(string.Empty, action); - } - - /// - /// Gets the schedule for a culture - /// - /// - /// - /// - public IEnumerable GetSchedule(string? culture, ContentScheduleAction? action = null) - { - if (culture is not null && _schedule.TryGetValue(culture, out var changes)) - return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value); - return Enumerable.Empty(); - } - - /// - /// Returns all schedules registered - /// - /// - public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList(); - - public object DeepClone() - { - var clone = new ContentScheduleCollection(); - foreach(var cultureSched in _schedule) - { - var list = new SortedList(); - foreach (var schedEntry in cultureSched.Value) - list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone()); - clone._schedule[cultureSched.Key] = list; - } - return clone; - } - - public override bool Equals(object? obj) - => obj is ContentScheduleCollection other && Equals(other); - - public bool Equals(ContentScheduleCollection? other) - { - if (other == null) return false; - - var thisSched = _schedule; - var thatSched = other._schedule; - - if (thisSched.Count != thatSched.Count) - return false; - - foreach (var (culture, thisList) in thisSched) - { - // if culture is missing, or actions differ, false - if (!thatSched.TryGetValue(culture, out var thatList) || !thatList.SequenceEqual(thisList)) - return false; - } - - return true; - } - - public static ContentScheduleCollection CreateWithEntry(DateTime? release, DateTime? expire) - { - var schedule = new ContentScheduleCollection(); - schedule.Add(string.Empty, release, expire); - return schedule; - } - - public static ContentScheduleCollection CreateWithEntry(string culture, DateTime? release, DateTime? expire) - { - var schedule = new ContentScheduleCollection(); - schedule.Add(culture, release, expire); - return schedule; - } + } + + /// + /// Clear all of the scheduled change type for invariant content + /// + /// + /// If specified, will clear all entries with dates less than or equal to the value + public void Clear(ContentScheduleAction action, DateTime? changeDate = null) => + Clear(string.Empty, action, changeDate); + + /// + /// Clear all of the scheduled change type for the culture + /// + /// + /// + /// If specified, will clear all entries with dates less than or equal to the value + public void Clear(string? culture, ContentScheduleAction action, DateTime? date = null) + { + if (culture is null || !_schedule.TryGetValue(culture, out SortedList? schedules)) + { + return; + } + + var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)) + .ToList(); + + foreach (KeyValuePair remove in removes) + { + var removed = schedules.Remove(remove.Value.Date); + if (!removed) + { + continue; + } + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value)); + } + + if (schedules.Count == 0) + { + _schedule.Remove(culture); + } + } + + /// + /// Returns all pending schedules based on the date and type provided + /// + /// + /// + /// + public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date) => + _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList(); + + /// + /// Gets the schedule for invariant content + /// + /// + public IEnumerable GetSchedule(ContentScheduleAction? action = null) => + GetSchedule(string.Empty, action); + + /// + /// Gets the schedule for a culture + /// + /// + /// + /// + public IEnumerable GetSchedule(string? culture, ContentScheduleAction? action = null) + { + if (culture is not null && _schedule.TryGetValue(culture, out SortedList? changes)) + { + return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value); + } + + return Enumerable.Empty(); + } + + public override bool Equals(object? obj) + => obj is ContentScheduleCollection other && Equals(other); + + public static ContentScheduleCollection CreateWithEntry(string culture, DateTime? release, DateTime? expire) + { + var schedule = new ContentScheduleCollection(); + schedule.Add(culture, release, expire); + return schedule; + } + + public override int GetHashCode() + { + throw new NotImplementedException(); } } diff --git a/src/Umbraco.Core/Models/ContentStatus.cs b/src/Umbraco.Core/Models/ContentStatus.cs index 15d5d59861..1fd1eeaa8a 100644 --- a/src/Umbraco.Core/Models/ContentStatus.cs +++ b/src/Umbraco.Core/Models/ContentStatus.cs @@ -1,46 +1,44 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Describes the states of a document, with regard to (schedule) publishing. +/// +[Serializable] +[DataContract] +public enum ContentStatus { + // typical flow: + // Unpublished (add release date)-> AwaitingRelease (release)-> Published (expire)-> Expired + /// - /// Describes the states of a document, with regard to (schedule) publishing. + /// The document is not trashed, and not published. /// - [Serializable] - [DataContract] - public enum ContentStatus - { - // typical flow: - // Unpublished (add release date)-> AwaitingRelease (release)-> Published (expire)-> Expired + [EnumMember] + Unpublished, - /// - /// The document is not trashed, and not published. - /// - [EnumMember] - Unpublished, + /// + /// The document is published. + /// + [EnumMember] + Published, - /// - /// The document is published. - /// - [EnumMember] - Published, + /// + /// The document is not trashed, not published, after being unpublished by a scheduled action. + /// + [EnumMember] + Expired, - /// - /// The document is not trashed, not published, after being unpublished by a scheduled action. - /// - [EnumMember] - Expired, + /// + /// The document is trashed. + /// + [EnumMember] + Trashed, - /// - /// The document is trashed. - /// - [EnumMember] - Trashed, - - /// - /// The document is not trashed, not published, and pending publication by a scheduled action. - /// - [EnumMember] - AwaitingRelease - } + /// + /// The document is not trashed, not published, and pending publication by a scheduled action. + /// + [EnumMember] + AwaitingRelease, } diff --git a/src/Umbraco.Core/Models/ContentTagsExtensions.cs b/src/Umbraco.Core/Models/ContentTagsExtensions.cs index 0dacd78844..1d52300460 100644 --- a/src/Umbraco.Core/Models/ContentTagsExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTagsExtensions.cs @@ -1,55 +1,74 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the class, to manage tags. +/// +public static class ContentTagsExtensions { /// - /// Provides extension methods for the class, to manage tags. + /// Assign tags. /// - public static class ContentTagsExtensions + /// The content item. + /// + /// The property alias. + /// The tags. + /// A value indicating whether to merge the tags with existing tags instead of replacing them. + /// A culture, for multi-lingual properties. + /// + /// + public static void AssignTags( + this IContentBase content, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + string propertyTypeAlias, + IEnumerable tags, + bool merge = false, + string? culture = null) => + content + .GetTagProperty(propertyTypeAlias) + .AssignTags(propertyEditors, dataTypeService, serializer, tags, merge, culture); + + /// + /// Remove tags. + /// + /// The content item. + /// + /// The property alias. + /// The tags. + /// A culture, for multi-lingual properties. + /// + /// + public static void RemoveTags( + this IContentBase content, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + string propertyTypeAlias, + IEnumerable tags, + string? culture = null) => + content.GetTagProperty(propertyTypeAlias) + .RemoveTags(propertyEditors, dataTypeService, serializer, tags, culture); + + // gets and validates the property + private static IProperty GetTagProperty(this IContentBase content, string propertyTypeAlias) { - /// - /// Assign tags. - /// - /// The content item. - /// - /// The property alias. - /// The tags. - /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// A culture, for multi-lingual properties. - /// - public static void AssignTags(this IContentBase content, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, string propertyTypeAlias, IEnumerable tags, bool merge = false, string? culture = null) + if (content == null) { - content.GetTagProperty(propertyTypeAlias).AssignTags(propertyEditors, dataTypeService, serializer, tags, merge, culture); + throw new ArgumentNullException(nameof(content)); } - /// - /// Remove tags. - /// - /// The content item. - /// - /// The property alias. - /// The tags. - /// A culture, for multi-lingual properties. - /// - public static void RemoveTags(this IContentBase content, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, string propertyTypeAlias, IEnumerable tags, string? culture = null) + IProperty? property = content.Properties[propertyTypeAlias]; + if (property != null) { - content.GetTagProperty(propertyTypeAlias).RemoveTags(propertyEditors, dataTypeService, serializer, tags, culture); + return property; } - // gets and validates the property - private static IProperty GetTagProperty(this IContentBase content, string propertyTypeAlias) - { - if (content == null) throw new ArgumentNullException(nameof(content)); - - var property = content.Properties[propertyTypeAlias]; - if (property != null) return property; - - throw new IndexOutOfRangeException($"Could not find a property with alias \"{propertyTypeAlias}\"."); - } + throw new IndexOutOfRangeException($"Could not find a property with alias \"{propertyTypeAlias}\"."); } } diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 9c21cf5e80..f4fe617a83 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -1,175 +1,172 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the content type that a object is based on +/// +[Serializable] +[DataContract(IsReference = true)] +public class ContentType : ContentTypeCompositionBase, IContentTypeWithHistoryCleanup { + public const bool SupportsPublishingConst = true; + + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> TemplateComparer = new( + (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), + templates => templates.GetHashCode()); + + private IEnumerable? _allowedTemplates; + + private int _defaultTemplate; + + private HistoryCleanup? _historyCleanup; + /// - /// Represents the content type that a object is based on + /// Constuctor for creating a ContentType with the parent's id. /// - [Serializable] - [DataContract(IsReference = true)] - public class ContentType : ContentTypeCompositionBase, IContentTypeWithHistoryCleanup + /// Only use this for creating ContentTypes at the root (with ParentId -1). + /// + /// + public ContentType(IShortStringHelper shortStringHelper, int parentId) + : base(shortStringHelper, parentId) { - public const bool SupportsPublishingConst = true; + _allowedTemplates = new List(); + HistoryCleanup = new HistoryCleanup(); + } - // Custom comparer for enumerable - private static readonly DelegateEqualityComparer> TemplateComparer = new ( - (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), - templates => templates.GetHashCode()); + /// + /// Constuctor for creating a ContentType with the parent as an inherited type. + /// + /// Use this to ensure inheritance from parent. + /// + /// + /// + public ContentType(IShortStringHelper shortStringHelper, IContentType parent, string alias) + : base(shortStringHelper, parent, alias) + { + _allowedTemplates = new List(); + HistoryCleanup = new HistoryCleanup(); + } - private IEnumerable? _allowedTemplates; + /// + public override bool SupportsPublishing => SupportsPublishingConst; - private int _defaultTemplate; + /// + /// Gets or sets the alias of the default Template. + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity + /// + [IgnoreDataMember] + public ITemplate? DefaultTemplate => + AllowedTemplates?.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); - /// - /// Constuctor for creating a ContentType with the parent's id. - /// - /// Only use this for creating ContentTypes at the root (with ParentId -1). - /// - public ContentType(IShortStringHelper shortStringHelper, int parentId) : base(shortStringHelper, parentId) + /// + public override ISimpleContentType ToSimple() => new SimpleContentType(this); + + [DataMember] + public int DefaultTemplateId + { + get => _defaultTemplate; + set => SetPropertyValueAndDetectChanges(value, ref _defaultTemplate, nameof(DefaultTemplateId)); + } + + /// + /// Gets or Sets a list of Templates which are allowed for the ContentType + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity + /// + [DataMember] + public IEnumerable? AllowedTemplates + { + get => _allowedTemplates; + set { - _allowedTemplates = new List(); - HistoryCleanup = new HistoryCleanup(); - } + SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer); - - /// - /// Constuctor for creating a ContentType with the parent as an inherited type. - /// - /// Use this to ensure inheritance from parent. - /// - /// - public ContentType(IShortStringHelper shortStringHelper, IContentType parent, string alias) - : base(shortStringHelper, parent, alias) - { - _allowedTemplates = new List(); - HistoryCleanup = new HistoryCleanup(); - } - - /// - public override bool SupportsPublishing => SupportsPublishingConst; - - /// - public override ISimpleContentType ToSimple() => new SimpleContentType(this); - - /// - /// Gets or sets the alias of the default Template. - /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! - /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, - /// we should not store direct entity - /// - [IgnoreDataMember] - public ITemplate? DefaultTemplate => - AllowedTemplates?.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); - - - [DataMember] - public int DefaultTemplateId - { - get => _defaultTemplate; - set => SetPropertyValueAndDetectChanges(value, ref _defaultTemplate, nameof(DefaultTemplateId)); - } - - /// - /// Gets or Sets a list of Templates which are allowed for the ContentType - /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! - /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, - /// we should not store direct entity - /// - [DataMember] - public IEnumerable? AllowedTemplates - { - get => _allowedTemplates; - set - { - SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer); - - if (_allowedTemplates?.Any(x => x.Id == _defaultTemplate) == false) - { - DefaultTemplateId = 0; - } - } - } - - private HistoryCleanup? _historyCleanup; - - public HistoryCleanup? HistoryCleanup - { - get => _historyCleanup; - set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup)); - } - - /// - /// Determines if AllowedTemplates contains templateId - /// - /// The template id to check - /// True if AllowedTemplates contains the templateId else False - public bool IsAllowedTemplate(int templateId) => - AllowedTemplates == null - ? false - : AllowedTemplates.Any(t => t.Id == templateId); - - /// - /// Determines if AllowedTemplates contains templateId - /// - /// The template alias to check - /// True if AllowedTemplates contains the templateAlias else False - public bool IsAllowedTemplate(string templateAlias) => - AllowedTemplates == null - ? false - : AllowedTemplates.Any(t => t.Alias.Equals(templateAlias, StringComparison.InvariantCultureIgnoreCase)); - - /// - /// Sets the default template for the ContentType - /// - /// Default - public void SetDefaultTemplate(ITemplate? template) - { - if (template == null) + if (_allowedTemplates?.Any(x => x.Id == _defaultTemplate) == false) { DefaultTemplateId = 0; - return; - } - - DefaultTemplateId = template.Id; - if (_allowedTemplates?.Any(x => x != null && x.Id == template.Id) == false) - { - var templates = AllowedTemplates?.ToList(); - templates?.Add(template); - AllowedTemplates = templates; } } - - /// - /// Removes a template from the list of allowed templates - /// - /// to remove - /// True if template was removed, otherwise False - public bool RemoveTemplate(ITemplate template) - { - if (DefaultTemplateId == template.Id) - { - DefaultTemplateId = default; - } - - var templates = AllowedTemplates?.ToList(); - ITemplate? remove = templates?.FirstOrDefault(x => x.Id == template.Id); - var result = remove is not null && templates is not null && templates.Remove(remove); - AllowedTemplates = templates; - - return result; - } - - /// - IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => - (IContentType)DeepCloneWithResetIdentities(newAlias); - - /// - public override bool IsDirty() => base.IsDirty() || (HistoryCleanup?.IsDirty() ?? false); } + + public HistoryCleanup? HistoryCleanup + { + get => _historyCleanup; + set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup)); + } + + /// + /// Determines if AllowedTemplates contains templateId + /// + /// The template id to check + /// True if AllowedTemplates contains the templateId else False + public bool IsAllowedTemplate(int templateId) => + AllowedTemplates == null + ? false + : AllowedTemplates.Any(t => t.Id == templateId); + + /// + /// Determines if AllowedTemplates contains templateId + /// + /// The template alias to check + /// True if AllowedTemplates contains the templateAlias else False + public bool IsAllowedTemplate(string templateAlias) => + AllowedTemplates == null + ? false + : AllowedTemplates.Any(t => t.Alias.Equals(templateAlias, StringComparison.InvariantCultureIgnoreCase)); + + /// + /// Sets the default template for the ContentType + /// + /// Default + public void SetDefaultTemplate(ITemplate? template) + { + if (template == null) + { + DefaultTemplateId = 0; + return; + } + + DefaultTemplateId = template.Id; + if (_allowedTemplates?.Any(x => x != null && x.Id == template.Id) == false) + { + var templates = AllowedTemplates?.ToList(); + templates?.Add(template); + AllowedTemplates = templates; + } + } + + /// + /// Removes a template from the list of allowed templates + /// + /// to remove + /// True if template was removed, otherwise False + public bool RemoveTemplate(ITemplate template) + { + if (DefaultTemplateId == template.Id) + { + DefaultTemplateId = default; + } + + var templates = AllowedTemplates?.ToList(); + ITemplate? remove = templates?.FirstOrDefault(x => x.Id == template.Id); + var result = remove is not null && templates is not null && templates.Remove(remove); + AllowedTemplates = templates; + + return result; + } + + /// + IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => + (IContentType)DeepCloneWithResetIdentities(newAlias); + + /// + public override bool IsDirty() => base.IsDirty() || (HistoryCleanup?.IsDirty() ?? false); } diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs index 529ae0bbe6..c4ab790dfe 100644 --- a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs @@ -1,17 +1,17 @@ -namespace Umbraco.Cms.Core.Models -{ - /// - /// Used when determining available compositions for a given content type - /// - public class ContentTypeAvailableCompositionsResult - { - public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) - { - Composition = composition; - Allowed = allowed; - } +namespace Umbraco.Cms.Core.Models; - public IContentTypeComposition Composition { get; private set; } - public bool Allowed { get; private set; } +/// +/// Used when determining available compositions for a given content type +/// +public class ContentTypeAvailableCompositionsResult +{ + public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) + { + Composition = composition; + Allowed = allowed; } + + public IContentTypeComposition Composition { get; } + + public bool Allowed { get; } } diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs index 180552cd74..4dc268faf3 100644 --- a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs @@ -1,26 +1,25 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Used when determining available compositions for a given content type +/// +public class ContentTypeAvailableCompositionsResults { - /// - /// Used when determining available compositions for a given content type - /// - public class ContentTypeAvailableCompositionsResults + public ContentTypeAvailableCompositionsResults() { - public ContentTypeAvailableCompositionsResults() - { - Ancestors = Enumerable.Empty(); - Results = Enumerable.Empty(); - } - - public ContentTypeAvailableCompositionsResults(IEnumerable ancestors, IEnumerable results) - { - Ancestors = ancestors; - Results = results; - } - - public IEnumerable Ancestors { get; private set; } - public IEnumerable Results { get; private set; } + Ancestors = Enumerable.Empty(); + Results = Enumerable.Empty(); } + + public ContentTypeAvailableCompositionsResults( + IEnumerable ancestors, + IEnumerable results) + { + Ancestors = ancestors; + Results = results; + } + + public IEnumerable Ancestors { get; } + + public IEnumerable Results { get; } } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index bf7cd0d8e3..6131e1b680 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -1,527 +1,542 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an abstract class for base ContentType properties and methods +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] +public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase { - /// - /// Represents an abstract class for base ContentType properties and methods - /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] - public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> ContentTypeSortComparer = + new( + (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable), + sorts => sorts.GetHashCode()); + + private readonly IShortStringHelper _shortStringHelper; + + private string _alias; + private bool _allowedAsRoot; // note: only one that's not 'pure element type' + private IEnumerable? _allowedContentTypes; + private string? _description; + private bool _hasPropertyTypeBeenRemoved; + private string? _icon = "icon-folder"; + private bool _isContainer; + private bool _isElement; + private PropertyGroupCollection _propertyGroups; + private string? _thumbnail = "folder.png"; + private ContentVariation _variations; + + protected ContentTypeBase(IShortStringHelper shortStringHelper, int parentId) { - private readonly IShortStringHelper _shortStringHelper; - - private string _alias; - private string? _description; - private string? _icon = "icon-folder"; - private string? _thumbnail = "folder.png"; - private bool _allowedAsRoot; // note: only one that's not 'pure element type' - private bool _isContainer; - private bool _isElement; - private PropertyGroupCollection _propertyGroups; - private PropertyTypeCollection _noGroupPropertyTypes; - private IEnumerable? _allowedContentTypes; - private bool _hasPropertyTypeBeenRemoved; - private ContentVariation _variations; - - protected ContentTypeBase(IShortStringHelper shortStringHelper, int parentId) + _alias = string.Empty; + _shortStringHelper = shortStringHelper; + if (parentId == 0) { - _alias = string.Empty; - _shortStringHelper = shortStringHelper; - if (parentId == 0) throw new ArgumentOutOfRangeException(nameof(parentId)); - ParentId = parentId; - - _allowedContentTypes = new List(); - _propertyGroups = new PropertyGroupCollection(); - - // actually OK as IsPublishing is constant - // ReSharper disable once VirtualMemberCallInConstructor - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; - - _variations = ContentVariation.Nothing; + throw new ArgumentOutOfRangeException(nameof(parentId)); } - protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent) - : this(shortStringHelper, parent, string.Empty) - { } + ParentId = parentId; - protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent, string alias) + _allowedContentTypes = new List(); + _propertyGroups = new PropertyGroupCollection(); + + // actually OK as IsPublishing is constant + // ReSharper disable once VirtualMemberCallInConstructor + PropertyTypeCollection = new PropertyTypeCollection(SupportsPublishing); + PropertyTypeCollection.CollectionChanged += PropertyTypesChanged; + + _variations = ContentVariation.Nothing; + } + + protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent) + : this(shortStringHelper, parent, string.Empty) + { + } + + protected ContentTypeBase(IShortStringHelper shortStringHelper, IContentTypeBase parent, string alias) + { + if (parent == null) { - if (parent == null) throw new ArgumentNullException(nameof(parent)); - SetParent(parent); - - _shortStringHelper = shortStringHelper; - _alias = alias; - _allowedContentTypes = new List(); - _propertyGroups = new PropertyGroupCollection(); - - // actually OK as IsPublishing is constant - // ReSharper disable once VirtualMemberCallInConstructor - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; - - _variations = ContentVariation.Nothing; + throw new ArgumentNullException(nameof(parent)); } - public abstract ISimpleContentType ToSimple(); + SetParent(parent); - /// - /// Gets a value indicating whether the content type supports publishing. - /// - /// - /// A publishing content type supports draft and published values for properties. - /// It is possible to retrieve either the draft (default) or published value of a property. - /// Setting the value always sets the draft value, which then needs to be published. - /// A non-publishing content type only supports one value for properties. Getting - /// the draft or published value of a property returns the same thing, and publishing - /// a value property has no effect. - /// - public abstract bool SupportsPublishing { get; } + _shortStringHelper = shortStringHelper; + _alias = alias; + _allowedContentTypes = new List(); + _propertyGroups = new PropertyGroupCollection(); - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> ContentTypeSortComparer = - new DelegateEqualityComparer>( - (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable), - sorts => sorts.GetHashCode()); + // actually OK as IsPublishing is constant + // ReSharper disable once VirtualMemberCallInConstructor + PropertyTypeCollection = new PropertyTypeCollection(SupportsPublishing); + PropertyTypeCollection.CollectionChanged += PropertyTypesChanged; - protected void PropertyGroupsChanged(object? sender, NotifyCollectionChangedEventArgs e) + _variations = ContentVariation.Nothing; + } + + /// + /// Gets a value indicating whether the content type supports publishing. + /// + /// + /// + /// A publishing content type supports draft and published values for properties. + /// It is possible to retrieve either the draft (default) or published value of a property. + /// Setting the value always sets the draft value, which then needs to be published. + /// + /// + /// A non-publishing content type only supports one value for properties. Getting + /// the draft or published value of a property returns the same thing, and publishing + /// a value property has no effect. + /// + /// + public abstract bool SupportsPublishing { get; } + + /// + /// The Alias of the ContentType + /// + [DataMember] + public virtual string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges( + value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), + ref _alias!, + nameof(Alias)); + } + + /// + /// A boolean flag indicating if a property type has been removed from this instance. + /// + /// + /// This is currently (specifically) used in order to know that we need to refresh the content cache which + /// needs to occur when a property has been removed from a content type + /// + [IgnoreDataMember] + internal bool HasPropertyTypeBeenRemoved + { + get => _hasPropertyTypeBeenRemoved; + private set { - OnPropertyChanged(nameof(PropertyGroups)); + _hasPropertyTypeBeenRemoved = value; + OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved)); } + } - protected void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) + /// + /// PropertyTypes that are not part of a PropertyGroup + /// + [IgnoreDataMember] + + // TODO: should we mark this as EditorBrowsable hidden since it really isn't ever used? + internal PropertyTypeCollection PropertyTypeCollection { get; private set; } + + public abstract ISimpleContentType ToSimple(); + + /// + /// Description for the ContentType + /// + [DataMember] + public string? Description + { + get => _description; + set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); + } + + /// + /// Name of the icon (sprite class) used to identify the ContentType + /// + [DataMember] + public string? Icon + { + get => _icon; + set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); + } + + /// + /// Name of the thumbnail used to identify the ContentType + /// + [DataMember] + public string? Thumbnail + { + get => _thumbnail; + set => SetPropertyValueAndDetectChanges(value, ref _thumbnail, nameof(Thumbnail)); + } + + /// + /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root + /// + [DataMember] + public bool AllowedAsRoot + { + get => _allowedAsRoot; + set => SetPropertyValueAndDetectChanges(value, ref _allowedAsRoot, nameof(AllowedAsRoot)); + } + + /// + /// Gets or Sets a boolean indicating whether this ContentType is a Container + /// + /// + /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. + /// + [DataMember] + public bool IsContainer + { + get => _isContainer; + set => SetPropertyValueAndDetectChanges(value, ref _isContainer, nameof(IsContainer)); + } + + /// + [DataMember] + public bool IsElement + { + get => _isElement; + set => SetPropertyValueAndDetectChanges(value, ref _isElement, nameof(IsElement)); + } + + /// + /// Gets or sets a list of integer Ids for allowed ContentTypes + /// + [DataMember] + public IEnumerable? AllowedContentTypes + { + get => _allowedContentTypes; + set => SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, nameof(AllowedContentTypes), ContentTypeSortComparer); + } + + /// + /// Gets or sets the content variation of the content type. + /// + public virtual ContentVariation Variations + { + get => _variations; + set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); + } + + /// + /// + /// A PropertyGroup corresponds to a Tab in the UI + /// Marked DoNotClone because we will manually deal with cloning and the event handlers + /// + [DataMember] + [DoNotClone] + public PropertyGroupCollection PropertyGroups + { + get => _propertyGroups; + set { - //enable this to detect duplicate property aliases. We do want this, however making this change in a - //patch release might be a little dangerous - - ////detect if there are any duplicate aliases - this cannot be allowed - //if (e.Action == NotifyCollectionChangedAction.Add - // || e.Action == NotifyCollectionChangedAction.Replace) - //{ - // var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); - // if (allAliases.HasDuplicates(false)) - // { - // var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); - // throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); - // } - //} - - OnPropertyChanged(nameof(PropertyTypes)); + _propertyGroups = value; + _propertyGroups.CollectionChanged += PropertyGroupsChanged; + PropertyGroupsChanged( + _propertyGroups, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + } - /// - /// The Alias of the ContentType - /// - [DataMember] - public virtual string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges( - value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), - ref _alias!, - nameof(Alias)); - } + /// + public bool SupportsVariation(string culture, string segment, bool wildcards = false) => - /// - /// Description for the ContentType - /// - [DataMember] - public string? Description - { - get => _description; - set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); - } + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + Variations.ValidateVariation(culture, segment, true, wildcards, false); - /// - /// Name of the icon (sprite class) used to identify the ContentType - /// - [DataMember] - public string? Icon - { - get => _icon; - set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); - } + /// + public bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false) => - /// - /// Name of the thumbnail used to identify the ContentType - /// - [DataMember] - public string? Thumbnail - { - get => _thumbnail; - set => SetPropertyValueAndDetectChanges(value, ref _thumbnail, nameof(Thumbnail)); - } + // non-exact validation: can accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + Variations.ValidateVariation(culture, segment, false, true, false); - /// - /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root - /// - [DataMember] - public bool AllowedAsRoot - { - get => _allowedAsRoot; - set => SetPropertyValueAndDetectChanges(value, ref _allowedAsRoot, nameof(AllowedAsRoot)); - } + /// + [IgnoreDataMember] + [DoNotClone] + public IEnumerable PropertyTypes => + PropertyTypeCollection.Union(PropertyGroups.SelectMany(x => x.PropertyTypes!)); - /// - /// Gets or Sets a boolean indicating whether this ContentType is a Container - /// - /// - /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. - /// - [DataMember] - public bool IsContainer + /// + [DoNotClone] + public IEnumerable NoGroupPropertyTypes + { + get => PropertyTypeCollection; + set { - get => _isContainer; - set => SetPropertyValueAndDetectChanges(value, ref _isContainer, nameof(IsContainer)); - } - - /// - [DataMember] - public bool IsElement - { - get => _isElement; - set => SetPropertyValueAndDetectChanges(value, ref _isElement, nameof(IsElement)); - } - - /// - /// Gets or sets a list of integer Ids for allowed ContentTypes - /// - [DataMember] - public IEnumerable? AllowedContentTypes - { - get => _allowedContentTypes; - set => SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, nameof(AllowedContentTypes), - ContentTypeSortComparer); - } - - /// - /// Gets or sets the content variation of the content type. - /// - public virtual ContentVariation Variations - { - get => _variations; - set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); - } - - /// - public bool SupportsVariation(string culture, string segment, bool wildcards = false) - { - // exact validation: cannot accept a 'null' culture if the property type varies - // by culture, and likewise for segment - // wildcard validation: can accept a '*' culture or segment - return Variations.ValidateVariation(culture, segment, true, wildcards, false); - } - - /// - public bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false) - { - // non-exact validation: can accept a 'null' culture if the property type varies - // by culture, and likewise for segment - // wildcard validation: can accept a '*' culture or segment - return Variations.ValidateVariation(culture, segment, false, true, false); - } - - /// - /// - /// A PropertyGroup corresponds to a Tab in the UI - /// Marked DoNotClone because we will manually deal with cloning and the event handlers - /// - [DataMember] - [DoNotClone] - public PropertyGroupCollection PropertyGroups - { - get => _propertyGroups; - set + if (PropertyTypeCollection != null) { - _propertyGroups = value; - _propertyGroups.CollectionChanged += PropertyGroupsChanged; - PropertyGroupsChanged(_propertyGroups, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - - /// - [IgnoreDataMember] - [DoNotClone] - public IEnumerable PropertyTypes - { - get - { - return _noGroupPropertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes!)); - } - } - - /// - [DoNotClone] - public IEnumerable NoGroupPropertyTypes - { - get => _noGroupPropertyTypes; - set - { - if (_noGroupPropertyTypes != null) - { - _noGroupPropertyTypes.ClearCollectionChangedEvents(); - } - - _noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing, value); - _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; - PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - - /// - /// A boolean flag indicating if a property type has been removed from this instance. - /// - /// - /// This is currently (specifically) used in order to know that we need to refresh the content cache which - /// needs to occur when a property has been removed from a content type - /// - [IgnoreDataMember] - internal bool HasPropertyTypeBeenRemoved - { - get => _hasPropertyTypeBeenRemoved; - private set - { - _hasPropertyTypeBeenRemoved = value; - OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved)); - } - } - - /// - /// Checks whether a PropertyType with a given alias already exists - /// - /// Alias of the PropertyType - /// Returns True if a PropertyType with the passed in alias exists, otherwise False - public abstract bool PropertyTypeExists(string? alias); - - /// - public abstract bool AddPropertyGroup(string alias, string name); - - /// - public abstract bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); - - /// - /// Adds a PropertyType, which does not belong to a PropertyGroup. - /// - /// to add - /// Returns True if PropertyType was added, otherwise False - public bool AddPropertyType(IPropertyType propertyType) - { - if (PropertyTypeExists(propertyType.Alias) == false) - { - _noGroupPropertyTypes.Add(propertyType); - return true; + PropertyTypeCollection.ClearCollectionChangedEvents(); } - return false; + PropertyTypeCollection = new PropertyTypeCollection(SupportsPublishing, value); + PropertyTypeCollection.CollectionChanged += PropertyTypesChanged; + PropertyTypesChanged( + PropertyTypeCollection, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + } - /// - /// Moves a PropertyType to a specified PropertyGroup - /// - /// Alias of the PropertyType to move - /// Alias of the PropertyGroup to move the PropertyType to - /// - /// If is null then the property is moved back to - /// "generic properties" ie does not have a tab anymore. - public bool MovePropertyType(string propertyTypeAlias, string propertyGroupAlias) + /// + /// Checks whether a PropertyType with a given alias already exists + /// + /// Alias of the PropertyType + /// Returns True if a PropertyType with the passed in alias exists, otherwise False + public abstract bool PropertyTypeExists(string? alias); + + /// + public abstract bool AddPropertyGroup(string alias, string name); + + /// + public abstract bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); + + /// + /// Adds a PropertyType, which does not belong to a PropertyGroup. + /// + /// to add + /// Returns True if PropertyType was added, otherwise False + public bool AddPropertyType(IPropertyType propertyType) + { + if (PropertyTypeExists(propertyType.Alias) == false) { - // get property, ensure it exists - var propertyType = PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); - if (propertyType == null) return false; - - // get new group, if required, and ensure it exists - PropertyGroup? newPropertyGroup = null; - if (propertyGroupAlias != null) - { - var index = PropertyGroups.IndexOfKey(propertyGroupAlias); - if (index == -1) return false; - - newPropertyGroup = PropertyGroups[index]; - } - - // get old group - var oldPropertyGroup = PropertyGroups.FirstOrDefault(x => - x.PropertyTypes?.Any(y => y.Alias == propertyTypeAlias) ?? false); - - // set new group - propertyType.PropertyGroupId = newPropertyGroup == null ? null : new Lazy(() => newPropertyGroup.Id, false); - - // remove from old group, if any - add to new group, if any - oldPropertyGroup?.PropertyTypes?.RemoveItem(propertyTypeAlias); - newPropertyGroup?.PropertyTypes?.Add(propertyType); - + PropertyTypeCollection.Add(propertyType); return true; } - /// - /// Removes a PropertyType from the current ContentType - /// - /// Alias of the to remove - public void RemovePropertyType(string alias) + return false; + } + + /// + /// Moves a PropertyType to a specified PropertyGroup + /// + /// Alias of the PropertyType to move + /// Alias of the PropertyGroup to move the PropertyType to + /// + /// + /// If is null then the property is moved back to + /// "generic properties" ie does not have a tab anymore. + /// + public bool MovePropertyType(string propertyTypeAlias, string propertyGroupAlias) + { + // get property, ensure it exists + IPropertyType? propertyType = PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); + if (propertyType == null) { - //check through each property group to see if we can remove the property type by alias from it - foreach (var propertyGroup in PropertyGroups) + return false; + } + + // get new group, if required, and ensure it exists + PropertyGroup? newPropertyGroup = null; + if (propertyGroupAlias != null) + { + var index = PropertyGroups.IndexOfKey(propertyGroupAlias); + if (index == -1) { - if (propertyGroup.PropertyTypes?.RemoveItem(alias) ?? false) - { - if (!HasPropertyTypeBeenRemoved) - { - HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(nameof(PropertyTypes)); - } - break; - } + return false; } - //check through each local property type collection (not assigned to a tab) - if (_noGroupPropertyTypes.RemoveItem(alias)) + newPropertyGroup = PropertyGroups[index]; + } + + // get old group + PropertyGroup? oldPropertyGroup = PropertyGroups.FirstOrDefault(x => x.PropertyTypes?.Any(y => y.Alias == propertyTypeAlias) ?? false); + + // set new group + propertyType.PropertyGroupId = + newPropertyGroup == null ? null : new Lazy(() => newPropertyGroup.Id, false); + + // remove from old group, if any - add to new group, if any + oldPropertyGroup?.PropertyTypes?.RemoveItem(propertyTypeAlias); + newPropertyGroup?.PropertyTypes?.Add(propertyType); + + return true; + } + + /// + /// Removes a PropertyType from the current ContentType + /// + /// Alias of the to remove + public void RemovePropertyType(string alias) + { + // check through each property group to see if we can remove the property type by alias from it + foreach (PropertyGroup propertyGroup in PropertyGroups) + { + if (propertyGroup.PropertyTypes?.RemoveItem(alias) ?? false) { if (!HasPropertyTypeBeenRemoved) { HasPropertyTypeBeenRemoved = true; OnPropertyChanged(nameof(PropertyTypes)); } + + break; } } - /// - /// Removes a PropertyGroup from the current ContentType - /// - /// Alias of the to remove - public void RemovePropertyGroup(string alias) + // check through each local property type collection (not assigned to a tab) + if (PropertyTypeCollection.RemoveItem(alias)) { - // if no group exists with that alias, do nothing - var index = PropertyGroups.IndexOfKey(alias); - if (index == -1) return; - - var group = PropertyGroups[index]; - - // first remove the group - PropertyGroups.Remove(group); - - if (group.PropertyTypes is not null) + if (!HasPropertyTypeBeenRemoved) { - // Then re-assign the group's properties to no group - foreach (var property in group.PropertyTypes) + HasPropertyTypeBeenRemoved = true; + OnPropertyChanged(nameof(PropertyTypes)); + } + } + } + + /// + /// Removes a PropertyGroup from the current ContentType + /// + /// Alias of the to remove + public void RemovePropertyGroup(string alias) + { + // if no group exists with that alias, do nothing + var index = PropertyGroups.IndexOfKey(alias); + if (index == -1) + { + return; + } + + PropertyGroup group = PropertyGroups[index]; + + // first remove the group + PropertyGroups.Remove(group); + + if (group.PropertyTypes is not null) + { + // Then re-assign the group's properties to no group + foreach (IPropertyType property in group.PropertyTypes) + { + property.PropertyGroupId = null; + PropertyTypeCollection.Add(property); + } + } + + OnPropertyChanged(nameof(PropertyGroups)); + } + + /// + /// Indicates whether the current entity is dirty. + /// + /// True if entity is dirty, otherwise False + public override bool IsDirty() + { + var dirtyEntity = base.IsDirty(); + + var dirtyGroups = PropertyGroups.Any(x => x.IsDirty()); + var dirtyTypes = PropertyTypes.Any(x => x.IsDirty()); + + return dirtyEntity || dirtyGroups || dirtyTypes; + } + + /// + /// Resets dirty properties by clearing the dictionary used to track changes. + /// + /// + /// Please note that resetting the dirty properties could potentially + /// obstruct the saving of a new or updated entity. + /// + public override void ResetDirtyProperties() + { + base.ResetDirtyProperties(); + + // loop through each property group to reset the property types + var propertiesReset = new List(); + + foreach (PropertyGroup propertyGroup in PropertyGroups) + { + propertyGroup.ResetDirtyProperties(); + if (propertyGroup.PropertyTypes is not null) + { + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) { - property.PropertyGroupId = null; - _noGroupPropertyTypes.Add(property); + propertyType.ResetDirtyProperties(); + propertiesReset.Add(propertyType.Id); } } - - OnPropertyChanged(nameof(PropertyGroups)); } - /// - /// PropertyTypes that are not part of a PropertyGroup - /// - [IgnoreDataMember] - // TODO: should we mark this as EditorBrowsable hidden since it really isn't ever used? - internal PropertyTypeCollection PropertyTypeCollection => _noGroupPropertyTypes; - - /// - /// Indicates whether the current entity is dirty. - /// - /// True if entity is dirty, otherwise False - public override bool IsDirty() + // then loop through our property type collection since some might not exist on a property group + // but don't re-reset ones we've already done. + foreach (IPropertyType propertyType in PropertyTypes.Where(x => propertiesReset.Contains(x.Id) == false)) { - bool dirtyEntity = base.IsDirty(); + propertyType.ResetDirtyProperties(); + } + } - bool dirtyGroups = PropertyGroups.Any(x => x.IsDirty()); - bool dirtyTypes = PropertyTypes.Any(x => x.IsDirty()); - - return dirtyEntity || dirtyGroups || dirtyTypes; + public ContentTypeBase DeepCloneWithResetIdentities(string alias) + { + var clone = (ContentTypeBase)DeepClone(); + clone.Alias = alias; + clone.Key = Guid.Empty; + foreach (PropertyGroup propertyGroup in clone.PropertyGroups) + { + propertyGroup.ResetIdentity(); + propertyGroup.ResetDirtyProperties(false); } - /// - /// Resets dirty properties by clearing the dictionary used to track changes. - /// - /// - /// Please note that resetting the dirty properties could potentially - /// obstruct the saving of a new or updated entity. - /// - public override void ResetDirtyProperties() + foreach (IPropertyType propertyType in clone.PropertyTypes) { - base.ResetDirtyProperties(); - - //loop through each property group to reset the property types - var propertiesReset = new List(); - - foreach (var propertyGroup in PropertyGroups) - { - propertyGroup.ResetDirtyProperties(); - if (propertyGroup.PropertyTypes is not null) - { - foreach (var propertyType in propertyGroup.PropertyTypes) - { - propertyType.ResetDirtyProperties(); - propertiesReset.Add(propertyType.Id); - } - } - } - - //then loop through our property type collection since some might not exist on a property group - //but don't re-reset ones we've already done. - foreach (var propertyType in PropertyTypes.Where(x => propertiesReset.Contains(x.Id) == false)) - { - propertyType.ResetDirtyProperties(); - } + propertyType.ResetIdentity(); + propertyType.ResetDirtyProperties(false); } - protected override void PerformDeepClone(object clone) + clone.ResetIdentity(); + clone.ResetDirtyProperties(false); + return clone; + } + + protected void PropertyGroupsChanged(object? sender, NotifyCollectionChangedEventArgs e) => + OnPropertyChanged(nameof(PropertyGroups)); + + protected void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) => + + // enable this to detect duplicate property aliases. We do want this, however making this change in a + // patch release might be a little dangerous + ////detect if there are any duplicate aliases - this cannot be allowed + // if (e.Action == NotifyCollectionChangedAction.Add + // || e.Action == NotifyCollectionChangedAction.Replace) + // { + // var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); + // if (allAliases.HasDuplicates(false)) + // { + // var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); + // throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); + // } + // } + OnPropertyChanged(nameof(PropertyTypes)); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (ContentTypeBase)clone; + + if (clonedEntity.PropertyTypeCollection != null) { - base.PerformDeepClone(clone); - - var clonedEntity = (ContentTypeBase) clone; - - if (clonedEntity._noGroupPropertyTypes != null) - { - //need to manually wire up the event handlers for the property type collections - we've ensured - // its ignored from the auto-clone process because its return values are unions, not raw and - // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 - - clonedEntity._noGroupPropertyTypes.ClearCollectionChangedEvents(); //clear this event handler if any - clonedEntity._noGroupPropertyTypes = (PropertyTypeCollection) _noGroupPropertyTypes.DeepClone(); //manually deep clone - clonedEntity._noGroupPropertyTypes.CollectionChanged += clonedEntity.PropertyTypesChanged; //re-assign correct event handler - } - - if (clonedEntity._propertyGroups != null) - { - clonedEntity._propertyGroups.ClearCollectionChangedEvents(); //clear this event handler if any - clonedEntity._propertyGroups = (PropertyGroupCollection) _propertyGroups.DeepClone(); //manually deep clone - clonedEntity._propertyGroups.CollectionChanged += clonedEntity.PropertyGroupsChanged; //re-assign correct event handler - } + // need to manually wire up the event handlers for the property type collections - we've ensured + // its ignored from the auto-clone process because its return values are unions, not raw and + // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 + clonedEntity.PropertyTypeCollection.ClearCollectionChangedEvents(); // clear this event handler if any + clonedEntity.PropertyTypeCollection = + (PropertyTypeCollection)PropertyTypeCollection.DeepClone(); // manually deep clone + clonedEntity.PropertyTypeCollection.CollectionChanged += + clonedEntity.PropertyTypesChanged; // re-assign correct event handler } - public ContentTypeBase DeepCloneWithResetIdentities(string alias) + if (clonedEntity._propertyGroups != null) { - var clone = (ContentTypeBase)DeepClone(); - clone.Alias = alias; - clone.Key = Guid.Empty; - foreach (var propertyGroup in clone.PropertyGroups) - { - propertyGroup.ResetIdentity(); - propertyGroup.ResetDirtyProperties(false); - } - foreach (var propertyType in clone.PropertyTypes) - { - propertyType.ResetIdentity(); - propertyType.ResetDirtyProperties(false); - } - - clone.ResetIdentity(); - clone.ResetDirtyProperties(false); - return clone; + clonedEntity._propertyGroups.ClearCollectionChangedEvents(); // clear this event handler if any + clonedEntity._propertyGroups = (PropertyGroupCollection)_propertyGroups.DeepClone(); // manually deep clone + clonedEntity._propertyGroups.CollectionChanged += + clonedEntity.PropertyGroupsChanged; // re-assign correct event handler } } } diff --git a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs index d771efa12b..12e0e5a138 100644 --- a/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeBaseExtensions.cs @@ -1,64 +1,79 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extensions methods for . +/// +public static class ContentTypeBaseExtensions { - /// - /// Provides extensions methods for . - /// - public static class ContentTypeBaseExtensions + public static PublishedItemType GetItemType(this IContentTypeBase contentType) { - public static PublishedItemType GetItemType(this IContentTypeBase contentType) + Type type = contentType.GetType(); + PublishedItemType itemType = PublishedItemType.Unknown; + if (contentType.IsElement) { - var type = contentType.GetType(); - var itemType = PublishedItemType.Unknown; - if (contentType.IsElement) itemType = PublishedItemType.Element; - else if (typeof(IContentType).IsAssignableFrom(type)) itemType = PublishedItemType.Content; - else if (typeof(IMediaType).IsAssignableFrom(type)) itemType = PublishedItemType.Media; - else if (typeof(IMemberType).IsAssignableFrom(type)) itemType = PublishedItemType.Member; - return itemType; + itemType = PublishedItemType.Element; + } + else if (typeof(IContentType).IsAssignableFrom(type)) + { + itemType = PublishedItemType.Content; + } + else if (typeof(IMediaType).IsAssignableFrom(type)) + { + itemType = PublishedItemType.Media; + } + else if (typeof(IMemberType).IsAssignableFrom(type)) + { + itemType = PublishedItemType.Member; } - /// - /// Used to check if any property type was changed between variant/invariant - /// - /// - /// - public static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType) - { - return contentType.WasPropertyTypeVariationChanged(out var _); - } + return itemType; + } - /// - /// Used to check if any property type was changed between variant/invariant - /// - /// - /// - internal static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType, out IReadOnlyCollection aliases) - { - var a = new List(); + /// + /// Used to check if any property type was changed between variant/invariant + /// + /// + /// + public static bool WasPropertyTypeVariationChanged(this IContentTypeBase contentType) => + contentType.WasPropertyTypeVariationChanged(out IReadOnlyCollection _); - // property variation change? - var hasAnyPropertyVariationChanged = contentType.PropertyTypes.Any(propertyType => + /// + /// Used to check if any property type was changed between variant/invariant + /// + /// + /// + /// + internal static bool WasPropertyTypeVariationChanged( + this IContentTypeBase contentType, + out IReadOnlyCollection aliases) + { + var a = new List(); + + // property variation change? + var hasAnyPropertyVariationChanged = contentType.PropertyTypes.Any(propertyType => + { + // skip new properties + // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly + var isNewProperty = propertyType.WasPropertyDirty("Id"); + if (isNewProperty) { - // skip new properties - // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly - var isNewProperty = propertyType.WasPropertyDirty("Id"); - if (isNewProperty) return false; + return false; + } - // variation change? - var dirty = propertyType.WasPropertyDirty("Variations"); - if (dirty) - a.Add(propertyType.Alias); + // variation change? + var dirty = propertyType.WasPropertyDirty("Variations"); + if (dirty) + { + a.Add(propertyType.Alias); + } - return dirty; + return dirty; + }); - }); - - aliases = a; - return hasAnyPropertyVariationChanged; - } + aliases = a; + return hasAnyPropertyVariationChanged; } } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 18dc1189f2..b7b9af6231 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -1,319 +1,338 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an abstract class for composition specific ContentType properties and methods +/// +[Serializable] +[DataContract(IsReference = true)] +public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentTypeComposition { - /// - /// Represents an abstract class for composition specific ContentType properties and methods - /// - [Serializable] - [DataContract(IsReference = true)] - public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentTypeComposition + private List _contentTypeComposition = new(); + private List _removedContentTypeKeyTracker = new(); + + protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId) + : base(shortStringHelper, parentId) { - private List _contentTypeComposition = new List(); - private List _removedContentTypeKeyTracker = new List(); + } - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId) - : base(shortStringHelper, parentId) - { } + protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, IContentTypeComposition parent) + : this(shortStringHelper, parent, string.Empty) + { + } - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper,IContentTypeComposition parent) - : this(shortStringHelper, parent, string.Empty) - { } + protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) + : base(shortStringHelper, parent, alias) => + AddContentType(parent); - protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) - : base(shortStringHelper, parent, alias) + public IEnumerable RemovedContentTypes => _removedContentTypeKeyTracker; + + /// + /// Gets or sets the content types that compose this content type. + /// + [DataMember] + public IEnumerable ContentTypeComposition + { + get => _contentTypeComposition; + set { - AddContentType(parent); + _contentTypeComposition = value.ToList(); + OnPropertyChanged(nameof(ContentTypeComposition)); } + } - public IEnumerable RemovedContentTypes => _removedContentTypeKeyTracker; - - /// - /// Gets or sets the content types that compose this content type. - /// - [DataMember] - public IEnumerable ContentTypeComposition + /// + [IgnoreDataMember] + public IEnumerable CompositionPropertyGroups + { + get { - get => _contentTypeComposition; - set + // we need to "acquire" composition groups and properties here, ie get our own clones, + // so that we can change their variation according to this content type variations. + // + // it would be nice to cache the resulting enumerable, but alas we cannot, otherwise + // any change to compositions are ignored and that breaks many things - and tracking + // changes to refresh the cache would be expensive. + void AcquireProperty(IPropertyType propertyType) { - _contentTypeComposition = value.ToList(); - OnPropertyChanged(nameof(ContentTypeComposition)); + propertyType.Variations &= Variations; + propertyType.ResetDirtyProperties(false); } - } - /// - [IgnoreDataMember] - public IEnumerable CompositionPropertyGroups - { - get - { - // we need to "acquire" composition groups and properties here, ie get our own clones, - // so that we can change their variation according to this content type variations. - // - // it would be nice to cache the resulting enumerable, but alas we cannot, otherwise - // any change to compositions are ignored and that breaks many things - and tracking - // changes to refresh the cache would be expensive. - - void AcquireProperty(IPropertyType propertyType) + return PropertyGroups.Union(ContentTypeComposition.SelectMany(x => x.CompositionPropertyGroups) + .Select(group => { - propertyType.Variations &= Variations; - propertyType.ResetDirtyProperties(false); - } - - return PropertyGroups.Union(ContentTypeComposition.SelectMany(x => x.CompositionPropertyGroups) - .Select(group => + group = (PropertyGroup)group.DeepClone(); + if (group.PropertyTypes is not null) { - group = (PropertyGroup) group.DeepClone(); - if (group.PropertyTypes is not null) + foreach (IPropertyType property in group.PropertyTypes) { - foreach (var property in group.PropertyTypes) - AcquireProperty(property); + AcquireProperty(property); } - return group; - })); - } + } + + return group; + })); } + } - /// - [IgnoreDataMember] - public IEnumerable CompositionPropertyTypes + /// + [IgnoreDataMember] + public IEnumerable CompositionPropertyTypes + { + get { - get + // we need to "acquire" composition properties here, ie get our own clones, + // so that we can change their variation according to this content type variations. + // + // see note in CompositionPropertyGroups for comments on caching the resulting enumerable + IPropertyType AcquireProperty(IPropertyType propertyType) { - // we need to "acquire" composition properties here, ie get our own clones, - // so that we can change their variation according to this content type variations. - // - // see note in CompositionPropertyGroups for comments on caching the resulting enumerable - - IPropertyType AcquireProperty(IPropertyType propertyType) - { - propertyType = (IPropertyType) propertyType.DeepClone(); - propertyType.Variations &= Variations; - propertyType.ResetDirtyProperties(false); - return propertyType; - } - - return ContentTypeComposition - .SelectMany(x => x.CompositionPropertyTypes) - .Select(AcquireProperty) - .Union(PropertyTypes); + propertyType = (IPropertyType)propertyType.DeepClone(); + propertyType.Variations &= Variations; + propertyType.ResetDirtyProperties(false); + return propertyType; } + + return ContentTypeComposition + .SelectMany(x => x.CompositionPropertyTypes) + .Select(AcquireProperty) + .Union(PropertyTypes); } + } - /// - public IEnumerable GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes(); + /// + public IEnumerable GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes(); - private IEnumerable GetRawComposedPropertyTypes(bool start = true) + /// + /// Adds a content type to the composition. + /// + /// The content type to add. + /// True if the content type was added, otherwise false. + public bool AddContentType(IContentTypeComposition? contentType) + { + if (contentType is null) { - var propertyTypes = ContentTypeComposition - .Cast() - .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes); - - if (!start) - propertyTypes = propertyTypes.Union(PropertyTypes); - - return propertyTypes; - } - - /// - /// Adds a content type to the composition. - /// - /// The content type to add. - /// True if the content type was added, otherwise false. - public bool AddContentType(IContentTypeComposition? contentType) - { - if (contentType is null) - { - return false; - } - if (contentType.ContentTypeComposition.Any(x => x.CompositionAliases().Any(ContentTypeCompositionExists))) - return false; - - if (string.IsNullOrEmpty(Alias) == false && Alias.Equals(contentType.Alias)) - return false; - - if (ContentTypeCompositionExists(contentType.Alias) == false) - { - // Before we actually go ahead and add the ContentType as a Composition we ensure that we don't - // end up with duplicate PropertyType aliases - in which case we throw an exception. - var conflictingPropertyTypeAliases = CompositionPropertyTypes.SelectMany( - x => contentType.CompositionPropertyTypes - .Where(y => y.Alias.Equals(x.Alias, StringComparison.InvariantCultureIgnoreCase)) - .Select(p => p.Alias)).ToList(); - - if (conflictingPropertyTypeAliases.Any()) - throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); - - _contentTypeComposition.Add(contentType); - - OnPropertyChanged(nameof(ContentTypeComposition)); - - return true; - } - return false; } - /// - /// Removes a content type with a specified alias from the composition. - /// - /// The alias of the content type to remove. - /// True if the content type was removed, otherwise false. - public bool RemoveContentType(string alias) + if (contentType.ContentTypeComposition.Any(x => x.CompositionAliases().Any(ContentTypeCompositionExists))) { - if (ContentTypeCompositionExists(alias)) - { - var contentTypeComposition = ContentTypeComposition.FirstOrDefault(x => x.Alias == alias); - if (contentTypeComposition == null) // You can't remove a composition from another composition - return false; - - _removedContentTypeKeyTracker.Add(contentTypeComposition.Id); - - // If the ContentType we are removing has Compositions of its own these needs to be removed as well - var compositionIdsToRemove = contentTypeComposition.CompositionIds().ToList(); - if (compositionIdsToRemove.Any()) - _removedContentTypeKeyTracker.AddRange(compositionIdsToRemove); - - OnPropertyChanged(nameof(ContentTypeComposition)); - - return _contentTypeComposition.Remove(contentTypeComposition); - } - return false; } - /// - /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes - /// - /// Alias of a - /// True if ContentType with alias exists, otherwise returns False - public bool ContentTypeCompositionExists(string alias) + if (string.IsNullOrEmpty(Alias) == false && Alias.Equals(contentType.Alias)) { - if (ContentTypeComposition.Any(x => x.Alias.Equals(alias))) - return true; - - if (ContentTypeComposition.Any(x => x.ContentTypeCompositionExists(alias))) - return true; - return false; } - /// - /// Checks whether a PropertyType with a given alias already exists - /// - /// Alias of the PropertyType - /// Returns True if a PropertyType with the passed in alias exists, otherwise False - public override bool PropertyTypeExists(string? alias) => CompositionPropertyTypes.Any(x => x.Alias == alias); - - /// - public override bool AddPropertyGroup(string alias, string name) => AddAndReturnPropertyGroup(alias, name) != null; - - private PropertyGroup? AddAndReturnPropertyGroup(string alias, string name) + if (ContentTypeCompositionExists(contentType.Alias) == false) { - // Ensure we don't have it already - if (PropertyGroups.Contains(alias)) - return null; + // Before we actually go ahead and add the ContentType as a Composition we ensure that we don't + // end up with duplicate PropertyType aliases - in which case we throw an exception. + var conflictingPropertyTypeAliases = CompositionPropertyTypes.SelectMany( + x => contentType.CompositionPropertyTypes + .Where(y => y.Alias.Equals(x.Alias, StringComparison.InvariantCultureIgnoreCase)) + .Select(p => p.Alias)).ToList(); - // Add new group - var group = new PropertyGroup(SupportsPublishing) + if (conflictingPropertyTypeAliases.Any()) { - Alias = alias, - Name = name - }; - - // check if it is inherited - there might be more than 1 but we want the 1st, to - // reuse its sort order - if there are more than 1 and they have different sort - // orders... there isn't much we can do anyways - var inheritGroup = CompositionPropertyGroups.FirstOrDefault(x => x.Alias == alias); - if (inheritGroup == null) - { - // no, just local, set sort order - var lastGroup = PropertyGroups.LastOrDefault(); - if (lastGroup != null) - group.SortOrder = lastGroup.SortOrder + 1; - } - else - { - // yes, inherited, re-use sort order - group.SortOrder = inheritGroup.SortOrder; + throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); } - // add - PropertyGroups.Add(group); + _contentTypeComposition.Add(contentType); - return group; - } - - /// - public override bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null) - { - // ensure no duplicate alias - over all composition properties - if (PropertyTypeExists(propertyType.Alias)) - return false; - - // get and ensure a group local to this content type - PropertyGroup? group; - var index = PropertyGroups.IndexOfKey(propertyGroupAlias); - if (index != -1) - { - group = PropertyGroups[index]; - } - else if (!string.IsNullOrEmpty(propertyGroupName)) - { - group = AddAndReturnPropertyGroup(propertyGroupAlias, propertyGroupName); - if (group == null) - { - return false; - } - } - else - { - // No group name specified, so we can't create a new one and add the property type - return false; - } - - // add property to group - propertyType.PropertyGroupId = new Lazy(() => group.Id); - group.PropertyTypes?.Add(propertyType); + OnPropertyChanged(nameof(ContentTypeComposition)); return true; } - /// - /// Gets a list of ContentType aliases from the current composition - /// - /// An enumerable list of string aliases - /// Does not contain the alias of the Current ContentType - public IEnumerable CompositionAliases() - => ContentTypeComposition - .Select(x => x.Alias) - .Union(ContentTypeComposition.SelectMany(x => x.CompositionAliases())); + return false; + } - /// - /// Gets a list of ContentType Ids from the current composition - /// - /// An enumerable list of integer ids - /// Does not contain the Id of the Current ContentType - public IEnumerable CompositionIds() - => ContentTypeComposition - .Select(x => x.Id) - .Union(ContentTypeComposition.SelectMany(x => x.CompositionIds())); - - protected override void PerformDeepClone(object clone) + /// + /// Removes a content type with a specified alias from the composition. + /// + /// The alias of the content type to remove. + /// True if the content type was removed, otherwise false. + public bool RemoveContentType(string alias) + { + if (ContentTypeCompositionExists(alias)) { - base.PerformDeepClone(clone); + IContentTypeComposition? contentTypeComposition = ContentTypeComposition.FirstOrDefault(x => x.Alias == alias); - var clonedEntity = (ContentTypeCompositionBase)clone; + // You can't remove a composition from another composition + if (contentTypeComposition == null) + { + return false; + } - // need to manually assign since this is an internal field and will not be automatically mapped - clonedEntity._removedContentTypeKeyTracker = new List(); - clonedEntity._contentTypeComposition = ContentTypeComposition.Select(x => (IContentTypeComposition)x.DeepClone()).ToList(); + _removedContentTypeKeyTracker.Add(contentTypeComposition.Id); + + // If the ContentType we are removing has Compositions of its own these needs to be removed as well + var compositionIdsToRemove = contentTypeComposition.CompositionIds().ToList(); + if (compositionIdsToRemove.Any()) + { + _removedContentTypeKeyTracker.AddRange(compositionIdsToRemove); + } + + OnPropertyChanged(nameof(ContentTypeComposition)); + + return _contentTypeComposition.Remove(contentTypeComposition); } + + return false; + } + + /// + /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes + /// + /// Alias of a + /// True if ContentType with alias exists, otherwise returns False + public bool ContentTypeCompositionExists(string alias) + { + if (ContentTypeComposition.Any(x => x.Alias.Equals(alias))) + { + return true; + } + + if (ContentTypeComposition.Any(x => x.ContentTypeCompositionExists(alias))) + { + return true; + } + + return false; + } + + /// + /// Checks whether a PropertyType with a given alias already exists + /// + /// Alias of the PropertyType + /// Returns True if a PropertyType with the passed in alias exists, otherwise False + public override bool PropertyTypeExists(string? alias) => CompositionPropertyTypes.Any(x => x.Alias == alias); + + /// + public override bool AddPropertyGroup(string alias, string name) => AddAndReturnPropertyGroup(alias, name) != null; + + /// + public override bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null) + { + // ensure no duplicate alias - over all composition properties + if (PropertyTypeExists(propertyType.Alias)) + { + return false; + } + + // get and ensure a group local to this content type + PropertyGroup? group; + var index = PropertyGroups.IndexOfKey(propertyGroupAlias); + if (index != -1) + { + group = PropertyGroups[index]; + } + else if (!string.IsNullOrEmpty(propertyGroupName)) + { + group = AddAndReturnPropertyGroup(propertyGroupAlias, propertyGroupName); + if (group == null) + { + return false; + } + } + else + { + // No group name specified, so we can't create a new one and add the property type + return false; + } + + // add property to group + propertyType.PropertyGroupId = new Lazy(() => group.Id); + group.PropertyTypes?.Add(propertyType); + + return true; + } + + /// + /// Gets a list of ContentType aliases from the current composition + /// + /// An enumerable list of string aliases + /// Does not contain the alias of the Current ContentType + public IEnumerable CompositionAliases() + => ContentTypeComposition + .Select(x => x.Alias) + .Union(ContentTypeComposition.SelectMany(x => x.CompositionAliases())); + + /// + /// Gets a list of ContentType Ids from the current composition + /// + /// An enumerable list of integer ids + /// Does not contain the Id of the Current ContentType + public IEnumerable CompositionIds() + => ContentTypeComposition + .Select(x => x.Id) + .Union(ContentTypeComposition.SelectMany(x => x.CompositionIds())); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (ContentTypeCompositionBase)clone; + + // need to manually assign since this is an internal field and will not be automatically mapped + clonedEntity._removedContentTypeKeyTracker = new List(); + clonedEntity._contentTypeComposition = + ContentTypeComposition.Select(x => (IContentTypeComposition)x.DeepClone()).ToList(); + } + + private IEnumerable GetRawComposedPropertyTypes(bool start = true) + { + IEnumerable propertyTypes = ContentTypeComposition + .Cast() + .SelectMany(x => start ? x.GetRawComposedPropertyTypes(false) : x.CompositionPropertyTypes); + + if (!start) + { + propertyTypes = propertyTypes.Union(PropertyTypes); + } + + return propertyTypes; + } + + private PropertyGroup? AddAndReturnPropertyGroup(string alias, string name) + { + // Ensure we don't have it already + if (PropertyGroups.Contains(alias)) + { + return null; + } + + // Add new group + var group = new PropertyGroup(SupportsPublishing) { Alias = alias, Name = name }; + + // check if it is inherited - there might be more than 1 but we want the 1st, to + // reuse its sort order - if there are more than 1 and they have different sort + // orders... there isn't much we can do anyways + PropertyGroup? inheritGroup = CompositionPropertyGroups.FirstOrDefault(x => x.Alias == alias); + if (inheritGroup == null) + { + // no, just local, set sort order + PropertyGroup? lastGroup = PropertyGroups.LastOrDefault(); + if (lastGroup != null) + { + group.SortOrder = lastGroup.SortOrder + 1; + } + } + else + { + // yes, inherited, re-use sort order + group.SortOrder = inheritGroup.SortOrder; + } + + // add + PropertyGroups.Add(group); + + return group; } } diff --git a/src/Umbraco.Core/Models/ContentTypeImportModel.cs b/src/Umbraco.Core/Models/ContentTypeImportModel.cs index 49d09c6821..5de62fcffa 100644 --- a/src/Umbraco.Core/Models/ContentTypeImportModel.cs +++ b/src/Umbraco.Core/Models/ContentTypeImportModel.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "contentTypeImportModel")] +public class ContentTypeImportModel : INotificationModel { - [DataContract(Name = "contentTypeImportModel")] - public class ContentTypeImportModel : INotificationModel - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "notifications")] - public List Notifications { get; } = new List(); + [DataMember(Name = "tempFileName")] + public string? TempFileName { get; set; } - [DataMember(Name = "tempFileName")] - public string? TempFileName { get; set; } - } + [DataMember(Name = "notifications")] + public List Notifications { get; } = new(); } diff --git a/src/Umbraco.Core/Models/ContentTypeSort.cs b/src/Umbraco.Core/Models/ContentTypeSort.cs index e7a11bad47..e10d650cac 100644 --- a/src/Umbraco.Core/Models/ContentTypeSort.cs +++ b/src/Umbraco.Core/Models/ContentTypeSort.cs @@ -1,78 +1,86 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a POCO for setting sort order on a ContentType reference +/// +public class ContentTypeSort : IValueObject, IDeepCloneable { - /// - /// Represents a POCO for setting sort order on a ContentType reference - /// - public class ContentTypeSort : IValueObject, IDeepCloneable + // this parameterless ctor should never be used BUT is required by AutoMapper in EntityMapperProfile + public ContentTypeSort() { - // this parameterless ctor should never be used BUT is required by AutoMapper in EntityMapperProfile - public ContentTypeSort() { } + } - /// - /// Initializes a new instance of the class. - /// - public ContentTypeSort(int id, int sortOrder) + /// + /// Initializes a new instance of the class. + /// + public ContentTypeSort(int id, int sortOrder) + { + Id = new Lazy(() => id); + SortOrder = sortOrder; + } + + public ContentTypeSort(Lazy id, int sortOrder, string alias) + { + Id = id; + SortOrder = sortOrder; + Alias = alias; + } + + /// + /// Gets or sets the Id of the ContentType + /// + public Lazy Id { get; set; } = new(() => 0); + + /// + /// Gets or sets the Sort Order of the ContentType + /// + public int SortOrder { get; set; } + + /// + /// Gets or sets the Alias of the ContentType + /// + public string Alias { get; set; } = string.Empty; + + public object DeepClone() + { + var clone = (ContentTypeSort)MemberwiseClone(); + var id = Id.Value; + clone.Id = new Lazy(() => id); + return clone; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - Id = new Lazy(() => id); - SortOrder = sortOrder; + return false; } - public ContentTypeSort(Lazy id, int sortOrder, string @alias) + if (ReferenceEquals(this, obj)) { - Id = id; - SortOrder = sortOrder; - Alias = alias; + return true; } - /// - /// Gets or sets the Id of the ContentType - /// - public Lazy Id { get; set; } = new Lazy(() => 0); - - /// - /// Gets or sets the Sort Order of the ContentType - /// - public int SortOrder { get; set; } - - /// - /// Gets or sets the Alias of the ContentType - /// - public string Alias { get; set; } = string.Empty; - - - public object DeepClone() + if (obj.GetType() != GetType()) { - var clone = (ContentTypeSort)MemberwiseClone(); - var id = Id.Value; - clone.Id = new Lazy(() => id); - return clone; + return false; } - protected bool Equals(ContentTypeSort other) - { - return Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); - } + return Equals((ContentTypeSort)obj); + } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((ContentTypeSort) obj); - } + protected bool Equals(ContentTypeSort other) => + Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); - public override int GetHashCode() + public override int GetHashCode() + { + unchecked { - unchecked - { - //The hash code will just be the alias if one is assigned, otherwise it will be the hash code of the Id. - //In some cases the alias can be null of the non lazy ctor is used, in that case, the lazy Id will already have a value created. - return Alias != null ? Alias.GetHashCode() : (Id.Value.GetHashCode() * 397); - } + // The hash code will just be the alias if one is assigned, otherwise it will be the hash code of the Id. + // In some cases the alias can be null of the non lazy ctor is used, in that case, the lazy Id will already have a value created. + return Alias != null ? Alias.GetHashCode() : Id.Value.GetHashCode() * 397; } - } } diff --git a/src/Umbraco.Core/Models/ContentVariation.cs b/src/Umbraco.Core/Models/ContentVariation.cs index 00c7f197a8..019da0eee0 100644 --- a/src/Umbraco.Core/Models/ContentVariation.cs +++ b/src/Umbraco.Core/Models/ContentVariation.cs @@ -1,37 +1,36 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Indicates how values can vary. +/// +/// +/// Values can vary by nothing, or culture, or segment, or both. +/// +/// Varying by culture implies that each culture version of a document can +/// be available or not, and published or not, individually. Varying by segment +/// is a property-level thing. +/// +/// +[Flags] +public enum ContentVariation : byte { /// - /// Indicates how values can vary. + /// Values do not vary. /// - /// - /// Values can vary by nothing, or culture, or segment, or both. - /// Varying by culture implies that each culture version of a document can - /// be available or not, and published or not, individually. Varying by segment - /// is a property-level thing. - /// - [Flags] - public enum ContentVariation : byte - { - /// - /// Values do not vary. - /// - Nothing = 0, + Nothing = 0, - /// - /// Values vary by culture. - /// - Culture = 1, + /// + /// Values vary by culture. + /// + Culture = 1, - /// - /// Values vary by segment. - /// - Segment = 2, + /// + /// Values vary by segment. + /// + Segment = 2, - /// - /// Values vary by culture and segment. - /// - CultureAndSegment = Culture | Segment - } + /// + /// Values vary by culture and segment. + /// + CultureAndSegment = Culture | Segment, } diff --git a/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs index 5fa0e98958..7d7cc6c578 100644 --- a/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs +++ b/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs @@ -1,17 +1,14 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class ContentVersionCleanupPolicySettings { - public class ContentVersionCleanupPolicySettings - { - public int ContentTypeId { get; set; } + public int ContentTypeId { get; set; } - public bool PreventCleanup { get; set; } + public bool PreventCleanup { get; set; } - public int? KeepAllVersionsNewerThanDays { get; set; } + public int? KeepAllVersionsNewerThanDays { get; set; } - public int? KeepLatestVersionPerDayForDays { get; set; } + public int? KeepLatestVersionPerDayForDays { get; set; } - public DateTime Updated { get; set; } - } + public DateTime Updated { get; set; } } diff --git a/src/Umbraco.Core/Models/ContentVersionMeta.cs b/src/Umbraco.Core/Models/ContentVersionMeta.cs index dbcd8540a0..cf95257716 100644 --- a/src/Umbraco.Core/Models/ContentVersionMeta.cs +++ b/src/Umbraco.Core/Models/ContentVersionMeta.cs @@ -1,45 +1,51 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class ContentVersionMeta { - public class ContentVersionMeta + public ContentVersionMeta() { - public int ContentId { get; } - public int ContentTypeId { get; } - public int VersionId { get; } - public int UserId { get; } - - public DateTime VersionDate { get; } - public bool CurrentPublishedVersion { get; } - public bool CurrentDraftVersion { get; } - public bool PreventCleanup { get; } - public string? Username { get; } - - public ContentVersionMeta() { } - - public ContentVersionMeta( - int versionId, - int contentId, - int contentTypeId, - int userId, - DateTime versionDate, - bool currentPublishedVersion, - bool currentDraftVersion, - bool preventCleanup, - string username) - { - VersionId = versionId; - ContentId = contentId; - ContentTypeId = contentTypeId; - - UserId = userId; - VersionDate = versionDate; - CurrentPublishedVersion = currentPublishedVersion; - CurrentDraftVersion = currentDraftVersion; - PreventCleanup = preventCleanup; - Username = username; - } - - public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}"; } + + public ContentVersionMeta( + int versionId, + int contentId, + int contentTypeId, + int userId, + DateTime versionDate, + bool currentPublishedVersion, + bool currentDraftVersion, + bool preventCleanup, + string username) + { + VersionId = versionId; + ContentId = contentId; + ContentTypeId = contentTypeId; + + UserId = userId; + VersionDate = versionDate; + CurrentPublishedVersion = currentPublishedVersion; + CurrentDraftVersion = currentDraftVersion; + PreventCleanup = preventCleanup; + Username = username; + } + + public int ContentId { get; } + + public int ContentTypeId { get; } + + public int VersionId { get; } + + public int UserId { get; } + + public DateTime VersionDate { get; } + + public bool CurrentPublishedVersion { get; } + + public bool CurrentDraftVersion { get; } + + public bool PreventCleanup { get; } + + public string? Username { get; } + + public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}"; } diff --git a/src/Umbraco.Core/Models/CultureImpact.cs b/src/Umbraco.Core/Models/CultureImpact.cs index fec02093d7..a6e83a7b7c 100644 --- a/src/Umbraco.Core/Models/CultureImpact.cs +++ b/src/Umbraco.Core/Models/CultureImpact.cs @@ -1,258 +1,311 @@ -using System; -using System.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the impact of a culture set. +/// +/// +/// +/// A set of cultures can be either all cultures (including the invariant culture), or +/// the invariant culture, or a specific culture. +/// +/// +public sealed class CultureImpact { /// - /// Represents the impact of a culture set. + /// Initializes a new instance of the class. + /// + /// The culture code. + /// A value indicating whether the culture is the default culture. + private CultureImpact(string? culture, bool isDefault = false) + { + if (culture != null && culture.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Culture \"\" is not valid here."); + } + + Culture = culture; + + if ((culture == null || culture == "*") && isDefault) + { + throw new ArgumentException("The invariant or 'all' culture can not be the default culture."); + } + + ImpactsOnlyDefaultCulture = isDefault; + } + + [Flags] + public enum Behavior : byte + { + AllCultures = 1, + InvariantCulture = 2, + ExplicitCulture = 4, + InvariantProperties = 8, + } + + /// + /// Gets the impact of 'all' cultures (including the invariant culture). + /// + public static CultureImpact All { get; } = new("*"); + + /// + /// Gets the impact of the invariant culture. + /// + public static CultureImpact Invariant { get; } = new(null); + + /// + /// Gets the culture code. /// /// - /// A set of cultures can be either all cultures (including the invariant culture), or - /// the invariant culture, or a specific culture. + /// Can be null (invariant) or * (all cultures) or a specific culture code. /// - public sealed class CultureImpact + public string? Culture { get; } + + /// + /// Gets a value indicating whether this impact impacts all cultures, including, + /// indirectly, the invariant culture. + /// + public bool ImpactsAllCultures => Culture == "*"; + + /// + /// Gets a value indicating whether this impact impacts only the invariant culture, + /// directly, not because all cultures are impacted. + /// + public bool ImpactsOnlyInvariantCulture => Culture == null; + + /// + /// Gets a value indicating whether this impact impacts an implicit culture. + /// + /// + /// And then it does not impact the invariant culture. The impacted + /// explicit culture could be the default culture. + /// + public bool ImpactsExplicitCulture => Culture != null && Culture != "*"; + + /// + /// Gets a value indicating whether this impact impacts the default culture, directly, + /// not because all cultures are impacted. + /// + public bool ImpactsOnlyDefaultCulture { get; } + + /// + /// Gets a value indicating whether this impact impacts the invariant properties, either + /// directly, or because all cultures are impacted, or because the default culture is impacted. + /// + public bool ImpactsInvariantProperties => Culture == null || Culture == "*" || ImpactsOnlyDefaultCulture; + + /// + /// Gets a value indicating whether this also impact impacts the invariant properties, + /// even though it does not impact the invariant culture, neither directly (ImpactsInvariantCulture) + /// nor indirectly (ImpactsAllCultures). + /// + public bool ImpactsAlsoInvariantProperties => !ImpactsOnlyInvariantCulture && + !ImpactsAllCultures && + ImpactsOnlyDefaultCulture; + + public Behavior CultureBehavior { - /// - /// Utility method to return the culture used for invariant property errors based on what cultures are being actively saved, - /// the default culture and the state of the current content item - /// - /// - /// - /// - /// - public static string? GetCultureForInvariantErrors(IContent? content, string?[] savingCultures, string? defaultCulture) + get { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (savingCultures == null) throw new ArgumentNullException(nameof(savingCultures)); - if (savingCultures.Length == 0) throw new ArgumentException(nameof(savingCultures)); - - var cultureForInvariantErrors = savingCultures.Any(x => x.InvariantEquals(defaultCulture)) - //the default culture is being flagged for saving so use it - ? defaultCulture - //If the content has no published version, we need to affiliate validation with the first variant being saved. - //If the content has a published version we will not affiliate the validation with any culture (null) - : !content.Published ? savingCultures[0] : null; - - return cultureForInvariantErrors; - } - - /// - /// Initializes a new instance of the class. - /// - /// The culture code. - /// A value indicating whether the culture is the default culture. - private CultureImpact(string? culture, bool isDefault = false) - { - if (culture != null && culture.IsNullOrWhiteSpace()) - throw new ArgumentException("Culture \"\" is not valid here."); - - Culture = culture; - - if ((culture == null || culture == "*") && isDefault) - throw new ArgumentException("The invariant or 'all' culture can not be the default culture."); - - ImpactsOnlyDefaultCulture = isDefault; - } - - /// - /// Gets the impact of 'all' cultures (including the invariant culture). - /// - public static CultureImpact All { get; } = new CultureImpact("*"); - - /// - /// Gets the impact of the invariant culture. - /// - public static CultureImpact Invariant { get; } = new CultureImpact(null); - - /// - /// Creates an impact instance representing the impact of a specific culture. - /// - /// The culture code. - /// A value indicating whether the culture is the default culture. - public static CultureImpact Explicit(string? culture, bool isDefault) - { - if (culture == null) - throw new ArgumentException("Culture is not explicit."); - if (culture.IsNullOrWhiteSpace()) - throw new ArgumentException("Culture \"\" is not explicit."); - if (culture == "*") - throw new ArgumentException("Culture \"*\" is not explicit."); - - return new CultureImpact(culture, isDefault); - } - - /// - /// Creates an impact instance representing the impact of a culture set, - /// in the context of a content item variation. - /// - /// The culture code. - /// A value indicating whether the culture is the default culture. - /// The content item. - /// - /// Validates that the culture is compatible with the variation. - /// - public static CultureImpact? Create(string culture, bool isDefault, IContent content) - { - // throws if not successful - TryCreate(culture, isDefault, content.ContentType.Variations, true, out var impact); - return impact; - } - - /// - /// Tries to create an impact instance representing the impact of a culture set, - /// in the context of a content item variation. - /// - /// The culture code. - /// A value indicating whether the culture is the default culture. - /// A content variation. - /// A value indicating whether to throw if the impact cannot be created. - /// The impact if it could be created, otherwise null. - /// A value indicating whether the impact could be created. - /// - /// Validates that the culture is compatible with the variation. - /// - internal static bool TryCreate(string culture, bool isDefault, ContentVariation variation, bool throwOnFail, out CultureImpact? impact) - { - impact = null; - - // if culture is invariant... - if (culture == null) + // null can only be invariant + if (Culture == null) { - // ... then variation must not vary by culture ... - if (variation.VariesByCulture()) - { - if (throwOnFail) - throw new InvalidOperationException("The invariant culture is not compatible with a varying variation."); - return false; - } - - // ... and it cannot be default - if (isDefault) - { - if (throwOnFail) - throw new InvalidOperationException("The invariant culture can not be the default culture."); - return false; - } - - impact = Invariant; - return true; + return Behavior.InvariantCulture | Behavior.InvariantProperties; } - // if culture is 'all'... - if (culture == "*") + // * is All which means its also invariant properties since this will include the default language + if (Culture == "*") { - // ... it cannot be default - if (isDefault) - { - if (throwOnFail) - throw new InvalidOperationException("The 'all' culture can not be the default culture."); - return false; - } - - // if variation does not vary by culture, then impact is invariant - impact = variation.VariesByCulture() ? All : Invariant; - return true; + return Behavior.AllCultures | Behavior.InvariantProperties; } - // neither null nor "*" - cannot be the empty string - if (culture.IsNullOrWhiteSpace()) + // else it's explicit + Behavior result = Behavior.ExplicitCulture; + + // if the explicit culture is the default, then the behavior is also InvariantProperties + if (ImpactsOnlyDefaultCulture) + { + result |= Behavior.InvariantProperties; + } + + return result; + } + } + + /// + /// Utility method to return the culture used for invariant property errors based on what cultures are being actively + /// saved, + /// the default culture and the state of the current content item + /// + /// + /// + /// + /// + public static string? GetCultureForInvariantErrors(IContent? content, string?[] savingCultures, string? defaultCulture) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (savingCultures == null) + { + throw new ArgumentNullException(nameof(savingCultures)); + } + + if (savingCultures.Length == 0) + { + throw new ArgumentException(nameof(savingCultures)); + } + + var cultureForInvariantErrors = savingCultures.Any(x => x.InvariantEquals(defaultCulture)) + + // the default culture is being flagged for saving so use it + ? defaultCulture + + // If the content has no published version, we need to affiliate validation with the first variant being saved. + // If the content has a published version we will not affiliate the validation with any culture (null) + : !content.Published + ? savingCultures[0] + : null; + + return cultureForInvariantErrors; + } + + /// + /// Creates an impact instance representing the impact of a specific culture. + /// + /// The culture code. + /// A value indicating whether the culture is the default culture. + public static CultureImpact Explicit(string? culture, bool isDefault) + { + if (culture == null) + { + throw new ArgumentException("Culture is not explicit."); + } + + if (culture.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Culture \"\" is not explicit."); + } + + if (culture == "*") + { + throw new ArgumentException("Culture \"*\" is not explicit."); + } + + return new CultureImpact(culture, isDefault); + } + + /// + /// Creates an impact instance representing the impact of a culture set, + /// in the context of a content item variation. + /// + /// The culture code. + /// A value indicating whether the culture is the default culture. + /// The content item. + /// + /// Validates that the culture is compatible with the variation. + /// + public static CultureImpact? Create(string culture, bool isDefault, IContent content) + { + // throws if not successful + TryCreate(culture, isDefault, content.ContentType.Variations, true, out CultureImpact? impact); + return impact; + } + + /// + /// Tries to create an impact instance representing the impact of a culture set, + /// in the context of a content item variation. + /// + /// The culture code. + /// A value indicating whether the culture is the default culture. + /// A content variation. + /// A value indicating whether to throw if the impact cannot be created. + /// The impact if it could be created, otherwise null. + /// A value indicating whether the impact could be created. + /// + /// Validates that the culture is compatible with the variation. + /// + internal static bool TryCreate(string culture, bool isDefault, ContentVariation variation, bool throwOnFail, out CultureImpact? impact) + { + impact = null; + + // if culture is invariant... + if (culture == null) + { + // ... then variation must not vary by culture ... + if (variation.VariesByCulture()) { if (throwOnFail) - throw new ArgumentException("Cannot be the empty string.", nameof(culture)); + { + throw new InvalidOperationException( + "The invariant culture is not compatible with a varying variation."); + } + return false; } - // if culture is specific, then variation must vary - if (!variation.VariesByCulture()) + // ... and it cannot be default + if (isDefault) { if (throwOnFail) - throw new InvalidOperationException($"The variant culture {culture} is not compatible with an invariant variation."); + { + throw new InvalidOperationException("The invariant culture can not be the default culture."); + } + return false; } - // return specific impact - impact = new CultureImpact(culture, isDefault); + impact = Invariant; return true; } - /// - /// Gets the culture code. - /// - /// - /// Can be null (invariant) or * (all cultures) or a specific culture code. - /// - public string? Culture { get; } - - /// - /// Gets a value indicating whether this impact impacts all cultures, including, - /// indirectly, the invariant culture. - /// - public bool ImpactsAllCultures => Culture == "*"; - - /// - /// Gets a value indicating whether this impact impacts only the invariant culture, - /// directly, not because all cultures are impacted. - /// - public bool ImpactsOnlyInvariantCulture => Culture == null; - - /// - /// Gets a value indicating whether this impact impacts an implicit culture. - /// - /// And then it does not impact the invariant culture. The impacted - /// explicit culture could be the default culture. - public bool ImpactsExplicitCulture => Culture != null && Culture != "*"; - - /// - /// Gets a value indicating whether this impact impacts the default culture, directly, - /// not because all cultures are impacted. - /// - public bool ImpactsOnlyDefaultCulture {get; } - - /// - /// Gets a value indicating whether this impact impacts the invariant properties, either - /// directly, or because all cultures are impacted, or because the default culture is impacted. - /// - public bool ImpactsInvariantProperties => Culture == null || Culture == "*" || ImpactsOnlyDefaultCulture; - - /// - /// Gets a value indicating whether this also impact impacts the invariant properties, - /// even though it does not impact the invariant culture, neither directly (ImpactsInvariantCulture) - /// nor indirectly (ImpactsAllCultures). - /// - public bool ImpactsAlsoInvariantProperties => !ImpactsOnlyInvariantCulture && - !ImpactsAllCultures && - ImpactsOnlyDefaultCulture; - - public Behavior CultureBehavior + // if culture is 'all'... + if (culture == "*") { - get + // ... it cannot be default + if (isDefault) { - //null can only be invariant - if (Culture == null) return Behavior.InvariantCulture | Behavior.InvariantProperties; + if (throwOnFail) + { + throw new InvalidOperationException("The 'all' culture can not be the default culture."); + } - // * is All which means its also invariant properties since this will include the default language - if (Culture == "*") return (Behavior.AllCultures | Behavior.InvariantProperties); - - //else it's explicit - var result = Behavior.ExplicitCulture; - - //if the explicit culture is the default, then the behavior is also InvariantProperties - if (ImpactsOnlyDefaultCulture) - result |= Behavior.InvariantProperties; - - return result; + return false; } + + // if variation does not vary by culture, then impact is invariant + impact = variation.VariesByCulture() ? All : Invariant; + return true; } - - [Flags] - public enum Behavior : byte + // neither null nor "*" - cannot be the empty string + if (culture.IsNullOrWhiteSpace()) { - AllCultures = 1, - InvariantCulture = 2, - ExplicitCulture = 4, - InvariantProperties = 8 + if (throwOnFail) + { + throw new ArgumentException("Cannot be the empty string.", nameof(culture)); + } + + return false; } + + // if culture is specific, then variation must vary + if (!variation.VariesByCulture()) + { + if (throwOnFail) + { + throw new InvalidOperationException( + $"The variant culture {culture} is not compatible with an invariant variation."); + } + + return false; + } + + // return specific impact + impact = new CultureImpact(culture, isDefault); + return true; } } diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index 6b33f07385..630ef338bd 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -1,196 +1,224 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Implements . +/// +[Serializable] +[DataContract(IsReference = true)] +public class DataType : TreeEntityBase, IDataType { + private readonly IConfigurationEditorJsonSerializer _serializer; + private object? _configuration; + private string? _configurationJson; + private ValueStorageType _databaseType; + private IDataEditor? _editor; + private bool _hasConfiguration; + /// - /// Implements . + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class DataType : TreeEntityBase, IDataType + public DataType(IDataEditor? editor, IConfigurationEditorJsonSerializer serializer, int parentId = -1) { - private IDataEditor? _editor; - private ValueStorageType _databaseType; - private readonly IConfigurationEditorJsonSerializer _serializer; - private object? _configuration; - private bool _hasConfiguration; - private string? _configurationJson; + _editor = editor ?? throw new ArgumentNullException(nameof(editor)); + _serializer = serializer ?? throw new ArgumentNullException(nameof(editor)); + ParentId = parentId; - /// - /// Initializes a new instance of the class. - /// - public DataType(IDataEditor? editor, IConfigurationEditorJsonSerializer serializer, int parentId = -1) + // set a default configuration + Configuration = _editor.GetConfigurationEditor().DefaultConfigurationObject; + } + + /// + [IgnoreDataMember] + public IDataEditor? Editor + { + get => _editor; + set { - _editor = editor ?? throw new ArgumentNullException(nameof(editor)); - _serializer = serializer ?? throw new ArgumentNullException(nameof(editor)); - ParentId = parentId; - - // set a default configuration - Configuration = _editor.GetConfigurationEditor().DefaultConfigurationObject; - } - - /// - [IgnoreDataMember] - public IDataEditor? Editor - { - get => _editor; - set + // ignore if no change + if (_editor?.Alias == value?.Alias) { - // ignore if no change - if (_editor?.Alias == value?.Alias) return; - OnPropertyChanged(nameof(Editor)); - - // try to map the existing configuration to the new configuration - // simulate saving to db and reloading (ie go via json) - var configuration = Configuration; - var json = _serializer.Serialize(configuration); - _editor = value; - - try - { - Configuration = _editor?.GetConfigurationEditor().FromDatabase(json, _serializer); - } - catch (Exception e) - { - throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); - } + return; } - } - /// - [DataMember] - public string EditorAlias => _editor?.Alias ?? string.Empty; + OnPropertyChanged(nameof(Editor)); - /// - [DataMember] - public ValueStorageType DatabaseType - { - get => _databaseType; - set => SetPropertyValueAndDetectChanges(value, ref _databaseType, nameof(DatabaseType)); - } + // try to map the existing configuration to the new configuration + // simulate saving to db and reloading (ie go via json) + var configuration = Configuration; + var json = _serializer.Serialize(configuration); + _editor = value; - /// - [DataMember] - public object? Configuration - { - get + try { - // if we know we have a configuration (which may be null), return it - // if we don't have an editor, then we have no configuration, return null - // else, use the editor to get the configuration object - - if (_hasConfiguration) return _configuration; - - try - { - _configuration = _editor?.GetConfigurationEditor().FromDatabase(_configurationJson, _serializer); - } - catch (Exception e) - { - throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); - } - - _hasConfiguration = true; - _configurationJson = null; - - return _configuration; + Configuration = _editor?.GetConfigurationEditor().FromDatabase(json, _serializer); } - set + catch (Exception e) { - if (value == null) - throw new ArgumentNullException(nameof(value)); - - // we don't support re-assigning the same object - // configurations are kinda non-mutable, mainly because detecting changes would be a pain - if (_configuration == value) // reference comparison - throw new ArgumentException("Configurations are kinda non-mutable. Do not reassign the same object.", nameof(value)); - - // validate configuration type - if (!_editor?.GetConfigurationEditor().IsConfiguration(value) ?? true) - throw new ArgumentException($"Value of type {value.GetType().Name} cannot be a configuration for editor {_editor?.Alias}, expecting.", nameof(value)); - - // extract database type from configuration object, if appropriate - if (value is IConfigureValueType valueTypeConfiguration) - DatabaseType = ValueTypes.ToStorageType(valueTypeConfiguration.ValueType); - - // extract database type from dictionary, if appropriate - if (value is IDictionary dictionaryConfiguration - && dictionaryConfiguration.TryGetValue(Constants.PropertyEditors.ConfigurationKeys.DataValueType, out var valueTypeObject) - && valueTypeObject is string valueTypeString - && ValueTypes.IsValue(valueTypeString)) - DatabaseType = ValueTypes.ToStorageType(valueTypeString); - - _configuration = value; - _hasConfiguration = true; - _configurationJson = null; - - // it's always a change - OnPropertyChanged(nameof(Configuration)); - } - } - - /// - /// Lazily set the configuration as a serialized json string. - /// - /// - /// Will be de-serialized on-demand. - /// This method is meant to be used when building entities from database, exclusively. - /// It does NOT register a property change to dirty. It ignores the fact that the configuration - /// may contain the database type, because the datatype DTO should also contain that database - /// type, and they should be the same. - /// Think before using! - /// - public void SetLazyConfiguration(string? configurationJson) - { - _hasConfiguration = false; - _configuration = null; - _configurationJson = configurationJson; - } - - /// - /// Gets a lazy configuration. - /// - /// - /// The configuration object will be lazily de-serialized. - /// This method is meant to be used when creating published datatypes, exclusively. - /// Think before using! - /// - internal Lazy GetLazyConfiguration() - { - // note: in both cases, make sure we capture what we need - we don't want - // to capture a reference to this full, potentially heavy, DataType instance. - - if (_hasConfiguration) - { - // if configuration has already been de-serialized, return - var capturedConfiguration = _configuration; - return new Lazy(() => capturedConfiguration); - } - else - { - // else, create a Lazy de-serializer - var capturedConfiguration = _configurationJson; - var capturedEditor = _editor; - return new Lazy(() => - { - try - { - return capturedEditor?.GetConfigurationEditor().FromDatabase(capturedConfiguration, _serializer); - } - catch (Exception e) - { - throw new InvalidOperationException($"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", e); - } - }); + throw new InvalidOperationException( + $"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + e); } } } + + /// + [DataMember] + public string EditorAlias => _editor?.Alias ?? string.Empty; + + /// + [DataMember] + public ValueStorageType DatabaseType + { + get => _databaseType; + set => SetPropertyValueAndDetectChanges(value, ref _databaseType, nameof(DatabaseType)); + } + + /// + [DataMember] + public object? Configuration + { + get + { + // if we know we have a configuration (which may be null), return it + // if we don't have an editor, then we have no configuration, return null + // else, use the editor to get the configuration object + if (_hasConfiguration) + { + return _configuration; + } + + try + { + _configuration = _editor?.GetConfigurationEditor().FromDatabase(_configurationJson, _serializer); + } + catch (Exception e) + { + throw new InvalidOperationException( + $"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + e); + } + + _hasConfiguration = true; + _configurationJson = null; + + return _configuration; + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + // we don't support re-assigning the same object + // configurations are kinda non-mutable, mainly because detecting changes would be a pain + // reference comparison + if (_configuration == value) + { + throw new ArgumentException( + "Configurations are kinda non-mutable. Do not reassign the same object.", + nameof(value)); + } + + // validate configuration type + if (!_editor?.GetConfigurationEditor().IsConfiguration(value) ?? true) + { + throw new ArgumentException( + $"Value of type {value.GetType().Name} cannot be a configuration for editor {_editor?.Alias}, expecting.", + nameof(value)); + } + + // extract database type from configuration object, if appropriate + if (value is IConfigureValueType valueTypeConfiguration) + { + DatabaseType = ValueTypes.ToStorageType(valueTypeConfiguration.ValueType); + } + + // extract database type from dictionary, if appropriate + if (value is IDictionary dictionaryConfiguration + && dictionaryConfiguration.TryGetValue( + Constants.PropertyEditors.ConfigurationKeys.DataValueType, + out var valueTypeObject) + && valueTypeObject is string valueTypeString + && ValueTypes.IsValue(valueTypeString)) + { + DatabaseType = ValueTypes.ToStorageType(valueTypeString); + } + + _configuration = value; + _hasConfiguration = true; + _configurationJson = null; + + // it's always a change + OnPropertyChanged(nameof(Configuration)); + } + } + + /// + /// Lazily set the configuration as a serialized json string. + /// + /// + /// Will be de-serialized on-demand. + /// + /// This method is meant to be used when building entities from database, exclusively. + /// It does NOT register a property change to dirty. It ignores the fact that the configuration + /// may contain the database type, because the datatype DTO should also contain that database + /// type, and they should be the same. + /// + /// Think before using! + /// + public void SetLazyConfiguration(string? configurationJson) + { + _hasConfiguration = false; + _configuration = null; + _configurationJson = configurationJson; + } + + /// + /// Gets a lazy configuration. + /// + /// + /// The configuration object will be lazily de-serialized. + /// This method is meant to be used when creating published datatypes, exclusively. + /// Think before using! + /// + internal Lazy GetLazyConfiguration() + { + // note: in both cases, make sure we capture what we need - we don't want + // to capture a reference to this full, potentially heavy, DataType instance. + if (_hasConfiguration) + { + // if configuration has already been de-serialized, return + var capturedConfiguration = _configuration; + return new Lazy(() => capturedConfiguration); + } + else + { + // else, create a Lazy de-serializer + var capturedConfiguration = _configurationJson; + IDataEditor? capturedEditor = _editor; + return new Lazy(() => + { + try + { + return capturedEditor?.GetConfigurationEditor().FromDatabase(capturedConfiguration, _serializer); + } + catch (Exception e) + { + throw new InvalidOperationException( + $"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + e); + } + }); + } + } } diff --git a/src/Umbraco.Core/Models/DataTypeExtensions.cs b/src/Umbraco.Core/Models/DataTypeExtensions.cs index 10419dca88..791f7b248b 100644 --- a/src/Umbraco.Core/Models/DataTypeExtensions.cs +++ b/src/Umbraco.Core/Models/DataTypeExtensions.cs @@ -1,95 +1,88 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extensions methods for . +/// +public static class DataTypeExtensions { - /// - /// Provides extensions methods for . - /// - public static class DataTypeExtensions + private static readonly ISet IdsOfBuildInDataTypes = new HashSet { - /// - /// Gets the configuration object. - /// - /// The expected type of the configuration object. - /// This datatype. - /// When the datatype configuration is not of the expected type. - public static T? ConfigurationAs(this IDataType dataType) - where T : class + Constants.DataTypes.Guids.ContentPickerGuid, + Constants.DataTypes.Guids.MemberPickerGuid, + Constants.DataTypes.Guids.MediaPickerGuid, + Constants.DataTypes.Guids.MultipleMediaPickerGuid, + Constants.DataTypes.Guids.RelatedLinksGuid, + Constants.DataTypes.Guids.MemberGuid, + Constants.DataTypes.Guids.ImageCropperGuid, + Constants.DataTypes.Guids.TagsGuid, + Constants.DataTypes.Guids.ListViewContentGuid, + Constants.DataTypes.Guids.ListViewMediaGuid, + Constants.DataTypes.Guids.ListViewMembersGuid, + Constants.DataTypes.Guids.DatePickerWithTimeGuid, + Constants.DataTypes.Guids.ApprovedColorGuid, + Constants.DataTypes.Guids.DropdownMultipleGuid, + Constants.DataTypes.Guids.RadioboxGuid, + Constants.DataTypes.Guids.DatePickerGuid, + Constants.DataTypes.Guids.DropdownGuid, + Constants.DataTypes.Guids.CheckboxListGuid, + Constants.DataTypes.Guids.CheckboxGuid, + Constants.DataTypes.Guids.NumericGuid, + Constants.DataTypes.Guids.RichtextEditorGuid, + Constants.DataTypes.Guids.TextstringGuid, + Constants.DataTypes.Guids.TextareaGuid, + Constants.DataTypes.Guids.UploadGuid, + Constants.DataTypes.Guids.UploadArticleGuid, + Constants.DataTypes.Guids.UploadAudioGuid, + Constants.DataTypes.Guids.UploadVectorGraphicsGuid, + Constants.DataTypes.Guids.UploadVideoGuid, + Constants.DataTypes.Guids.LabelStringGuid, + Constants.DataTypes.Guids.LabelDecimalGuid, + Constants.DataTypes.Guids.LabelDateTimeGuid, + Constants.DataTypes.Guids.LabelBigIntGuid, + Constants.DataTypes.Guids.LabelTimeGuid, + Constants.DataTypes.Guids.LabelDateTimeGuid, + }; + + /// + /// Gets the configuration object. + /// + /// The expected type of the configuration object. + /// This datatype. + /// When the datatype configuration is not of the expected type. + public static T? ConfigurationAs(this IDataType dataType) + where T : class + { + if (dataType == null) { - if (dataType == null) - throw new ArgumentNullException(nameof(dataType)); - - var configuration = dataType.Configuration; - - switch (configuration) - { - case null: - return null; - case T configurationAsT: - return configurationAsT; - } - - throw new InvalidCastException($"Cannot cast dataType configuration, of type {configuration.GetType().Name}, to {typeof(T).Name}."); + throw new ArgumentNullException(nameof(dataType)); } - private static readonly ISet IdsOfBuildInDataTypes = new HashSet() - { - Constants.DataTypes.Guids.ContentPickerGuid, - Constants.DataTypes.Guids.MemberPickerGuid, - Constants.DataTypes.Guids.MediaPickerGuid, - Constants.DataTypes.Guids.MultipleMediaPickerGuid, - Constants.DataTypes.Guids.RelatedLinksGuid, - Constants.DataTypes.Guids.MemberGuid, - Constants.DataTypes.Guids.ImageCropperGuid, - Constants.DataTypes.Guids.TagsGuid, - Constants.DataTypes.Guids.ListViewContentGuid, - Constants.DataTypes.Guids.ListViewMediaGuid, - Constants.DataTypes.Guids.ListViewMembersGuid, - Constants.DataTypes.Guids.DatePickerWithTimeGuid, - Constants.DataTypes.Guids.ApprovedColorGuid, - Constants.DataTypes.Guids.DropdownMultipleGuid, - Constants.DataTypes.Guids.RadioboxGuid, - Constants.DataTypes.Guids.DatePickerGuid, - Constants.DataTypes.Guids.DropdownGuid, - Constants.DataTypes.Guids.CheckboxListGuid, - Constants.DataTypes.Guids.CheckboxGuid, - Constants.DataTypes.Guids.NumericGuid, - Constants.DataTypes.Guids.RichtextEditorGuid, - Constants.DataTypes.Guids.TextstringGuid, - Constants.DataTypes.Guids.TextareaGuid, - Constants.DataTypes.Guids.UploadGuid, - Constants.DataTypes.Guids.UploadArticleGuid, - Constants.DataTypes.Guids.UploadAudioGuid, - Constants.DataTypes.Guids.UploadVectorGraphicsGuid, - Constants.DataTypes.Guids.UploadVideoGuid, - Constants.DataTypes.Guids.LabelStringGuid, - Constants.DataTypes.Guids.LabelDecimalGuid, - Constants.DataTypes.Guids.LabelDateTimeGuid, - Constants.DataTypes.Guids.LabelBigIntGuid, - Constants.DataTypes.Guids.LabelTimeGuid, - Constants.DataTypes.Guids.LabelDateTimeGuid, - }; + var configuration = dataType.Configuration; - /// - /// Returns true if this date type is build-in/default. - /// - /// The data type definition. - /// - public static bool IsBuildInDataType(this IDataType dataType) + switch (configuration) { - return IsBuildInDataType(dataType.Key); - } - - /// - /// Returns true if this date type is build-in/default. - /// - public static bool IsBuildInDataType(Guid key) - { - return IdsOfBuildInDataTypes.Contains(key); + case null: + return null; + case T configurationAsT: + return configurationAsT; } + throw new InvalidCastException( + $"Cannot cast dataType configuration, of type {configuration.GetType().Name}, to {typeof(T).Name}."); } + + /// + /// Returns true if this date type is build-in/default. + /// + /// The data type definition. + /// + public static bool IsBuildInDataType(this IDataType dataType) => IsBuildInDataType(dataType.Key); + + /// + /// Returns true if this date type is build-in/default. + /// + public static bool IsBuildInDataType(Guid key) => IdsOfBuildInDataTypes.Contains(key); } diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index 4dc293641c..ce34dab6f1 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -1,205 +1,215 @@ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Models -{ - public static class DeepCloneHelper - { - /// - /// Stores the metadata for the properties for a given type so we know how to create them - /// - private struct ClonePropertyInfo - { - public ClonePropertyInfo(PropertyInfo propertyInfo) : this() - { - if (propertyInfo == null) throw new ArgumentNullException("propertyInfo"); - PropertyInfo = propertyInfo; - } +namespace Umbraco.Cms.Core.Models; - public PropertyInfo PropertyInfo { get; private set; } - public bool IsDeepCloneable { get; set; } - public Type? GenericListType { get; set; } - public bool IsList - { - get { return GenericListType != null; } - } +public static class DeepCloneHelper +{ + /// + /// Used to avoid constant reflection (perf) + /// + private static readonly ConcurrentDictionary PropCache = new(); + + /// + /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the + /// outcome is 'output') + /// + /// + /// + /// + public static void DeepCloneRefProperties(IDeepCloneable input, IDeepCloneable output) + { + Type inputType = input.GetType(); + Type outputType = output.GetType(); + + if (inputType != outputType) + { + throw new InvalidOperationException("Both the input and output types must be the same"); } - /// - /// Used to avoid constant reflection (perf) - /// - private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); + // get the property metadata from cache so we only have to figure this out once per type + ClonePropertyInfo[] refProperties = PropCache.GetOrAdd(inputType, type => + inputType.GetProperties() + .Select(propertyInfo => + { + if ( - /// - /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the outcome is 'output') - /// - /// - /// - /// - public static void DeepCloneRefProperties(IDeepCloneable input, IDeepCloneable output) - { - var inputType = input.GetType(); - var outputType = output.GetType(); + // is not attributed with the ignore clone attribute + propertyInfo.GetCustomAttribute() != null - if (inputType != outputType) - { - throw new InvalidOperationException("Both the input and output types must be the same"); - } + // reference type but not string + || propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof(string) - //get the property metadata from cache so we only have to figure this out once per type - var refProperties = PropCache.GetOrAdd(inputType, type => - inputType.GetProperties() - .Select(propertyInfo => + // settable + || propertyInfo.CanWrite == false + + // non-indexed + || propertyInfo.GetIndexParameters().Any()) { - if ( - //is not attributed with the ignore clone attribute - propertyInfo.GetCustomAttribute() != null - //reference type but not string - || propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof (string) - //settable - || propertyInfo.CanWrite == false - //non-indexed - || propertyInfo.GetIndexParameters().Any()) + return null; + } + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + { + return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; + } + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) + && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + { + if (propertyInfo.PropertyType.IsGenericType + && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>))) + { + // if it is a IEnumerable<>, IReadOnlyCollection, IList or ICollection<> we'll use a List<> since it implements them all + Type genericType = + typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); + return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; + } + + if (propertyInfo.PropertyType.IsArray + || (propertyInfo.PropertyType.IsInterface && + propertyInfo.PropertyType.IsGenericType == false)) + { + // if its an array, we'll create a list to work with first and then convert to array later + // otherwise if its just a regular derivative of IEnumerable, we can use a list too + return new ClonePropertyInfo(propertyInfo) { GenericListType = typeof(List) }; + } + + // skip instead of trying to create instance of abstract or interface + if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) { return null; } - - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + // its a custom IEnumerable, we'll try to create it + try { - return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; - } + var custom = Activator.CreateInstance(propertyInfo.PropertyType); - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) - && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) - { - if (propertyInfo.PropertyType.IsGenericType - && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>))) - { - //if it is a IEnumerable<>, IReadOnlyCollection, IList or ICollection<> we'll use a List<> since it implements them all - var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); - return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; - } - if (propertyInfo.PropertyType.IsArray - || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) - { - //if its an array, we'll create a list to work with first and then convert to array later - //otherwise if its just a regular derivative of IEnumerable, we can use a list too - return new ClonePropertyInfo(propertyInfo) { GenericListType = typeof(List) }; - } - //skip instead of trying to create instance of abstract or interface - if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) + // if it's an IList we can work with it, otherwise we cannot + if (custom is not IList) { return null; } - //its a custom IEnumerable, we'll try to create it - try - { - var custom = Activator.CreateInstance(propertyInfo.PropertyType); - //if it's an IList we can work with it, otherwise we cannot - var newList = custom as IList; - if (newList == null) - { - return null; - } - return new ClonePropertyInfo(propertyInfo) {GenericListType = propertyInfo.PropertyType}; - } - catch (Exception) - { - //could not create this type so we'll skip it - return null; - } + return new ClonePropertyInfo(propertyInfo) { GenericListType = propertyInfo.PropertyType }; } - return new ClonePropertyInfo(propertyInfo); - }) - .Where(x => x.HasValue) - .Select(x => x!.Value) - .ToArray()); + catch (Exception) + { + // could not create this type so we'll skip it + return null; + } + } - foreach (var clonePropertyInfo in refProperties) + return new ClonePropertyInfo(propertyInfo); + }) + .Where(x => x.HasValue) + .Select(x => x!.Value) + .ToArray()); + + foreach (ClonePropertyInfo clonePropertyInfo in refProperties) + { + if (clonePropertyInfo.IsDeepCloneable) { - if (clonePropertyInfo.IsDeepCloneable) - { - //this ref property is also deep cloneable so clone it - var result = (IDeepCloneable?)clonePropertyInfo.PropertyInfo.GetValue(input, null); + // this ref property is also deep cloneable so clone it + var result = (IDeepCloneable?)clonePropertyInfo.PropertyInfo.GetValue(input, null); - if (result != null) - { - //set the cloned value to the property - clonePropertyInfo.PropertyInfo.SetValue(output, result.DeepClone(), null); - } + if (result != null) + { + // set the cloned value to the property + clonePropertyInfo.PropertyInfo.SetValue(output, result.DeepClone(), null); } - else if (clonePropertyInfo.IsList) + } + else if (clonePropertyInfo.IsList) + { + var enumerable = (IEnumerable?)clonePropertyInfo.PropertyInfo.GetValue(input, null); + if (enumerable == null) { - var enumerable = (IEnumerable?)clonePropertyInfo.PropertyInfo.GetValue(input, null); - if (enumerable == null) continue; + continue; + } - var newList = clonePropertyInfo.GenericListType is not null ? (IList?)Activator.CreateInstance(clonePropertyInfo.GenericListType) : null; + IList? newList = clonePropertyInfo.GenericListType is not null + ? (IList?)Activator.CreateInstance(clonePropertyInfo.GenericListType) + : null; - var isUsableType = true; + var isUsableType = true; - //now clone each item - foreach (var o in enumerable) + // now clone each item + foreach (var o in enumerable) + { + // first check if the item is deep cloneable and copy that way + if (o is IDeepCloneable dc) { - //first check if the item is deep cloneable and copy that way - var dc = o as IDeepCloneable; - if (dc != null) - { - newList?.Add(dc.DeepClone()); - } - else if (o is string || o.GetType().IsValueType) - { - //check if the item is a value type or a string, then we can just use it - newList?.Add(o); - } - else - { - //this will occur if the item is not a string or value type or IDeepCloneable, in this case we cannot - // clone each element, we'll need to skip this property, people will have to manually clone this list - isUsableType = false; - break; - } + newList?.Add(dc.DeepClone()); } - - //if this was not usable, skip this property - if (isUsableType == false) + else if (o is string || o.GetType().IsValueType) { - continue; - } - - if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) - { - //need to convert to array - var arr = (object?[]?)Activator.CreateInstance(clonePropertyInfo.PropertyInfo.PropertyType, newList?.Count ?? 0); - for (int i = 0; i < newList?.Count; i++) - { - if (arr != null) - { - arr[i] = newList[i]; - } - } - - //set the cloned collection - clonePropertyInfo.PropertyInfo.SetValue(output, arr, null); + // check if the item is a value type or a string, then we can just use it + newList?.Add(o); } else { - //set the cloned collection - clonePropertyInfo.PropertyInfo.SetValue(output, newList, null); + // this will occur if the item is not a string or value type or IDeepCloneable, in this case we cannot + // clone each element, we'll need to skip this property, people will have to manually clone this list + isUsableType = false; + break; + } + } + + // if this was not usable, skip this property + if (isUsableType == false) + { + continue; + } + + if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) + { + // need to convert to array + var arr = (object?[]?)Activator.CreateInstance( + clonePropertyInfo.PropertyInfo.PropertyType, + newList?.Count ?? 0); + for (var i = 0; i < newList?.Count; i++) + { + if (arr != null) + { + arr[i] = newList[i]; + } } + // set the cloned collection + clonePropertyInfo.PropertyInfo.SetValue(output, arr, null); + } + else + { + // set the cloned collection + clonePropertyInfo.PropertyInfo.SetValue(output, newList, null); } } } + } + /// + /// Stores the metadata for the properties for a given type so we know how to create them + /// + private struct ClonePropertyInfo + { + public ClonePropertyInfo(PropertyInfo propertyInfo) + : this() + { + PropertyInfo = propertyInfo ?? throw new ArgumentNullException("propertyInfo"); + } + + public PropertyInfo PropertyInfo { get; } + + public bool IsDeepCloneable { get; set; } + + public Type? GenericListType { get; set; } + + public bool IsList => GenericListType != null; } } diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index 14cd3bb2e5..7473cef60f 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -1,83 +1,82 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Dictionary Item +/// +[Serializable] +[DataContract(IsReference = true)] +public class DictionaryItem : EntityBase, IDictionaryItem { - /// - /// Represents a Dictionary Item - /// - [Serializable] - [DataContract(IsReference = true)] - public class DictionaryItem : EntityBase, IDictionaryItem - { - public Func? GetLanguage { get; set; } - private Guid? _parentId; - private string _itemKey; - private IEnumerable _translations; - - public DictionaryItem(string itemKey) - : this(null, itemKey) - {} - - public DictionaryItem(Guid? parentId, string itemKey) - { - _parentId = parentId; - _itemKey = itemKey; - _translations = new List(); - } - - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> DictionaryTranslationComparer = - new DelegateEqualityComparer>( + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> + DictionaryTranslationComparer = + new( (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), enumerable => enumerable.GetHashCode()); - /// - /// Gets or Sets the Parent Id of the Dictionary Item - /// - [DataMember] - public Guid? ParentId - { - get { return _parentId; } - set { SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); } - } + private string _itemKey; + private Guid? _parentId; + private IEnumerable _translations; - /// - /// Gets or sets the Key for the Dictionary Item - /// - [DataMember] - public string ItemKey - { - get { return _itemKey; } - set { SetPropertyValueAndDetectChanges(value, ref _itemKey!, nameof(ItemKey)); } - } + public DictionaryItem(string itemKey) + : this(null, itemKey) + { + } - /// - /// Gets or sets a list of translations for the Dictionary Item - /// - [DataMember] - public IEnumerable Translations + public DictionaryItem(Guid? parentId, string itemKey) + { + _parentId = parentId; + _itemKey = itemKey; + _translations = new List(); + } + + public Func? GetLanguage { get; set; } + + /// + /// Gets or Sets the Parent Id of the Dictionary Item + /// + [DataMember] + public Guid? ParentId + { + get => _parentId; + set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); + } + + /// + /// Gets or sets the Key for the Dictionary Item + /// + [DataMember] + public string ItemKey + { + get => _itemKey; + set => SetPropertyValueAndDetectChanges(value, ref _itemKey!, nameof(ItemKey)); + } + + /// + /// Gets or sets a list of translations for the Dictionary Item + /// + [DataMember] + public IEnumerable Translations + { + get => _translations; + set { - get { return _translations; } - set + IDictionaryTranslation[] asArray = value.ToArray(); + + // ensure the language callback is set on each translation + if (GetLanguage != null) { - var asArray = value?.ToArray(); - //ensure the language callback is set on each translation - if (GetLanguage != null && asArray is not null) + foreach (DictionaryTranslation translation in asArray.OfType()) { - foreach (var translation in asArray.OfType()) - { - translation.GetLanguage = GetLanguage; - } + translation.GetLanguage = GetLanguage; } - - SetPropertyValueAndDetectChanges(asArray, ref _translations!, nameof(Translations), - DictionaryTranslationComparer); } + + SetPropertyValueAndDetectChanges(asArray, ref _translations!, nameof(Translations), DictionaryTranslationComparer); } } } diff --git a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs index 137680aa27..3e6c051201 100644 --- a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs +++ b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs @@ -1,31 +1,29 @@ -using System.Linq; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions -{ - public static class DictionaryItemExtensions - { - /// - /// Returns the translation value for the language id, if no translation is found it returns an empty string - /// - /// - /// - /// - public static string? GetTranslatedValue(this IDictionaryItem d, int languageId) - { - var trans = d.Translations?.FirstOrDefault(x => x.LanguageId == languageId); - return trans == null ? string.Empty : trans.Value; - } +namespace Umbraco.Extensions; - /// - /// Returns the default translated value based on the default language - /// - /// - /// - public static string? GetDefaultValue(this IDictionaryItem d) - { - var defaultTranslation = d.Translations?.FirstOrDefault(x => x.Language?.Id == 1); - return defaultTranslation == null ? string.Empty : defaultTranslation.Value; - } +public static class DictionaryItemExtensions +{ + /// + /// Returns the translation value for the language id, if no translation is found it returns an empty string + /// + /// + /// + /// + public static string? GetTranslatedValue(this IDictionaryItem d, int languageId) + { + IDictionaryTranslation? trans = d.Translations.FirstOrDefault(x => x.LanguageId == languageId); + return trans == null ? string.Empty : trans.Value; + } + + /// + /// Returns the default translated value based on the default language + /// + /// + /// + public static string? GetDefaultValue(this IDictionaryItem d) + { + IDictionaryTranslation? defaultTranslation = d.Translations.FirstOrDefault(x => x.Language?.Id == 1); + return defaultTranslation == null ? string.Empty : defaultTranslation.Value; } } diff --git a/src/Umbraco.Core/Models/DictionaryTranslation.cs b/src/Umbraco.Core/Models/DictionaryTranslation.cs index d0d98a64db..5d44768388 100644 --- a/src/Umbraco.Core/Models/DictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/DictionaryTranslation.cs @@ -1,107 +1,107 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a translation for a +/// +[Serializable] +[DataContract(IsReference = true)] +public class DictionaryTranslation : EntityBase, IDictionaryTranslation { - /// - /// Represents a translation for a - /// - [Serializable] - [DataContract(IsReference = true)] - public class DictionaryTranslation : EntityBase, IDictionaryTranslation + private ILanguage? _language; + + // note: this will be memberwise cloned + private string _value; + + public DictionaryTranslation(ILanguage language, string value) { - public Func? GetLanguage { get; set; } + _language = language ?? throw new ArgumentNullException("language"); + LanguageId = _language.Id; + _value = value; + } - private ILanguage? _language; - private string _value; - //note: this will be memberwise cloned - private int _languageId; + public DictionaryTranslation(ILanguage language, string value, Guid uniqueId) + { + _language = language ?? throw new ArgumentNullException("language"); + LanguageId = _language.Id; + _value = value; + Key = uniqueId; + } - public DictionaryTranslation(ILanguage language, string value) + public DictionaryTranslation(int languageId, string value) + { + LanguageId = languageId; + _value = value; + } + + public DictionaryTranslation(int languageId, string value, Guid uniqueId) + { + LanguageId = languageId; + _value = value; + Key = uniqueId; + } + + public Func? GetLanguage { get; set; } + + /// + /// Gets or sets the for the translation + /// + /// + /// Marked as DoNotClone - TODO: this member shouldn't really exist here in the first place, the DictionaryItem + /// class will have a deep hierarchy of objects which all get deep cloned which we don't want. This should have simply + /// just referenced a language ID not the actual language object. In v8 we need to fix this. + /// We're going to have to do the same hacky stuff we had to do with the Template/File contents so that this is + /// returned + /// on a callback. + /// + [DataMember] + [DoNotClone] + public ILanguage? Language + { + get { - if (language == null) throw new ArgumentNullException("language"); - _language = language; - _languageId = _language.Id; - _value = value; - } - - public DictionaryTranslation(ILanguage language, string value, Guid uniqueId) - { - if (language == null) throw new ArgumentNullException("language"); - _language = language; - _languageId = _language.Id; - _value = value; - Key = uniqueId; - } - - public DictionaryTranslation(int languageId, string value) - { - _languageId = languageId; - _value = value; - } - - public DictionaryTranslation(int languageId, string value, Guid uniqueId) - { - _languageId = languageId; - _value = value; - Key = uniqueId; - } - - /// - /// Gets or sets the for the translation - /// - /// - /// Marked as DoNotClone - TODO: this member shouldn't really exist here in the first place, the DictionaryItem - /// class will have a deep hierarchy of objects which all get deep cloned which we don't want. This should have simply - /// just referenced a language ID not the actual language object. In v8 we need to fix this. - /// We're going to have to do the same hacky stuff we had to do with the Template/File contents so that this is returned - /// on a callback. - /// - [DataMember] - [DoNotClone] - public ILanguage? Language - { - get + if (_language != null) { - if (_language != null) - return _language; - - // else, must lazy-load - if (GetLanguage != null && _languageId > 0) - _language = GetLanguage(_languageId); return _language; } - set + + // else, must lazy-load + if (GetLanguage != null && LanguageId > 0) { - SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); - _languageId = _language == null ? -1 : _language.Id; + _language = GetLanguage(LanguageId); } + + return _language; } - public int LanguageId + set { - get { return _languageId; } - } - - /// - /// Gets or sets the translated text - /// - [DataMember] - public string Value - { - get { return _value; } - set { SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); } - } - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (DictionaryTranslation)clone; - - // clear fields that were memberwise-cloned and that we don't want to clone - clonedEntity._language = null; + SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); + LanguageId = _language == null ? -1 : _language.Id; } } + + public int LanguageId { get; private set; } + + /// + /// Gets or sets the translated text + /// + [DataMember] + public string Value + { + get => _value; + set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); + } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (DictionaryTranslation)clone; + + // clear fields that were memberwise-cloned and that we don't want to clone + clonedEntity._language = null; + } } diff --git a/src/Umbraco.Core/Models/DoNotCloneAttribute.cs b/src/Umbraco.Core/Models/DoNotCloneAttribute.cs index 39a7bcd900..1fb0b3cd4b 100644 --- a/src/Umbraco.Core/Models/DoNotCloneAttribute.cs +++ b/src/Umbraco.Core/Models/DoNotCloneAttribute.cs @@ -1,23 +1,16 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Used to attribute properties that have a setter and are a reference type +/// that should be ignored for cloning when using the DeepCloneHelper +/// +/// +/// This attribute must be used: +/// * when the property is backed by a field but the result of the property is the un-natural data stored in the field +/// This attribute should not be used: +/// * when the property is virtual +/// * when the setter performs additional required logic other than just setting the underlying field +/// +public class DoNotCloneAttribute : Attribute { - /// - /// Used to attribute properties that have a setter and are a reference type - /// that should be ignored for cloning when using the DeepCloneHelper - /// - /// - /// - /// This attribute must be used: - /// * when the property is backed by a field but the result of the property is the un-natural data stored in the field - /// - /// This attribute should not be used: - /// * when the property is virtual - /// * when the setter performs additional required logic other than just setting the underlying field - /// - /// - public class DoNotCloneAttribute : Attribute - { - - } } diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs index 0255cfd40e..ac19eef0c8 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs @@ -1,45 +1,42 @@ -using System; +namespace Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.Editors +/// +/// Represents data that has been submitted to be saved for a content property +/// +/// +/// This object exists because we may need to save additional data for each property, more than just +/// the string representation of the value being submitted. An example of this is uploaded files. +/// +public class ContentPropertyData { - /// - /// Represents data that has been submitted to be saved for a content property - /// - /// - /// This object exists because we may need to save additional data for each property, more than just - /// the string representation of the value being submitted. An example of this is uploaded files. - /// - public class ContentPropertyData + public ContentPropertyData(object? value, object? dataTypeConfiguration) { - public ContentPropertyData(object? value, object? dataTypeConfiguration) - { - Value = value; - DataTypeConfiguration = dataTypeConfiguration; - } - - /// - /// The value submitted for the property - /// - public object? Value { get; } - - /// - /// The data type configuration for the property. - /// - public object? DataTypeConfiguration { get; } - - /// - /// Gets or sets the unique identifier of the content owning the property. - /// - public Guid ContentKey { get; set; } - - /// - /// Gets or sets the unique identifier of the property type. - /// - public Guid PropertyTypeKey { get; set; } - - /// - /// Gets or sets the uploaded files. - /// - public ContentPropertyFile[]? Files { get; set; } + Value = value; + DataTypeConfiguration = dataTypeConfiguration; } + + /// + /// The value submitted for the property + /// + public object? Value { get; } + + /// + /// The data type configuration for the property. + /// + public object? DataTypeConfiguration { get; } + + /// + /// Gets or sets the unique identifier of the content owning the property. + /// + public Guid ContentKey { get; set; } + + /// + /// Gets or sets the unique identifier of the property type. + /// + public Guid PropertyTypeKey { get; set; } + + /// + /// Gets or sets the uploaded files. + /// + public ContentPropertyFile[]? Files { get; set; } } diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs index d1bc9127ce..9bb098697c 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs @@ -1,43 +1,42 @@ -namespace Umbraco.Cms.Core.Models.Editors +namespace Umbraco.Cms.Core.Models.Editors; + +/// +/// Represents an uploaded file for a property. +/// +public class ContentPropertyFile { + /// + /// Gets or sets the property alias. + /// + public string? PropertyAlias { get; set; } /// - /// Represents an uploaded file for a property. + /// When dealing with content variants, this is the culture for the variant /// - public class ContentPropertyFile - { - /// - /// Gets or sets the property alias. - /// - public string? PropertyAlias { get; set; } + public string? Culture { get; set; } - /// - /// When dealing with content variants, this is the culture for the variant - /// - public string? Culture { get; set; } + /// + /// When dealing with content variants, this is the segment for the variant + /// + public string? Segment { get; set; } - /// - /// When dealing with content variants, this is the segment for the variant - /// - public string? Segment { get; set; } + /// + /// An array of metadata that is parsed out from the file info posted to the server which is set on the client. + /// + /// + /// This can be used for property types like Nested Content that need to have special unique identifiers for each file + /// since there might be multiple files + /// per property. + /// + public string[]? Metadata { get; set; } - /// - /// An array of metadata that is parsed out from the file info posted to the server which is set on the client. - /// - /// - /// This can be used for property types like Nested Content that need to have special unique identifiers for each file since there might be multiple files - /// per property. - /// - public string[]? Metadata { get; set; } + /// + /// Gets or sets the name of the file. + /// + public string? FileName { get; set; } - /// - /// Gets or sets the name of the file. - /// - public string? FileName { get; set; } - - /// - /// Gets or sets the temporary path where the file has been uploaded. - /// - public string TempFilePath { get; set; } = string.Empty; - } + /// + /// Gets or sets the temporary path where the file has been uploaded. + /// + public string TempFilePath { get; set; } = string.Empty; } diff --git a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs index 4efc5017e1..c093962408 100644 --- a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs +++ b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs @@ -1,70 +1,56 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.Models.Editors +/// +/// Used to track reference to other entities in a property value +/// +public struct UmbracoEntityReference : IEquatable { - /// - /// Used to track reference to other entities in a property value - /// - public struct UmbracoEntityReference : IEquatable + private static readonly UmbracoEntityReference _empty = new(UnknownTypeUdi.Instance, string.Empty); + + public UmbracoEntityReference(Udi udi, string relationTypeAlias) { - private static readonly UmbracoEntityReference _empty = new UmbracoEntityReference(UnknownTypeUdi.Instance, string.Empty); + Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias)); + } - public UmbracoEntityReference(Udi udi, string relationTypeAlias) + public UmbracoEntityReference(Udi udi) + { + Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + + switch (udi.EntityType) { - Udi = udi ?? throw new ArgumentNullException(nameof(udi)); - RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias)); - } - - public UmbracoEntityReference(Udi udi) - { - Udi = udi ?? throw new ArgumentNullException(nameof(udi)); - - switch (udi.EntityType) - { - case Constants.UdiEntityType.Media: - RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias; - break; - default: - RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; - break; - } - } - - public static UmbracoEntityReference Empty() => _empty; - - public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); - - public Udi Udi { get; } - public string RelationTypeAlias { get; } - - public override bool Equals(object? obj) - { - return obj is UmbracoEntityReference reference && Equals(reference); - } - - public bool Equals(UmbracoEntityReference other) - { - return EqualityComparer.Default.Equals(Udi, other.Udi) && - RelationTypeAlias == other.RelationTypeAlias; - } - - public override int GetHashCode() - { - var hashCode = -487348478; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Udi); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelationTypeAlias); - return hashCode; - } - - public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) - { - return left.Equals(right); - } - - public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) - { - return !(left == right); + case Constants.UdiEntityType.Media: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias; + break; + default: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; + break; } } + + public Udi Udi { get; } + + public static UmbracoEntityReference Empty() => _empty; + + public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); + + public string RelationTypeAlias { get; } + + public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right); + + public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference); + + public bool Equals(UmbracoEntityReference other) => + EqualityComparer.Default.Equals(Udi, other.Udi) && + RelationTypeAlias == other.RelationTypeAlias; + + public override int GetHashCode() + { + var hashCode = -487348478; + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Udi); + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(RelationTypeAlias); + return hashCode; + } + + public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) => !(left == right); } diff --git a/src/Umbraco.Core/Models/Email/EmailMessage.cs b/src/Umbraco.Core/Models/Email/EmailMessage.cs index b012bbfeb3..1419285417 100644 --- a/src/Umbraco.Core/Models/Email/EmailMessage.cs +++ b/src/Umbraco.Core/Models/Email/EmailMessage.cs @@ -1,82 +1,86 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Models.Email +public class EmailMessage { - public class EmailMessage + public EmailMessage(string? from, string? to, string? subject, string? body, bool isBodyHtml) + : this(from, new[] { to }, null, null, null, subject, body, isBodyHtml, null) { - public string? From { get; } + } - public string?[] To { get; } + public EmailMessage( + string? from, + string?[] to, + string[]? cc, + string[]? bcc, + string[]? replyTo, + string? subject, + string? body, + bool isBodyHtml, + IEnumerable? attachments) + { + ArgumentIsNotNullOrEmpty(to, nameof(to)); + ArgumentIsNotNullOrEmpty(subject, nameof(subject)); + ArgumentIsNotNullOrEmpty(body, nameof(body)); - public string[]? Cc { get; } + From = from; + To = to; + Cc = cc; + Bcc = bcc; + ReplyTo = replyTo; + Subject = subject; + Body = body; + IsBodyHtml = isBodyHtml; + Attachments = attachments?.ToList(); + } - public string[]? Bcc { get; } + public string? From { get; } - public string[]? ReplyTo { get; } + public string?[] To { get; } - public string? Subject { get; } + public string[]? Cc { get; } - public string? Body { get; } + public string[]? Bcc { get; } - public bool IsBodyHtml { get; } + public string[]? ReplyTo { get; } - public IList? Attachments { get; } + public string? Subject { get; } - public bool HasAttachments => Attachments != null && Attachments.Count > 0; + public string? Body { get; } - public EmailMessage(string? from, string? to, string? subject, string? body, bool isBodyHtml) - : this(from, new[] { to }, null, null, null, subject, body, isBodyHtml, null) + public bool IsBodyHtml { get; } + + public IList? Attachments { get; } + + public bool HasAttachments => Attachments != null && Attachments.Count > 0; + + private static void ArgumentIsNotNullOrEmpty(string? arg, string argName) + { + if (arg == null) { + throw new ArgumentNullException(argName); } - public EmailMessage(string? from, string?[] to, string[]? cc, string[]? bcc, string[]? replyTo, string? subject, string? body, bool isBodyHtml, IEnumerable? attachments) + if (arg.Length == 0) { - ArgumentIsNotNullOrEmpty(to, nameof(to)); - ArgumentIsNotNullOrEmpty(subject, nameof(subject)); - ArgumentIsNotNullOrEmpty(body, nameof(body)); + throw new ArgumentException("Value cannot be empty.", argName); + } + } - From = from; - To = to; - Cc = cc; - Bcc = bcc; - ReplyTo = replyTo; - Subject = subject; - Body = body; - IsBodyHtml = isBodyHtml; - Attachments = attachments?.ToList(); + private static void ArgumentIsNotNullOrEmpty(string?[]? arg, string argName) + { + if (arg == null) + { + throw new ArgumentNullException(argName); } - private static void ArgumentIsNotNullOrEmpty(string? arg, string argName) + if (arg.Length == 0) { - if (arg == null) - { - throw new ArgumentNullException(argName); - } - - if (arg.Length == 0) - { - throw new ArgumentException("Value cannot be empty.", argName); - } + throw new ArgumentException("Value cannot be an empty array.", argName); } - private static void ArgumentIsNotNullOrEmpty(string?[]? arg, string argName) + if (arg.Any(x => x is not null && x.Length > 0) == false) { - if (arg == null) - { - throw new ArgumentNullException(argName); - } - - if (arg.Length == 0) - { - throw new ArgumentException("Value cannot be an empty array.", argName); - } - - if (arg.Any(x => x is not null && x.Length > 0) == false) - { - throw new ArgumentException("Value cannot be an array containing only null or empty elements.", argName); - } + throw new ArgumentException("Value cannot be an array containing only null or empty elements.", argName); } } } diff --git a/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs b/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs index bbb24b69f7..96c52ef9e7 100644 --- a/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs +++ b/src/Umbraco.Core/Models/Email/EmailMessageAttachment.cs @@ -1,17 +1,14 @@ -using System.IO; +namespace Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Models.Email +public class EmailMessageAttachment { - public class EmailMessageAttachment + public EmailMessageAttachment(Stream stream, string fileName) { - public Stream Stream { get; } - - public string FileName { get; } - - public EmailMessageAttachment(Stream stream, string fileName) - { - Stream = stream; - FileName = fileName; - } + Stream = stream; + FileName = fileName; } + + public Stream Stream { get; } + + public string FileName { get; } } diff --git a/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs b/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs index 755947c6a4..c9488f0798 100644 --- a/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs +++ b/src/Umbraco.Core/Models/Email/NotificationEmailAddress.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Models.Email +namespace Umbraco.Cms.Core.Models.Email; + +/// +/// Represents an email address used for notifications. Contains both the address and its display name. +/// +public class NotificationEmailAddress { - /// - /// Represents an email address used for notifications. Contains both the address and its display name. - /// - public class NotificationEmailAddress + public NotificationEmailAddress(string address, string displayName) { - public string DisplayName { get; } - - public string Address { get; } - - public NotificationEmailAddress(string address, string displayName) - { - Address = address; - DisplayName = displayName; - } + Address = address; + DisplayName = displayName; } + + public string DisplayName { get; } + + public string Address { get; } } diff --git a/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs b/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs index c71519d83f..abfea360d9 100644 --- a/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs +++ b/src/Umbraco.Core/Models/Email/NotificationEmailModel.cs @@ -1,54 +1,49 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Models.Email +/// +/// Represents an email when sent with notifications. +/// +public class NotificationEmailModel { - /// - /// Represents an email when sent with notifications. - /// - public class NotificationEmailModel + public NotificationEmailModel( + NotificationEmailAddress? from, + IEnumerable? to, + IEnumerable? cc, + IEnumerable? bcc, + IEnumerable? replyTo, + string? subject, + string? body, + IEnumerable? attachments, + bool isBodyHtml) { - public NotificationEmailAddress? From { get; } - - public IEnumerable? To { get; } - - public IEnumerable? Cc { get; } - - public IEnumerable? Bcc { get; } - - public IEnumerable? ReplyTo { get; } - - public string? Subject { get; } - - public string? Body { get; } - - public bool IsBodyHtml { get; } - - public IList? Attachments { get; } - - public bool HasAttachments => Attachments != null && Attachments.Count > 0; - - public NotificationEmailModel( - NotificationEmailAddress? from, - IEnumerable? to, - IEnumerable? cc, - IEnumerable? bcc, - IEnumerable? replyTo, - string? subject, - string? body, - IEnumerable? attachments, - bool isBodyHtml) - { - From = from; - To = to; - Cc = cc; - Bcc = bcc; - ReplyTo = replyTo; - Subject = subject; - Body = body; - IsBodyHtml = isBodyHtml; - Attachments = attachments?.ToList(); - } - + From = from; + To = to; + Cc = cc; + Bcc = bcc; + ReplyTo = replyTo; + Subject = subject; + Body = body; + IsBodyHtml = isBodyHtml; + Attachments = attachments?.ToList(); } + + public NotificationEmailAddress? From { get; } + + public IEnumerable? To { get; } + + public IEnumerable? Cc { get; } + + public IEnumerable? Bcc { get; } + + public IEnumerable? ReplyTo { get; } + + public string? Subject { get; } + + public string? Body { get; } + + public bool IsBodyHtml { get; } + + public IList? Attachments { get; } + + public bool HasAttachments => Attachments != null && Attachments.Count > 0; } diff --git a/src/Umbraco.Core/Models/Entities/BeingDirty.cs b/src/Umbraco.Core/Models/Entities/BeingDirty.cs index 7b078b35b8..0ae2a142ed 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirty.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirty.cs @@ -1,36 +1,30 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Provides a concrete implementation of . +/// +/// +/// +/// This class is provided for classes that cannot inherit from +/// and therefore need to implement , by re-using some of +/// logic. +/// +/// +public sealed class BeingDirty : BeingDirtyBase { /// - /// Provides a concrete implementation of . + /// Sets a property value, detects changes and manages the dirty flag. /// - /// - /// This class is provided for classes that cannot inherit from - /// and therefore need to implement , by re-using some of - /// logic. - /// - public sealed class BeingDirty : BeingDirtyBase - { - /// - /// Sets a property value, detects changes and manages the dirty flag. - /// - /// The type of the value. - /// The new value. - /// A reference to the value to set. - /// The property name. - /// A comparer to compare property values. - public new void SetPropertyValueAndDetectChanges(T value, ref T? valueRef, string propertyName, IEqualityComparer? comparer = null) - { - base.SetPropertyValueAndDetectChanges(value, ref valueRef, propertyName, comparer); - } + /// The type of the value. + /// The new value. + /// A reference to the value to set. + /// The property name. + /// A comparer to compare property values. + public new void SetPropertyValueAndDetectChanges(T value, ref T? valueRef, string propertyName, IEqualityComparer? comparer = null) => + base.SetPropertyValueAndDetectChanges(value, ref valueRef, propertyName, comparer); - /// - /// Registers that a property has changed. - /// - public new void OnPropertyChanged(string propertyName) - { - base.OnPropertyChanged(propertyName); - } - } + /// + /// Registers that a property has changed. + /// + public new void OnPropertyChanged(string propertyName) => base.OnPropertyChanged(propertyName); } diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs index c63ee54a6d..887477c743 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs @@ -1,191 +1,176 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections; using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Provides a base implementation of and . +/// +[Serializable] +[DataContract(IsReference = true)] +public abstract class BeingDirtyBase : IRememberBeingDirty { - /// - /// Provides a base implementation of and . - /// - [Serializable] - [DataContract(IsReference = true)] - public abstract class BeingDirtyBase : IRememberBeingDirty + private Dictionary? _currentChanges; // which properties have changed? + private Dictionary? _savedChanges; // which properties had changed at last commit? + private bool _withChanges = true; // should we track changes? + + #region ICanBeDirty + + /// + public virtual bool IsDirty() => _currentChanges != null && _currentChanges.Any(); + + /// + public virtual bool IsPropertyDirty(string propertyName) => + _currentChanges != null && _currentChanges.ContainsKey(propertyName); + + /// + public virtual IEnumerable GetDirtyProperties() => + + // ReSharper disable once MergeConditionalExpression + _currentChanges == null + ? Enumerable.Empty() + : _currentChanges.Where(x => x.Value).Select(x => x.Key); + + /// + /// Saves dirty properties so they can be checked with WasDirty. + public virtual void ResetDirtyProperties() => ResetDirtyProperties(true); + + #endregion + + #region IRememberBeingDirty + + /// + public virtual bool WasDirty() => _savedChanges != null && _savedChanges.Any(); + + /// + public virtual bool WasPropertyDirty(string propertyName) => + _savedChanges != null && _savedChanges.ContainsKey(propertyName); + + /// + public virtual void ResetWereDirtyProperties() => + + // note: cannot .Clear() because when memberwise-cloning this will be the SAME + // instance as the one on the clone, so we need to create a new instance. + _savedChanges = null; + + /// + public virtual void ResetDirtyProperties(bool rememberDirty) { - private bool _withChanges = true; // should we track changes? - private Dictionary? _currentChanges; // which properties have changed? - private Dictionary? _savedChanges; // which properties had changed at last commit? + // capture changes if remembering + // clone the dictionary in case it's shared by an entity clone + _savedChanges = rememberDirty && _currentChanges != null + ? _currentChanges.ToDictionary(v => v.Key, v => v.Value) + : null; - #region ICanBeDirty + // note: cannot .Clear() because when memberwise-clone this will be the SAME + // instance as the one on the clone, so we need to create a new instance. + _currentChanges = null; + } - /// - public virtual bool IsDirty() + /// + public virtual IEnumerable GetWereDirtyProperties() => + + // ReSharper disable once MergeConditionalExpression + _savedChanges == null + ? Enumerable.Empty() + : _savedChanges.Where(x => x.Value).Select(x => x.Key); + + #endregion + + #region Change Tracking + + /// + /// Occurs when a property changes. + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Registers that a property has changed. + /// + protected virtual void OnPropertyChanged(string propertyName) + { + if (_withChanges == false) { - return _currentChanges != null && _currentChanges.Any(); + return; } - /// - public virtual bool IsPropertyDirty(string propertyName) + if (_currentChanges == null) { - return _currentChanges != null && _currentChanges.ContainsKey(propertyName); + _currentChanges = new Dictionary(); } - /// - public virtual IEnumerable GetDirtyProperties() + _currentChanges[propertyName] = true; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Disables change tracking. + /// + public void DisableChangeTracking() => _withChanges = false; + + /// + /// Enables change tracking. + /// + public void EnableChangeTracking() => _withChanges = true; + + /// + /// Sets a property value, detects changes and manages the dirty flag. + /// + /// The type of the value. + /// The new value. + /// A reference to the value to set. + /// The property name. + /// A comparer to compare property values. + protected void SetPropertyValueAndDetectChanges(T? value, ref T? valueRef, string propertyName, IEqualityComparer? comparer = null) + { + if (comparer == null) { - // ReSharper disable once MergeConditionalExpression - return _currentChanges == null - ? Enumerable.Empty() - : _currentChanges.Where(x => x.Value).Select(x => x.Key); - } - - /// - /// Saves dirty properties so they can be checked with WasDirty. - public virtual void ResetDirtyProperties() - { - ResetDirtyProperties(true); - } - - #endregion - - #region IRememberBeingDirty - - /// - public virtual bool WasDirty() - { - return _savedChanges != null && _savedChanges.Any(); - } - - /// - public virtual bool WasPropertyDirty(string propertyName) - { - return _savedChanges != null && _savedChanges.ContainsKey(propertyName); - } - - /// - public virtual void ResetWereDirtyProperties() - { - // note: cannot .Clear() because when memberwise-cloning this will be the SAME - // instance as the one on the clone, so we need to create a new instance. - _savedChanges = null; - } - - /// - public virtual void ResetDirtyProperties(bool rememberDirty) - { - // capture changes if remembering - // clone the dictionary in case it's shared by an entity clone - _savedChanges = rememberDirty && _currentChanges != null - ? _currentChanges.ToDictionary(v => v.Key, v => v.Value) - : null; - - // note: cannot .Clear() because when memberwise-clone this will be the SAME - // instance as the one on the clone, so we need to create a new instance. - _currentChanges = null; - } - - /// - public virtual IEnumerable GetWereDirtyProperties() - { - // ReSharper disable once MergeConditionalExpression - return _savedChanges == null - ? Enumerable.Empty() - : _savedChanges.Where(x => x.Value).Select(x => x.Key); - } - - #endregion - - #region Change Tracking - - /// - /// Occurs when a property changes. - /// - public event PropertyChangedEventHandler? PropertyChanged; - - /// - /// Registers that a property has changed. - /// - protected virtual void OnPropertyChanged(string propertyName) - { - if (_withChanges == false) - return; - - if (_currentChanges == null) - _currentChanges = new Dictionary(); - - _currentChanges[propertyName] = true; - - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - /// - /// Disables change tracking. - /// - public void DisableChangeTracking() - { - _withChanges = false; - } - - /// - /// Enables change tracking. - /// - public void EnableChangeTracking() - { - _withChanges = true; - } - - /// - /// Sets a property value, detects changes and manages the dirty flag. - /// - /// The type of the value. - /// The new value. - /// A reference to the value to set. - /// The property name. - /// A comparer to compare property values. - protected void SetPropertyValueAndDetectChanges(T? value, ref T? valueRef, string propertyName, IEqualityComparer? comparer = null) - { - if (comparer == null) + // if no comparer is provided, use the default provider, as long as the value is not + // an IEnumerable - exclude strings, which are IEnumerable but have a default comparer + Type typeofT = typeof(T); + if (!(typeofT == typeof(string)) && typeof(IEnumerable).IsAssignableFrom(typeofT)) { - // if no comparer is provided, use the default provider, as long as the value is not - // an IEnumerable - exclude strings, which are IEnumerable but have a default comparer - var typeofT = typeof(T); - if (!(typeofT == typeof(string)) && typeof(IEnumerable).IsAssignableFrom(typeofT)) - throw new ArgumentNullException(nameof(comparer), "A custom comparer must be supplied for IEnumerable values."); - comparer = EqualityComparer.Default; + throw new ArgumentNullException(nameof(comparer), "A custom comparer must be supplied for IEnumerable values."); } - // compare values - var changed = _withChanges && comparer.Equals(valueRef, value) == false; - - // assign the new value - valueRef = value; - - // handle change - if (changed) - OnPropertyChanged(propertyName); + comparer = EqualityComparer.Default; } - /// - /// Detects changes and manages the dirty flag. - /// - /// The type of the value. - /// The new value. - /// The original value. - /// The property name. - /// A comparer to compare property values. - /// A value indicating whether we know values have changed and no comparison is required. - protected void DetectChanges(T value, T orig, string propertyName, IEqualityComparer comparer, bool changed) + // compare values + var changed = _withChanges && comparer.Equals(valueRef, value) == false; + + // assign the new value + valueRef = value; + + // handle change + if (changed) { - // compare values - changed = _withChanges && (changed || !comparer.Equals(orig, value)); - - // handle change - if (changed) - OnPropertyChanged(propertyName); + OnPropertyChanged(propertyName); } - - #endregion } + + /// + /// Detects changes and manages the dirty flag. + /// + /// The type of the value. + /// The new value. + /// The original value. + /// The property name. + /// A comparer to compare property values. + /// A value indicating whether we know values have changed and no comparison is required. + protected void DetectChanges(T value, T orig, string propertyName, IEqualityComparer comparer, bool changed) + { + // compare values + changed = _withChanges && (changed || !comparer.Equals(orig, value)); + + // handle change + if (changed) + { + OnPropertyChanged(propertyName); + } + } + + #endregion } diff --git a/src/Umbraco.Core/Models/Entities/ContentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/ContentEntitySlim.cs index 74bd4e4f44..3b9d139ba7 100644 --- a/src/Umbraco.Core/Models/Entities/ContentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/ContentEntitySlim.cs @@ -1,17 +1,16 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Implements . +/// +public class ContentEntitySlim : EntitySlim, IContentEntitySlim { - /// - /// Implements . - /// - public class ContentEntitySlim : EntitySlim, IContentEntitySlim - { - /// - public string ContentTypeAlias { get; set; } = string.Empty; + /// + public string ContentTypeAlias { get; set; } = string.Empty; - /// - public string? ContentTypeIcon { get; set; } + /// + public string? ContentTypeIcon { get; set; } - /// - public string? ContentTypeThumbnail { get; set; } - } + /// + public string? ContentTypeThumbnail { get; set; } } diff --git a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs index 3bc410fc9b..a5c0ca23c9 100644 --- a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs @@ -1,48 +1,42 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Implements . +/// +public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim { + private static readonly IReadOnlyDictionary Empty = new Dictionary(); - /// - /// Implements . - /// - public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim + private IReadOnlyDictionary? _cultureNames; + private IEnumerable? _editedCultures; + private IEnumerable? _publishedCultures; + + /// + public IReadOnlyDictionary CultureNames { - private static readonly IReadOnlyDictionary Empty = new Dictionary(); - - private IReadOnlyDictionary? _cultureNames; - private IEnumerable? _publishedCultures; - private IEnumerable? _editedCultures; - - /// - public IReadOnlyDictionary CultureNames - { - get => _cultureNames ?? Empty; - set => _cultureNames = value; - } - - /// - public IEnumerable PublishedCultures - { - get => _publishedCultures ?? Enumerable.Empty(); - set => _publishedCultures = value; - } - - /// - public IEnumerable EditedCultures - { - get => _editedCultures ?? Enumerable.Empty(); - set => _editedCultures = value; - } - - public ContentVariation Variations { get; set; } - - /// - public bool Published { get; set; } - - /// - public bool Edited { get; set; } - + get => _cultureNames ?? Empty; + set => _cultureNames = value; } + + /// + public IEnumerable PublishedCultures + { + get => _publishedCultures ?? Enumerable.Empty(); + set => _publishedCultures = value; + } + + /// + public IEnumerable EditedCultures + { + get => _editedCultures ?? Enumerable.Empty(); + set => _editedCultures = value; + } + + public ContentVariation Variations { get; set; } + + /// + public bool Published { get; set; } + + /// + public bool Edited { get; set; } } diff --git a/src/Umbraco.Core/Models/Entities/EntityBase.cs b/src/Umbraco.Core/Models/Entities/EntityBase.cs index 57b9eeae1f..df60d97a1e 100644 --- a/src/Umbraco.Core/Models/Entities/EntityBase.cs +++ b/src/Umbraco.Core/Models/Entities/EntityBase.cs @@ -1,155 +1,156 @@ -using System; using System.Diagnostics; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Provides a base class for entities. +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {" + nameof(Id) + "}")] +public abstract class EntityBase : BeingDirtyBase, IEntity { - /// - /// Provides a base class for entities. - /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {" + nameof(Id) + "}")] - public abstract class EntityBase : BeingDirtyBase, IEntity - { #if DEBUG_MODEL public Guid InstanceId = Guid.NewGuid(); #endif - private bool _hasIdentity; - private int _id; - private Guid _key; - private DateTime _createDate; - private DateTime _updateDate; + private bool _hasIdentity; + private int _id; + private Guid _key; + private DateTime _createDate; + private DateTime _updateDate; - /// - [DataMember] - public int Id + /// + [DataMember] + public int Id + { + get => _id; + set { - get => _id; - set + SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); + _hasIdentity = value != 0; + } + } + + /// + [DataMember] + public Guid Key + { + get + { + // if an entity does NOT have a key yet, assign one now + if (_key == Guid.Empty) { - SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); - _hasIdentity = value != 0; + _key = Guid.NewGuid(); } + + return _key; + } + set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); + } + + /// + [DataMember] + public DateTime CreateDate + { + get => _createDate; + set => SetPropertyValueAndDetectChanges(value, ref _createDate, nameof(CreateDate)); + } + + /// + [DataMember] + public DateTime UpdateDate + { + get => _updateDate; + set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate)); + } + + /// + [DataMember] + public DateTime? DeleteDate { get; set; } // no change tracking - not persisted + + /// + [DataMember] + public virtual bool HasIdentity => _hasIdentity; + + /// + /// Resets the entity identity. + /// + public virtual void ResetIdentity() + { + _id = default; + _key = Guid.Empty; + _hasIdentity = false; + } + + public virtual bool Equals(EntityBase? other) => + other != null && (ReferenceEquals(this, other) || SameIdentityAs(other)); + + public override bool Equals(object? obj) => + obj != null && (ReferenceEquals(this, obj) || SameIdentityAs(obj as EntityBase)); + + public override int GetHashCode() + { + unchecked + { + var hashCode = HasIdentity.GetHashCode(); + hashCode = (hashCode * 397) ^ Id; + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } + + private bool SameIdentityAs(EntityBase? other) + { + if (other == null) + { + return false; } - /// - [DataMember] - public Guid Key + // same identity if + // - same object (reference equals) + // - or same CLR type, both have identities, and they are identical + if (ReferenceEquals(this, other)) { - get - { - // if an entity does NOT have a key yet, assign one now - if (_key == Guid.Empty) - _key = Guid.NewGuid(); - return _key; - } - set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); + return true; } - /// - [DataMember] - public DateTime CreateDate - { - get => _createDate; - set => SetPropertyValueAndDetectChanges(value, ref _createDate, nameof(CreateDate)); - } + return GetType() == other.GetType() && HasIdentity && other.HasIdentity && Id == other.Id; + } - /// - [DataMember] - public DateTime UpdateDate - { - get => _updateDate; - set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate)); - } - - /// - [DataMember] - public DateTime? DeleteDate { get; set; } // no change tracking - not persisted - - /// - [DataMember] - public virtual bool HasIdentity => _hasIdentity; - - /// - /// Resets the entity identity. - /// - public virtual void ResetIdentity() - { - _id = default; - _key = Guid.Empty; - _hasIdentity = false; - } - - public virtual bool Equals(EntityBase? other) - { - return other != null && (ReferenceEquals(this, other) || SameIdentityAs(other)); - } - - public override bool Equals(object? obj) - { - return obj != null && (ReferenceEquals(this, obj) || SameIdentityAs(obj as EntityBase)); - } - - private bool SameIdentityAs(EntityBase? other) - { - if (other == null) return false; - - // same identity if - // - same object (reference equals) - // - or same CLR type, both have identities, and they are identical - - if (ReferenceEquals(this, other)) - return true; - - return GetType() == other.GetType() && HasIdentity && other.HasIdentity && Id == other.Id; - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = HasIdentity.GetHashCode(); - hashCode = (hashCode * 397) ^ Id; - hashCode = (hashCode * 397) ^ GetType().GetHashCode(); - return hashCode; - } - } - - public object DeepClone() - { - // memberwise-clone (ie shallow clone) the entity - var unused = Key; // ensure that 'this' has a key, before cloning - var clone = (EntityBase) MemberwiseClone(); + public object DeepClone() + { + // memberwise-clone (ie shallow clone) the entity + Guid unused = Key; // ensure that 'this' has a key, before cloning + var clone = (EntityBase)MemberwiseClone(); #if DEBUG_MODEL clone.InstanceId = Guid.NewGuid(); #endif - //disable change tracking while we deep clone IDeepCloneable properties - clone.DisableChangeTracking(); + // disable change tracking while we deep clone IDeepCloneable properties + clone.DisableChangeTracking(); - // deep clone ref properties that are IDeepCloneable - DeepCloneHelper.DeepCloneRefProperties(this, clone); + // deep clone ref properties that are IDeepCloneable + DeepCloneHelper.DeepCloneRefProperties(this, clone); - PerformDeepClone(clone); + PerformDeepClone(clone); - // clear changes (ensures the clone has its own dictionaries) - clone.ResetDirtyProperties(false); + // clear changes (ensures the clone has its own dictionaries) + clone.ResetDirtyProperties(false); - //re-enable change tracking - clone.EnableChangeTracking(); + // re-enable change tracking + clone.EnableChangeTracking(); - return clone; - } + return clone; + } - /// - /// Used by inheritors to modify the DeepCloning logic - /// - /// - protected virtual void PerformDeepClone(object clone) - { - } + /// + /// Used by inheritors to modify the DeepCloning logic + /// + /// + protected virtual void PerformDeepClone(object clone) + { } } diff --git a/src/Umbraco.Core/Models/Entities/EntityExtensions.cs b/src/Umbraco.Core/Models/Entities/EntityExtensions.cs index ba3421349d..53801875ae 100644 --- a/src/Umbraco.Core/Models/Entities/EntityExtensions.cs +++ b/src/Umbraco.Core/Models/Entities/EntityExtensions.cs @@ -1,49 +1,49 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class EntityExtensions { - public static class EntityExtensions + /// + /// Updates the entity when it is being saved. + /// + public static void UpdatingEntity(this IEntity entity) { - /// - /// Updates the entity when it is being saved. - /// - public static void UpdatingEntity(this IEntity entity) + DateTime now = DateTime.Now; + + if (entity.CreateDate == default) { - var now = DateTime.Now; - - if (entity.CreateDate == default) - { - entity.CreateDate = now; - } - - // set the update date if not already set - if (entity.UpdateDate == default || (entity is ICanBeDirty canBeDirty && canBeDirty.IsPropertyDirty("UpdateDate") == false)) - { - entity.UpdateDate = now; - } + entity.CreateDate = now; } - /// - /// Updates the entity when it is being saved for the first time. - /// - public static void AddingEntity(this IEntity entity) + // set the update date if not already set + if (entity.UpdateDate == default || + (entity is ICanBeDirty canBeDirty && canBeDirty.IsPropertyDirty("UpdateDate") == false)) { - var now = DateTime.Now; - var canBeDirty = entity as ICanBeDirty; + entity.UpdateDate = now; + } + } - // set the create and update dates, if not already set - if (entity.CreateDate == default || canBeDirty?.IsPropertyDirty("CreateDate") == false) - { - entity.CreateDate = now; - } - if (entity.UpdateDate == default || canBeDirty?.IsPropertyDirty("UpdateDate") == false) - { - entity.UpdateDate = now; - } + /// + /// Updates the entity when it is being saved for the first time. + /// + public static void AddingEntity(this IEntity entity) + { + DateTime now = DateTime.Now; + var canBeDirty = entity as ICanBeDirty; + + // set the create and update dates, if not already set + if (entity.CreateDate == default || canBeDirty?.IsPropertyDirty("CreateDate") == false) + { + entity.CreateDate = now; + } + + if (entity.UpdateDate == default || canBeDirty?.IsPropertyDirty("UpdateDate") == false) + { + entity.UpdateDate = now; } } } diff --git a/src/Umbraco.Core/Models/Entities/EntitySlim.cs b/src/Umbraco.Core/Models/Entities/EntitySlim.cs index c4bc473661..91acaea3fd 100644 --- a/src/Umbraco.Core/Models/Entities/EntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/EntitySlim.cs @@ -1,181 +1,153 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Implementation of for internal use. +/// +/// +/// +/// Although it implements , this class does not +/// implement and everything this interface defines, throws. +/// +/// +/// Although it implements , this class does not +/// implement and deep-cloning throws. +/// +/// +public class EntitySlim : IEntitySlim { /// - /// Implementation of for internal use. + /// Gets an entity representing "root". /// - /// - /// Although it implements , this class does not - /// implement and everything this interface defines, throws. - /// Although it implements , this class does not - /// implement and deep-cloning throws. - /// - public class EntitySlim : IEntitySlim + public static readonly IEntitySlim Root = new EntitySlim { Path = "-1", Name = "root", HasChildren = true }; + + private IDictionary? _additionalData; + + // implement IEntity + + /// + [DataMember] + public int Id { get; set; } + + /// + [DataMember] + public Guid Key { get; set; } + + /// + [DataMember] + public DateTime CreateDate { get; set; } + + /// + [DataMember] + public DateTime UpdateDate { get; set; } + + /// + [DataMember] + public DateTime? DeleteDate { get; set; } + + /// + [DataMember] + public bool HasIdentity => Id != 0; + + // implement ITreeEntity + + /// + [DataMember] + public string? Name { get; set; } + + /// + [DataMember] + public int CreatorId { get; set; } + + /// + [DataMember] + public int ParentId { get; set; } + + /// + [DataMember] + public int Level { get; set; } + + /// + public void SetParent(ITreeEntity? parent) => + throw new InvalidOperationException("This property won't be implemented."); + + /// + [DataMember] + public string Path { get; set; } = string.Empty; + + /// + [DataMember] + public int SortOrder { get; set; } + + /// + [DataMember] + public bool Trashed { get; set; } + + // implement IUmbracoEntity + + /// + [DataMember] + public IDictionary? AdditionalData => +_additionalData ??= new Dictionary(); + + /// + [IgnoreDataMember] + public bool HasAdditionalData => _additionalData != null; + + // implement IEntitySlim + + /// + [DataMember] + public Guid NodeObjectType { get; set; } + + /// + [DataMember] + public bool HasChildren { get; set; } + + /// + [DataMember] + public virtual bool IsContainer { get; set; } + + #region IDeepCloneable + + /// + public object DeepClone() => throw new InvalidOperationException("This method won't be implemented."); + + #endregion + + public void ResetIdentity() { - private IDictionary? _additionalData; - - /// - /// Gets an entity representing "root". - /// - public static readonly IEntitySlim Root = new EntitySlim { Path = "-1", Name = "root", HasChildren = true }; - - // implement IEntity - - /// - [DataMember] - public int Id { get; set; } - - /// - [DataMember] - public Guid Key { get; set; } - - /// - [DataMember] - public DateTime CreateDate { get; set; } - - /// - [DataMember] - public DateTime UpdateDate { get; set; } - - /// - [DataMember] - public DateTime? DeleteDate { get; set; } - - /// - [DataMember] - public bool HasIdentity => Id != 0; - - - // implement ITreeEntity - - /// - [DataMember] - public string? Name { get; set; } - - /// - [DataMember] - public int CreatorId { get; set; } - - /// - [DataMember] - public int ParentId { get; set; } - - /// - public void SetParent(ITreeEntity? parent) => throw new InvalidOperationException("This property won't be implemented."); - - /// - [DataMember] - public int Level { get; set; } - - /// - [DataMember] - public string Path { get; set; } = string.Empty; - - /// - [DataMember] - public int SortOrder { get; set; } - - /// - [DataMember] - public bool Trashed { get; set; } - - - // implement IUmbracoEntity - - /// - [DataMember] - public IDictionary? AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); - - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; - - - // implement IEntitySlim - - /// - [DataMember] - public Guid NodeObjectType { get; set; } - - /// - [DataMember] - public bool HasChildren { get; set; } - - /// - [DataMember] - public virtual bool IsContainer { get; set; } - - - #region IDeepCloneable - - /// - public object DeepClone() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - #endregion - - public void ResetIdentity() - { - Id = default; - Key = Guid.Empty; - } - - #region IRememberBeingDirty - - // IEntitySlim does *not* track changes, but since it indirectly implements IUmbracoEntity, - // and therefore IRememberBeingDirty, we have to have those methods - which all throw. - - public bool IsDirty() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public bool IsPropertyDirty(string propName) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public IEnumerable GetDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public void ResetDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public bool WasDirty() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public bool WasPropertyDirty(string propertyName) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public void ResetWereDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public void ResetDirtyProperties(bool rememberDirty) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - public IEnumerable GetWereDirtyProperties() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - #endregion - + Id = default; + Key = Guid.Empty; } + + #region IRememberBeingDirty + + // IEntitySlim does *not* track changes, but since it indirectly implements IUmbracoEntity, + // and therefore IRememberBeingDirty, we have to have those methods - which all throw. + public bool IsDirty() => throw new InvalidOperationException("This method won't be implemented."); + + public bool IsPropertyDirty(string propName) => + throw new InvalidOperationException("This method won't be implemented."); + + public IEnumerable GetDirtyProperties() => + throw new InvalidOperationException("This method won't be implemented."); + + public void ResetDirtyProperties() => throw new InvalidOperationException("This method won't be implemented."); + + public bool WasDirty() => throw new InvalidOperationException("This method won't be implemented."); + + public bool WasPropertyDirty(string propertyName) => + throw new InvalidOperationException("This method won't be implemented."); + + public void ResetWereDirtyProperties() => throw new InvalidOperationException("This method won't be implemented."); + + public void ResetDirtyProperties(bool rememberDirty) => + throw new InvalidOperationException("This method won't be implemented."); + + public IEnumerable GetWereDirtyProperties() => + throw new InvalidOperationException("This method won't be implemented."); + + #endregion } diff --git a/src/Umbraco.Core/Models/Entities/ICanBeDirty.cs b/src/Umbraco.Core/Models/Entities/ICanBeDirty.cs index d8644431d5..23d50d54d9 100644 --- a/src/Umbraco.Core/Models/Entities/ICanBeDirty.cs +++ b/src/Umbraco.Core/Models/Entities/ICanBeDirty.cs @@ -1,43 +1,41 @@ -using System.Collections.Generic; using System.ComponentModel; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Defines an entity that tracks property changes and can be dirty. +/// +public interface ICanBeDirty { + event PropertyChangedEventHandler PropertyChanged; + /// - /// Defines an entity that tracks property changes and can be dirty. + /// Determines whether the current entity is dirty. /// - public interface ICanBeDirty - { - /// - /// Determines whether the current entity is dirty. - /// - bool IsDirty(); + bool IsDirty(); - /// - /// Determines whether a specific property is dirty. - /// - bool IsPropertyDirty(string propName); + /// + /// Determines whether a specific property is dirty. + /// + bool IsPropertyDirty(string propName); - /// - /// Gets properties that are dirty. - /// - IEnumerable GetDirtyProperties(); + /// + /// Gets properties that are dirty. + /// + IEnumerable GetDirtyProperties(); - /// - /// Resets dirty properties. - /// - void ResetDirtyProperties(); + /// + /// Resets dirty properties. + /// + void ResetDirtyProperties(); - /// - /// Disables change tracking. - /// - void DisableChangeTracking(); + /// + /// Disables change tracking. + /// + void DisableChangeTracking(); - /// - /// Enables change tracking. - /// - void EnableChangeTracking(); - - event PropertyChangedEventHandler PropertyChanged; - } + /// + /// Enables change tracking. + /// + void EnableChangeTracking(); } diff --git a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs index 52ea701af3..78ddf9bd82 100644 --- a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs @@ -1,23 +1,22 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Represents a lightweight content entity, managed by the entity service. +/// +public interface IContentEntitySlim : IEntitySlim { /// - /// Represents a lightweight content entity, managed by the entity service. + /// Gets the content type alias. /// - public interface IContentEntitySlim : IEntitySlim - { - /// - /// Gets the content type alias. - /// - string ContentTypeAlias { get; } + string ContentTypeAlias { get; } - /// - /// Gets the content type icon. - /// - string? ContentTypeIcon { get; } + /// + /// Gets the content type icon. + /// + string? ContentTypeIcon { get; } - /// - /// Gets the content type thumbnail. - /// - string? ContentTypeThumbnail { get; } - } + /// + /// Gets the content type thumbnail. + /// + string? ContentTypeThumbnail { get; } } diff --git a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs index d160e144bb..75e16476c2 100644 --- a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs @@ -1,42 +1,37 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Represents a lightweight document entity, managed by the entity service. +/// +public interface IDocumentEntitySlim : IContentEntitySlim { + /// + /// Gets the variant name for each culture + /// + IReadOnlyDictionary CultureNames { get; } /// - /// Represents a lightweight document entity, managed by the entity service. + /// Gets the published cultures. /// - public interface IDocumentEntitySlim : IContentEntitySlim - { - /// - /// Gets the variant name for each culture - /// - IReadOnlyDictionary CultureNames { get; } + IEnumerable PublishedCultures { get; } - /// - /// Gets the published cultures. - /// - IEnumerable PublishedCultures { get; } + /// + /// Gets the edited cultures. + /// + IEnumerable EditedCultures { get; } - /// - /// Gets the edited cultures. - /// - IEnumerable EditedCultures { get; } + /// + /// Gets the content variation of the content type. + /// + ContentVariation Variations { get; } - /// - /// Gets the content variation of the content type. - /// - ContentVariation Variations { get; } + /// + /// Gets a value indicating whether the content is published. + /// + bool Published { get; } - /// - /// Gets a value indicating whether the content is published. - /// - bool Published { get; } - - /// - /// Gets a value indicating whether the content has been edited. - /// - bool Edited { get; } - - } + /// + /// Gets a value indicating whether the content has been edited. + /// + bool Edited { get; } } diff --git a/src/Umbraco.Core/Models/Entities/IEntity.cs b/src/Umbraco.Core/Models/Entities/IEntity.cs index 6aeea58553..859975adfb 100644 --- a/src/Umbraco.Core/Models/Entities/IEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IEntity.cs @@ -1,47 +1,46 @@ -using System; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Defines an entity. +/// +public interface IEntity : IDeepCloneable { /// - /// Defines an entity. + /// Gets or sets the integer identifier of the entity. /// - public interface IEntity : IDeepCloneable - { - /// - /// Gets or sets the integer identifier of the entity. - /// - int Id { get; set; } + int Id { get; set; } - /// - /// Gets or sets the Guid unique identifier of the entity. - /// - Guid Key { get; set; } + /// + /// Gets or sets the Guid unique identifier of the entity. + /// + Guid Key { get; set; } - /// - /// Gets or sets the creation date. - /// - DateTime CreateDate { get; set; } + /// + /// Gets or sets the creation date. + /// + DateTime CreateDate { get; set; } - /// - /// Gets or sets the last update date. - /// - DateTime UpdateDate { get; set; } + /// + /// Gets or sets the last update date. + /// + DateTime UpdateDate { get; set; } - /// - /// Gets or sets the delete date. - /// - /// - /// The delete date is null when the entity has not been deleted. - /// The delete date has a value when the entity instance has been deleted, but this value - /// is transient and not persisted in database (since the entity does not exist anymore). - /// - DateTime? DeleteDate { get; set; } + /// + /// Gets or sets the delete date. + /// + /// + /// The delete date is null when the entity has not been deleted. + /// + /// The delete date has a value when the entity instance has been deleted, but this value + /// is transient and not persisted in database (since the entity does not exist anymore). + /// + /// + DateTime? DeleteDate { get; set; } - /// - /// Gets a value indicating whether the entity has an identity. - /// - bool HasIdentity { get; } + /// + /// Gets a value indicating whether the entity has an identity. + /// + bool HasIdentity { get; } - void ResetIdentity(); - } + void ResetIdentity(); } diff --git a/src/Umbraco.Core/Models/Entities/IEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IEntitySlim.cs index dfdb00edaa..120d417d1a 100644 --- a/src/Umbraco.Core/Models/Entities/IEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IEntitySlim.cs @@ -1,25 +1,22 @@ -using System; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Represents a lightweight entity, managed by the entity service. +/// +public interface IEntitySlim : IUmbracoEntity, IHaveAdditionalData { /// - /// Represents a lightweight entity, managed by the entity service. + /// Gets or sets the entity object type. /// - public interface IEntitySlim : IUmbracoEntity, IHaveAdditionalData - { - /// - /// Gets or sets the entity object type. - /// - Guid NodeObjectType { get; } + Guid NodeObjectType { get; } - /// - /// Gets or sets a value indicating whether the entity has children. - /// - bool HasChildren { get; } + /// + /// Gets or sets a value indicating whether the entity has children. + /// + bool HasChildren { get; } - /// - /// Gets a value indicating whether the entity is a container. - /// - bool IsContainer { get; } - } + /// + /// Gets a value indicating whether the entity is a container. + /// + bool IsContainer { get; } } diff --git a/src/Umbraco.Core/Models/Entities/IHaveAdditionalData.cs b/src/Umbraco.Core/Models/Entities/IHaveAdditionalData.cs index 651e6a5f7a..a2ac3a247a 100644 --- a/src/Umbraco.Core/Models/Entities/IHaveAdditionalData.cs +++ b/src/Umbraco.Core/Models/Entities/IHaveAdditionalData.cs @@ -1,42 +1,43 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Provides support for additional data. +/// +/// +/// Additional data are transient, not deep-cloned. +/// +public interface IHaveAdditionalData { /// - /// Provides support for additional data. + /// Gets additional data for this entity. /// /// - /// Additional data are transient, not deep-cloned. + /// Can be empty, but never null. To avoid allocating, do not + /// test for emptiness, but use instead. /// - public interface IHaveAdditionalData - { - /// - /// Gets additional data for this entity. - /// - /// Can be empty, but never null. To avoid allocating, do not - /// test for emptiness, but use instead. - IDictionary? AdditionalData { get; } + IDictionary? AdditionalData { get; } - /// - /// Determines whether this entity has additional data. - /// - /// Use this property to check for additional data without - /// getting , to avoid allocating. - bool HasAdditionalData { get; } + /// + /// Determines whether this entity has additional data. + /// + /// + /// Use this property to check for additional data without + /// getting , to avoid allocating. + /// + bool HasAdditionalData { get; } - // how to implement: + // how to implement: - /* - private IDictionary _additionalData; + /* + private IDictionary _additionalData; - /// - [DataMember] - [DoNotClone] - PublicAccessEntry IDictionary AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); + /// + [DataMember] + [DoNotClone] + PublicAccessEntry IDictionary AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); - /// - [IgnoreDataMember] - PublicAccessEntry bool HasAdditionalData => _additionalData != null; - */ - } + /// + [IgnoreDataMember] + PublicAccessEntry bool HasAdditionalData => _additionalData != null; + */ } diff --git a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs index 3a2996c6fe..019a6f1f7b 100644 --- a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs @@ -1,14 +1,12 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Represents a lightweight media entity, managed by the entity service. +/// +public interface IMediaEntitySlim : IContentEntitySlim { /// - /// Represents a lightweight media entity, managed by the entity service. + /// The media file's path/URL /// - public interface IMediaEntitySlim : IContentEntitySlim - { - - /// - /// The media file's path/URL - /// - string? MediaPath { get; } - } + string? MediaPath { get; } } diff --git a/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs index a43607fda7..0ded537035 100644 --- a/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs @@ -1,7 +1,5 @@ -namespace Umbraco.Cms.Core.Models.Entities -{ - public interface IMemberEntitySlim : IContentEntitySlim - { +namespace Umbraco.Cms.Core.Models.Entities; - } +public interface IMemberEntitySlim : IContentEntitySlim +{ } diff --git a/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs b/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs index 618bab2698..85c1c472b5 100644 --- a/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs +++ b/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs @@ -1,40 +1,40 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Entities +/// +/// Defines an entity that tracks property changes and can be dirty, and remembers +/// which properties were dirty when the changes were committed. +/// +public interface IRememberBeingDirty : ICanBeDirty { /// - /// Defines an entity that tracks property changes and can be dirty, and remembers - /// which properties were dirty when the changes were committed. + /// Determines whether the current entity is dirty. /// - public interface IRememberBeingDirty : ICanBeDirty - { - /// - /// Determines whether the current entity is dirty. - /// - /// A property was dirty if it had been changed and the changes were committed. - bool WasDirty(); + /// A property was dirty if it had been changed and the changes were committed. + bool WasDirty(); - /// - /// Determines whether a specific property was dirty. - /// - /// A property was dirty if it had been changed and the changes were committed. - bool WasPropertyDirty(string propertyName); + /// + /// Determines whether a specific property was dirty. + /// + /// A property was dirty if it had been changed and the changes were committed. + bool WasPropertyDirty(string propertyName); - /// - /// Resets properties that were dirty. - /// - void ResetWereDirtyProperties(); + /// + /// Resets properties that were dirty. + /// + void ResetWereDirtyProperties(); - /// - /// Resets dirty properties. - /// - /// A value indicating whether to remember dirty properties. - /// When is true, dirty properties are saved so they can be checked with WasDirty. - void ResetDirtyProperties(bool rememberDirty); + /// + /// Resets dirty properties. + /// + /// A value indicating whether to remember dirty properties. + /// + /// When is true, dirty properties are saved so they can be checked with + /// WasDirty. + /// + void ResetDirtyProperties(bool rememberDirty); - /// - /// Gets properties that were dirty. - /// - IEnumerable GetWereDirtyProperties(); - } + /// + /// Gets properties that were dirty. + /// + IEnumerable GetWereDirtyProperties(); } diff --git a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs index af105d63ff..b66368e425 100644 --- a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs +++ b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs @@ -1,56 +1,57 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Defines an entity that belongs to a tree. +/// +public interface ITreeEntity : IEntity { /// - /// Defines an entity that belongs to a tree. + /// Gets or sets the name of the entity. /// - public interface ITreeEntity : IEntity - { - /// - /// Gets or sets the name of the entity. - /// - string? Name { get; set; } + string? Name { get; set; } - /// - /// Gets or sets the identifier of the user who created this entity. - /// - int CreatorId { get; set; } + /// + /// Gets or sets the identifier of the user who created this entity. + /// + int CreatorId { get; set; } - /// - /// Gets or sets the identifier of the parent entity. - /// - int ParentId { get; set; } + /// + /// Gets or sets the identifier of the parent entity. + /// + int ParentId { get; set; } - /// - /// Sets the parent entity. - /// - /// Use this method to set the parent entity when the parent entity is known, but has not - /// been persistent and does not yet have an identity. The parent identifier will be retrieved - /// from the parent entity when needed. If the parent entity still does not have an entity by that - /// time, an exception will be thrown by getter. - void SetParent(ITreeEntity? parent); + /// + /// Gets or sets the level of the entity. + /// + int Level { get; set; } - /// - /// Gets or sets the level of the entity. - /// - int Level { get; set; } + /// + /// Gets or sets the path to the entity. + /// + string Path { get; set; } - /// - /// Gets or sets the path to the entity. - /// - string Path { get; set; } + /// + /// Gets or sets the sort order of the entity. + /// + int SortOrder { get; set; } - /// - /// Gets or sets the sort order of the entity. - /// - int SortOrder { get; set; } + /// + /// Gets a value indicating whether this entity is trashed. + /// + /// + /// Trashed entities are located in the recycle bin. + /// Always false for entities that do not support being trashed. + /// + bool Trashed { get; } - /// - /// Gets a value indicating whether this entity is trashed. - /// - /// - /// Trashed entities are located in the recycle bin. - /// Always false for entities that do not support being trashed. - /// - bool Trashed { get; } - } + /// + /// Sets the parent entity. + /// + /// + /// Use this method to set the parent entity when the parent entity is known, but has not + /// been persistent and does not yet have an identity. The parent identifier will be retrieved + /// from the parent entity when needed. If the parent entity still does not have an entity by that + /// time, an exception will be thrown by getter. + /// + void SetParent(ITreeEntity? parent); } diff --git a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs index d89e5d9312..3d8c89c7c4 100644 --- a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Models.Entities -{ +namespace Umbraco.Cms.Core.Models.Entities; - /// - /// Represents an entity that can be managed by the entity service. - /// - /// - /// An IUmbracoEntity can be related to another via the IRelationService. - /// IUmbracoEntities can be retrieved with the IEntityService. - /// An IUmbracoEntity can participate in notifications. - /// - public interface IUmbracoEntity : ITreeEntity - { } +/// +/// Represents an entity that can be managed by the entity service. +/// +/// +/// An IUmbracoEntity can be related to another via the IRelationService. +/// IUmbracoEntities can be retrieved with the IEntityService. +/// An IUmbracoEntity can participate in notifications. +/// +public interface IUmbracoEntity : ITreeEntity +{ } diff --git a/src/Umbraco.Core/Models/Entities/IValueObject.cs b/src/Umbraco.Core/Models/Entities/IValueObject.cs index e1b7ea01a6..f101f531fa 100644 --- a/src/Umbraco.Core/Models/Entities/IValueObject.cs +++ b/src/Umbraco.Core/Models/Entities/IValueObject.cs @@ -1,11 +1,9 @@ -namespace Umbraco.Cms.Core.Models.Entities -{ - /// - /// Marker interface for value object, eg. objects without - /// the same kind of identity as an Entity (with its Id). - /// - public interface IValueObject - { +namespace Umbraco.Cms.Core.Models.Entities; - } +/// +/// Marker interface for value object, eg. objects without +/// the same kind of identity as an Entity (with its Id). +/// +public interface IValueObject +{ } diff --git a/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs index fd3c01e15c..fb73e2332d 100644 --- a/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/MediaEntitySlim.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Implements . +/// +public class MediaEntitySlim : ContentEntitySlim, IMediaEntitySlim { - /// - /// Implements . - /// - public class MediaEntitySlim : ContentEntitySlim, IMediaEntitySlim - { - public string? MediaPath { get; set; } - } + public string? MediaPath { get; set; } } diff --git a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs index 66e3650fc5..923fef2477 100644 --- a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs @@ -1,6 +1,5 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +public class MemberEntitySlim : ContentEntitySlim, IMemberEntitySlim { - public class MemberEntitySlim : ContentEntitySlim, IMemberEntitySlim - { - } } diff --git a/src/Umbraco.Core/Models/Entities/TreeEntityBase.cs b/src/Umbraco.Core/Models/Entities/TreeEntityBase.cs index b5d6f40a4c..f10e49b957 100644 --- a/src/Umbraco.Core/Models/Entities/TreeEntityBase.cs +++ b/src/Umbraco.Core/Models/Entities/TreeEntityBase.cs @@ -1,106 +1,120 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Provides a base class for tree entities. +/// +public abstract class TreeEntityBase : EntityBase, ITreeEntity { - /// - /// Provides a base class for tree entities. - /// - public abstract class TreeEntityBase : EntityBase, ITreeEntity + private int _creatorId; + private bool _hasParentId; + private int _level; + private string _name = null!; + private ITreeEntity? _parent; + private int _parentId; + private string _path = string.Empty; + private int _sortOrder; + private bool _trashed; + + /// + [DataMember] + public string? Name { - private string _name = null!; - private int _creatorId; - private int _parentId; - private bool _hasParentId; - private ITreeEntity? _parent; - private int _level; - private string _path = String.Empty; - private int _sortOrder; - private bool _trashed; + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } + /// + [DataMember] + public int CreatorId + { + get => _creatorId; + set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); + } - /// - [DataMember] - public int CreatorId + /// + [DataMember] + public int ParentId + { + get { - get => _creatorId; - set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); - } - - /// - [DataMember] - public int ParentId - { - get + if (_hasParentId) { - if (_hasParentId) return _parentId; - - if (_parent == null) throw new InvalidOperationException("Content does not have a parent."); - if (!_parent.HasIdentity) throw new InvalidOperationException("Content's parent does not have an identity."); - - _parentId = _parent.Id; - if (_parentId == 0) - throw new Exception("Panic: parent has an identity but id is zero."); - - _hasParentId = true; - _parent = null; return _parentId; } - set + + if (_parent == null) { - if (value == 0) - throw new ArgumentException("Value cannot be zero.", nameof(value)); - SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); - _hasParentId = true; - _parent = null; + throw new InvalidOperationException("Content does not have a parent."); } + + if (!_parent.HasIdentity) + { + throw new InvalidOperationException("Content's parent does not have an identity."); + } + + _parentId = _parent.Id; + if (_parentId == 0) + { + throw new Exception("Panic: parent has an identity but id is zero."); + } + + _hasParentId = true; + _parent = null; + return _parentId; } - /// - public void SetParent(ITreeEntity? parent) + set { - _hasParentId = false; - _parent = parent; - OnPropertyChanged(nameof(ParentId)); - } + if (value == 0) + { + throw new ArgumentException("Value cannot be zero.", nameof(value)); + } - /// - [DataMember] - public int Level - { - get => _level; - set => SetPropertyValueAndDetectChanges(value, ref _level, nameof(Level)); - } - - /// - [DataMember] - public string Path - { - get => _path; - set => SetPropertyValueAndDetectChanges(value, ref _path!, nameof(Path)); - } - - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } - - /// - [DataMember] - public bool Trashed - { - get => _trashed; - set => SetPropertyValueAndDetectChanges(value, ref _trashed, nameof(Trashed)); + SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); + _hasParentId = true; + _parent = null; } } + + /// + [DataMember] + public int Level + { + get => _level; + set => SetPropertyValueAndDetectChanges(value, ref _level, nameof(Level)); + } + + /// + public void SetParent(ITreeEntity? parent) + { + _hasParentId = false; + _parent = parent; + OnPropertyChanged(nameof(ParentId)); + } + + /// + [DataMember] + public string Path + { + get => _path; + set => SetPropertyValueAndDetectChanges(value, ref _path!, nameof(Path)); + } + + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } + + /// + [DataMember] + public bool Trashed + { + get => _trashed; + set => SetPropertyValueAndDetectChanges(value, ref _trashed, nameof(Trashed)); + } } diff --git a/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs b/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs index 6fd147ace7..fe284a1e11 100644 --- a/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs +++ b/src/Umbraco.Core/Models/Entities/TreeEntityPath.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Models.Entities +namespace Umbraco.Cms.Core.Models.Entities; + +/// +/// Represents the path of a tree entity. +/// +public class TreeEntityPath { /// - /// Represents the path of a tree entity. + /// Gets or sets the identifier of the entity. /// - public class TreeEntityPath - { - /// - /// Gets or sets the identifier of the entity. - /// - public int Id { get; set; } + public int Id { get; set; } - /// - /// Gets or sets the path of the entity. - /// - public string Path { get; set; } = null!; - } + /// + /// Gets or sets the path of the entity. + /// + public string Path { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 114d78605c..762297af07 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -1,88 +1,91 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a folder for organizing entities such as content types and data types. +/// +public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity { - /// - /// Represents a folder for organizing entities such as content types and data types. - /// - public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity + private static readonly Dictionary ObjectTypeMap = new() { - private readonly Guid _containedObjectType; + { Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer }, + { Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer }, + { Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer }, + }; - private static readonly Dictionary ObjectTypeMap = new Dictionary + /// + /// Initializes a new instance of an class. + /// + public EntityContainer(Guid containedObjectType) + { + if (ObjectTypeMap.ContainsKey(containedObjectType) == false) { - { Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer }, - { Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer }, - { Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer } - }; - - /// - /// Initializes a new instance of an class. - /// - public EntityContainer(Guid containedObjectType) - { - if (ObjectTypeMap.ContainsKey(containedObjectType) == false) - throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); - _containedObjectType = containedObjectType; - - ParentId = -1; - Path = "-1"; - Level = 0; - SortOrder = 0; + throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); } - /// - /// Initializes a new instance of an class. - /// - public EntityContainer(int id, Guid uniqueId, int parentId, string path, int level, int sortOrder, Guid containedObjectType, string? name, int userId) - : this(containedObjectType) + ContainedObjectType = containedObjectType; + + ParentId = -1; + Path = "-1"; + Level = 0; + SortOrder = 0; + } + + /// + /// Initializes a new instance of an class. + /// + public EntityContainer(int id, Guid uniqueId, int parentId, string path, int level, int sortOrder, Guid containedObjectType, string? name, int userId) + : this(containedObjectType) + { + Id = id; + Key = uniqueId; + ParentId = parentId; + Name = name; + Path = path; + Level = level; + SortOrder = sortOrder; + CreatorId = userId; + } + + /// + /// Gets or sets the node object type of the contained objects. + /// + public Guid ContainedObjectType { get; } + + /// + /// Gets the node object type of the container objects. + /// + public Guid ContainerObjectType => ObjectTypeMap[ContainedObjectType]; + + /// + /// Gets the container object type corresponding to a contained object type. + /// + /// The contained object type. + /// The object type of containers containing objects of the contained object type. + public static Guid GetContainerObjectType(Guid containedObjectType) + { + if (ObjectTypeMap.ContainsKey(containedObjectType) == false) { - Id = id; - Key = uniqueId; - ParentId = parentId; - Name = name; - Path = path; - Level = level; - SortOrder = sortOrder; - CreatorId = userId; + throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); } - /// - /// Gets or sets the node object type of the contained objects. - /// - public Guid ContainedObjectType => _containedObjectType; + return ObjectTypeMap[containedObjectType]; + } - /// - /// Gets the node object type of the container objects. - /// - public Guid ContainerObjectType => ObjectTypeMap[_containedObjectType]; - - /// - /// Gets the container object type corresponding to a contained object type. - /// - /// The contained object type. - /// The object type of containers containing objects of the contained object type. - public static Guid GetContainerObjectType(Guid containedObjectType) + /// + /// Gets the contained object type corresponding to a container object type. + /// + /// The container object type. + /// The object type of objects that containers of the container object type can contain. + public static Guid GetContainedObjectType(Guid containerObjectType) + { + Guid contained = ObjectTypeMap.FirstOrDefault(x => x.Value == containerObjectType).Key; + if (contained == null) { - if (ObjectTypeMap.ContainsKey(containedObjectType) == false) - throw new ArgumentException("Not a contained object type.", nameof(containedObjectType)); - return ObjectTypeMap[containedObjectType]; + throw new ArgumentException("Not a container object type.", nameof(containerObjectType)); } - /// - /// Gets the contained object type corresponding to a container object type. - /// - /// The container object type. - /// The object type of objects that containers of the container object type can contain. - public static Guid GetContainedObjectType(Guid containerObjectType) - { - var contained = ObjectTypeMap.FirstOrDefault(x => x.Value == containerObjectType).Key; - if (contained == null) - throw new ArgumentException("Not a container object type.", nameof(containerObjectType)); - return contained; - } + return contained; } } diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs index 3865d4eee7..8abfdd1ef5 100644 --- a/src/Umbraco.Core/Models/File.cs +++ b/src/Umbraco.Core/Models/File.cs @@ -1,160 +1,154 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an abstract file which provides basic functionality for a File with an Alias and Name +/// +[Serializable] +[DataContract(IsReference = true)] +public abstract class File : EntityBase, IFile { - /// - /// Represents an abstract file which provides basic functionality for a File with an Alias and Name - /// - [Serializable] - [DataContract(IsReference = true)] - public abstract class File : EntityBase, IFile + private string? _alias; + + // initialize to string.Empty so that it is possible to save a new file, + // should use the lazyContent ctor to set it to null when loading existing. + // cannot simply use HasIdentity as some classes (eg Script) override it + // in a weird way. + private string? _content; + private string? _name; + private string _path; + + protected File(string path, Func? getFileContent = null) { - private string _path; - private string _originalPath; + _path = SanitizePath(path); + OriginalPath = _path; + GetFileContent = getFileContent; + _content = getFileContent != null ? null : string.Empty; + } - // initialize to string.Empty so that it is possible to save a new file, - // should use the lazyContent ctor to set it to null when loading existing. - // cannot simply use HasIdentity as some classes (eg Script) override it - // in a weird way. - private string? _content; - public Func? GetFileContent { get; set; } + public Func? GetFileContent { get; set; } - protected File(string path, Func? getFileContent = null) + /// + /// Gets or sets the Name of the File including extension + /// + [DataMember] + public virtual string Name => _name ??= System.IO.Path.GetFileName(Path); + + /// + /// Gets or sets the Alias of the File, which is the name without the extension + /// + [DataMember] + public virtual string Alias + { + get { - _path = SanitizePath(path); - _originalPath = _path; - GetFileContent = getFileContent; - _content = getFileContent != null ? null : string.Empty; - } - - private string? _alias; - private string? _name; - - private static string SanitizePath(string path) - { - return path - .Replace('\\', System.IO.Path.DirectorySeparatorChar) - .Replace('/', System.IO.Path.DirectorySeparatorChar); - - //Don't strip the start - this was a bug fixed in 7.3, see ScriptRepositoryTests.PathTests - //.TrimStart(System.IO.Path.DirectorySeparatorChar) - //.TrimStart('/'); - } - - /// - /// Gets or sets the Name of the File including extension - /// - [DataMember] - public virtual string Name - { - get { return _name ?? (_name = System.IO.Path.GetFileName(Path)); } - } - - /// - /// Gets or sets the Alias of the File, which is the name without the extension - /// - [DataMember] - public virtual string Alias - { - get + if (_alias == null) { - if (_alias == null) + var name = System.IO.Path.GetFileName(Path); + if (name == null) { - var name = System.IO.Path.GetFileName(Path); - if (name == null) return string.Empty; - var lastIndexOf = name.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase); - _alias = name.Substring(0, lastIndexOf); + return string.Empty; } - return _alias; + + var lastIndexOf = name.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase); + _alias = name.Substring(0, lastIndexOf); } - } - /// - /// Gets or sets the Path to the File from the root of the file's associated IFileSystem - /// - [DataMember] - public virtual string Path - { - get { return _path; } - set - { - //reset - _alias = null; - _name = null; - - SetPropertyValueAndDetectChanges(SanitizePath(value), ref _path!, nameof(Path)); - } - } - - /// - /// Gets the original path of the file - /// - public string OriginalPath - { - get { return _originalPath; } - } - - /// - /// Called to re-set the OriginalPath to the Path - /// - public void ResetOriginalPath() - { - _originalPath = _path; - } - - /// - /// Gets or sets the Content of a File - /// - /// Marked as DoNotClone, because it should be lazy-reloaded from disk. - [DataMember] - [DoNotClone] - public virtual string? Content - { - get - { - if (_content != null) - return _content; - - // else, must lazy-load, and ensure it's not null - if (GetFileContent != null) - _content = GetFileContent(this); - return _content ?? (_content = string.Empty); - } - set - { - SetPropertyValueAndDetectChanges( - value ?? string.Empty, // cannot set to null - ref _content, nameof(Content)); - } - } - - /// - /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) - /// - public string? VirtualPath { get; set; } - - // this exists so that class that manage name and alias differently, eg Template, - // can implement their own cloning - (though really, not sure it's even needed) - protected virtual void DeepCloneNameAndAlias(File clone) - { - // set fields that have a lazy value, by forcing evaluation of the lazy - clone._name = Name; - clone._alias = Alias; - } - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedFile = (File)clone; - - // clear fields that were memberwise-cloned and that we don't want to clone - clonedFile._content = null; - - // ... - DeepCloneNameAndAlias(clonedFile); + return _alias; } } + + /// + /// Gets or sets the Path to the File from the root of the file's associated IFileSystem + /// + [DataMember] + public virtual string Path + { + get => _path; + set + { + // reset + _alias = null; + _name = null; + + SetPropertyValueAndDetectChanges(SanitizePath(value), ref _path!, nameof(Path)); + } + } + + /// + /// Gets the original path of the file + /// + public string OriginalPath { get; private set; } + + /// + /// Gets or sets the Content of a File + /// + /// Marked as DoNotClone, because it should be lazy-reloaded from disk. + [DataMember] + [DoNotClone] + public virtual string? Content + { + get + { + if (_content != null) + { + return _content; + } + + // else, must lazy-load, and ensure it's not null + if (GetFileContent != null) + { + _content = GetFileContent(this); + } + + return _content ??= string.Empty; + } + set => + SetPropertyValueAndDetectChanges( + value ?? string.Empty, // cannot set to null + ref _content, + nameof(Content)); + } + + /// + /// Called to re-set the OriginalPath to the Path + /// + public void ResetOriginalPath() => OriginalPath = _path; + + /// + /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) + /// + public string? VirtualPath { get; set; } + + // Don't strip the start - this was a bug fixed in 7.3, see ScriptRepositoryTests.PathTests + // .TrimStart(System.IO.Path.DirectorySeparatorChar) + // .TrimStart('/'); + // this exists so that class that manage name and alias differently, eg Template, + // can implement their own cloning - (though really, not sure it's even needed) + protected virtual void DeepCloneNameAndAlias(File clone) + { + // set fields that have a lazy value, by forcing evaluation of the lazy + clone._name = Name; + clone._alias = Alias; + } + + private static string SanitizePath(string path) => + path + .Replace('\\', System.IO.Path.DirectorySeparatorChar) + .Replace('/', System.IO.Path.DirectorySeparatorChar); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedFile = (File)clone; + + // clear fields that were memberwise-cloned and that we don't want to clone + clonedFile._content = null; + + // ... + DeepCloneNameAndAlias(clonedFile); + } } diff --git a/src/Umbraco.Core/Models/Folder.cs b/src/Umbraco.Core/Models/Folder.cs index 810bcaf3b3..60e636ca6e 100644 --- a/src/Umbraco.Core/Models/Folder.cs +++ b/src/Umbraco.Core/Models/Folder.cs @@ -1,14 +1,10 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public sealed class Folder : EntityBase { - public sealed class Folder : EntityBase - { - public Folder(string folderPath) - { - Path = folderPath; - } + public Folder(string folderPath) => Path = folderPath; - public string Path { get; set; } - } + public string Path { get; set; } } diff --git a/src/Umbraco.Core/Models/HaveAdditionalDataExtensions.cs b/src/Umbraco.Core/Models/HaveAdditionalDataExtensions.cs index 1c1c377403..79db47414a 100644 --- a/src/Umbraco.Core/Models/HaveAdditionalDataExtensions.cs +++ b/src/Umbraco.Core/Models/HaveAdditionalDataExtensions.cs @@ -1,20 +1,27 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class HaveAdditionalDataExtensions { - public static class HaveAdditionalDataExtensions + /// + /// Gets additional data. + /// + public static object? GetAdditionalDataValueIgnoreCase(this IHaveAdditionalData entity, string key, object? defaultValue) { - /// - /// Gets additional data. - /// - public static object? GetAdditionalDataValueIgnoreCase(this IHaveAdditionalData entity, string key, object? defaultValue) + if (!entity.HasAdditionalData) { - if (!entity.HasAdditionalData) return defaultValue; - if (entity.AdditionalData?.ContainsKeyIgnoreCase(key) == false) return defaultValue; - return entity.AdditionalData?.GetValueIgnoreCase(key, defaultValue); + return defaultValue; } + + if (entity.AdditionalData?.ContainsKeyIgnoreCase(key) == false) + { + return defaultValue; + } + + return entity.AdditionalData?.GetValueIgnoreCase(key, defaultValue); } } diff --git a/src/Umbraco.Core/Models/IAuditEntry.cs b/src/Umbraco.Core/Models/IAuditEntry.cs index e12237f06d..3a1b412ce0 100644 --- a/src/Umbraco.Core/Models/IAuditEntry.cs +++ b/src/Umbraco.Core/Models/IAuditEntry.cs @@ -1,60 +1,62 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an audited event. +/// +/// +/// +/// The free-form details properties can be used to capture relevant infos (for example, +/// a user email and identifier) at the time of the audited event, even though they may change +/// later on - but we want to keep a track of their value at that time. +/// +/// +/// Depending on audit loggers, these properties can be purely free-form text, or +/// contain json serialized objects. +/// +/// +public interface IAuditEntry : IEntity, IRememberBeingDirty { /// - /// Represents an audited event. + /// Gets or sets the identifier of the user triggering the audited event. /// - /// - /// The free-form details properties can be used to capture relevant infos (for example, - /// a user email and identifier) at the time of the audited event, even though they may change - /// later on - but we want to keep a track of their value at that time. - /// Depending on audit loggers, these properties can be purely free-form text, or - /// contain json serialized objects. - /// - public interface IAuditEntry : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the identifier of the user triggering the audited event. - /// - int PerformingUserId { get; set; } + int PerformingUserId { get; set; } - /// - /// Gets or sets free-form details about the user triggering the audited event. - /// - string? PerformingDetails { get; set; } + /// + /// Gets or sets free-form details about the user triggering the audited event. + /// + string? PerformingDetails { get; set; } - /// - /// Gets or sets the IP address or the request triggering the audited event. - /// - string? PerformingIp { get; set; } + /// + /// Gets or sets the IP address or the request triggering the audited event. + /// + string? PerformingIp { get; set; } - /// - /// Gets or sets the date and time of the audited event. - /// - DateTime EventDateUtc { get; set; } + /// + /// Gets or sets the date and time of the audited event. + /// + DateTime EventDateUtc { get; set; } - /// - /// Gets or sets the identifier of the user affected by the audited event. - /// - /// Not used when no single user is affected by the event. - int AffectedUserId { get; set; } + /// + /// Gets or sets the identifier of the user affected by the audited event. + /// + /// Not used when no single user is affected by the event. + int AffectedUserId { get; set; } - /// - /// Gets or sets free-form details about the entity affected by the audited event. - /// - /// The entity affected by the event can be another user, a member... - string? AffectedDetails { get; set; } + /// + /// Gets or sets free-form details about the entity affected by the audited event. + /// + /// The entity affected by the event can be another user, a member... + string? AffectedDetails { get; set; } - /// - /// Gets or sets the type of the audited event. - /// - string? EventType { get; set; } + /// + /// Gets or sets the type of the audited event. + /// + string? EventType { get; set; } - /// - /// Gets or sets free-form details about the audited event. - /// - string? EventDetails { get; set; } - } + /// + /// Gets or sets free-form details about the audited event. + /// + string? EventDetails { get; set; } } diff --git a/src/Umbraco.Core/Models/IAuditItem.cs b/src/Umbraco.Core/Models/IAuditItem.cs index dbc7ad1fd4..dbf4fe01e8 100644 --- a/src/Umbraco.Core/Models/IAuditItem.cs +++ b/src/Umbraco.Core/Models/IAuditItem.cs @@ -1,35 +1,34 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an audit item. +/// +public interface IAuditItem : IEntity { /// - /// Represents an audit item. + /// Gets the audit type. /// - public interface IAuditItem : IEntity - { - /// - /// Gets the audit type. - /// - AuditType AuditType { get; } + AuditType AuditType { get; } - /// - /// Gets the audited entity type. - /// - string? EntityType { get; } + /// + /// Gets the audited entity type. + /// + string? EntityType { get; } - /// - /// Gets the audit user identifier. - /// - int UserId { get; } + /// + /// Gets the audit user identifier. + /// + int UserId { get; } - /// - /// Gets the audit comments. - /// - string? Comment { get; } + /// + /// Gets the audit comments. + /// + string? Comment { get; } - /// - /// Gets optional additional data parameters. - /// - string? Parameters { get; } - } + /// + /// Gets optional additional data parameters. + /// + string? Parameters { get; } } diff --git a/src/Umbraco.Core/Models/IConsent.cs b/src/Umbraco.Core/Models/IConsent.cs index 747e7a145c..bae0294283 100644 --- a/src/Umbraco.Core/Models/IConsent.cs +++ b/src/Umbraco.Core/Models/IConsent.cs @@ -1,55 +1,55 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a consent state. +/// +/// +/// +/// A consent is fully identified by a source (whoever is consenting), a context (for +/// example, an application), and an action (whatever is consented). +/// +/// A consent state registers the state of the consent (granted, revoked...). +/// +public interface IConsent : IEntity, IRememberBeingDirty { /// - /// Represents a consent state. + /// Determines whether the consent entity represents the current state. + /// + bool Current { get; } + + /// + /// Gets the unique identifier of whoever is consenting. + /// + string? Source { get; } + + /// + /// Gets the unique identifier of the context of the consent. /// /// - /// A consent is fully identified by a source (whoever is consenting), a context (for - /// example, an application), and an action (whatever is consented). - /// A consent state registers the state of the consent (granted, revoked...). + /// Represents the domain, application, scope... of the action. + /// When the action is a Udi, this should be the Udi type. /// - public interface IConsent : IEntity, IRememberBeingDirty - { - /// - /// Determines whether the consent entity represents the current state. - /// - bool Current { get; } + string? Context { get; } - /// - /// Gets the unique identifier of whoever is consenting. - /// - string? Source { get; } + /// + /// Gets the unique identifier of the consented action. + /// + string? Action { get; } - /// - /// Gets the unique identifier of the context of the consent. - /// - /// - /// Represents the domain, application, scope... of the action. - /// When the action is a Udi, this should be the Udi type. - /// - string? Context { get; } + /// + /// Gets the state of the consent. + /// + ConsentState State { get; } - /// - /// Gets the unique identifier of the consented action. - /// - string? Action { get; } + /// + /// Gets some additional free text. + /// + string? Comment { get; } - /// - /// Gets the state of the consent. - /// - ConsentState State { get; } - - /// - /// Gets some additional free text. - /// - string? Comment { get; } - - /// - /// Gets the previous states of this consent. - /// - IEnumerable? History { get; } - } + /// + /// Gets the previous states of this consent. + /// + IEnumerable? History { get; } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index e538b307e2..9e36306cfc 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -1,131 +1,138 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a document. +/// +/// +/// A document can be published, rendered by a template. +/// +public interface IContent : IContentBase { + /// + /// Gets or sets the template id used to render the content. + /// + int? TemplateId { get; set; } /// - /// Represents a document. + /// Gets a value indicating whether the content is published. + /// + /// The property tells you which version of the content is currently published. + bool Published { get; set; } + + PublishedState PublishedState { get; set; } + + /// + /// Gets a value indicating whether the content has been edited. /// /// - /// A document can be published, rendered by a template. + /// Will return `true` once unpublished edits have been made after the version with + /// has been published. /// - public interface IContent : IContentBase - { - /// - /// Gets or sets the template id used to render the content. - /// - int? TemplateId { get; set; } + bool Edited { get; set; } - /// - /// Gets a value indicating whether the content is published. - /// - /// The property tells you which version of the content is currently published. - bool Published { get; set; } + /// + /// Gets the version identifier for the currently published version of the content. + /// + int PublishedVersionId { get; set; } - PublishedState PublishedState { get; set; } + /// + /// Gets a value indicating whether the content item is a blueprint. + /// + bool Blueprint { get; set; } - /// - /// Gets a value indicating whether the content has been edited. - /// - /// Will return `true` once unpublished edits have been made after the version with has been published. - bool Edited { get; set; } + /// + /// Gets the template id used to render the published version of the content. + /// + /// When editing the content, the template can change, but this will not until the content is published. + int? PublishTemplateId { get; set; } - /// - /// Gets the version identifier for the currently published version of the content. - /// - int PublishedVersionId { get; set; } + /// + /// Gets the name of the published version of the content. + /// + /// When editing the content, the name can change, but this will not until the content is published. + string? PublishName { get; set; } - /// - /// Gets a value indicating whether the content item is a blueprint. - /// - bool Blueprint { get; set; } + /// + /// Gets the identifier of the user who published the content. + /// + int? PublisherId { get; set; } - /// - /// Gets the template id used to render the published version of the content. - /// - /// When editing the content, the template can change, but this will not until the content is published. - int? PublishTemplateId { get; set; } + /// + /// Gets the date and time the content was published. + /// + DateTime? PublishDate { get; set; } - /// - /// Gets the name of the published version of the content. - /// - /// When editing the content, the name can change, but this will not until the content is published. - string? PublishName { get; set; } + /// + /// Gets the published culture infos of the content. + /// + /// + /// + /// Because a dictionary key cannot be null this cannot get the invariant + /// name, which must be get via the property. + /// + /// + ContentCultureInfosCollection? PublishCultureInfos { get; set; } - /// - /// Gets the identifier of the user who published the content. - /// - int? PublisherId { get; set; } + /// + /// Gets the published cultures. + /// + IEnumerable PublishedCultures { get; } - /// - /// Gets the date and time the content was published. - /// - DateTime? PublishDate { get; set; } + /// + /// Gets the edited cultures. + /// + IEnumerable? EditedCultures { get; set; } - /// - /// Gets a value indicating whether a culture is published. - /// - /// - /// A culture becomes published whenever values for this culture are published, - /// and the content published name for this culture is non-null. It becomes non-published - /// whenever values for this culture are unpublished. - /// A culture becomes published as soon as PublishCulture has been invoked, - /// even though the document might not have been saved yet (and can have no identity). - /// Does not support the '*' wildcard (returns false). - /// - bool IsCulturePublished(string culture); + /// + /// Gets a value indicating whether a culture is published. + /// + /// + /// + /// A culture becomes published whenever values for this culture are published, + /// and the content published name for this culture is non-null. It becomes non-published + /// whenever values for this culture are unpublished. + /// + /// + /// A culture becomes published as soon as PublishCulture has been invoked, + /// even though the document might not have been saved yet (and can have no identity). + /// + /// Does not support the '*' wildcard (returns false). + /// + bool IsCulturePublished(string culture); - /// - /// Gets the date a culture was published. - /// - DateTime? GetPublishDate(string culture); + /// + /// Gets the date a culture was published. + /// + DateTime? GetPublishDate(string culture); - /// - /// Gets a value indicated whether a given culture is edited. - /// - /// - /// A culture is edited when it is available, and not published or published but - /// with changes. - /// A culture can be edited even though the document might now have been saved yet (and can have no identity). - /// Does not support the '*' wildcard (returns false). - /// - bool IsCultureEdited(string culture); + /// + /// Gets a value indicated whether a given culture is edited. + /// + /// + /// + /// A culture is edited when it is available, and not published or published but + /// with changes. + /// + /// A culture can be edited even though the document might now have been saved yet (and can have no identity). + /// Does not support the '*' wildcard (returns false). + /// + bool IsCultureEdited(string culture); - /// - /// Gets the name of the published version of the content for a given culture. - /// - /// - /// When editing the content, the name can change, but this will not until the content is published. - /// When is null, gets the invariant - /// language, which is the value of the property. - /// - string? GetPublishName(string? culture); + /// + /// Gets the name of the published version of the content for a given culture. + /// + /// + /// When editing the content, the name can change, but this will not until the content is published. + /// + /// When is null, gets the invariant + /// language, which is the value of the property. + /// + /// + string? GetPublishName(string? culture); - /// - /// Gets the published culture infos of the content. - /// - /// - /// Because a dictionary key cannot be null this cannot get the invariant - /// name, which must be get via the property. - /// - ContentCultureInfosCollection? PublishCultureInfos { get; set; } - - /// - /// Gets the published cultures. - /// - IEnumerable PublishedCultures { get; } - - /// - /// Gets the edited cultures. - /// - IEnumerable? EditedCultures { get; set; } - - /// - /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset - /// - /// - IContent DeepCloneWithResetIdentities(); - - } + /// + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset + /// + /// + IContent DeepCloneWithResetIdentities(); } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 20e78816ae..5f4ccf5244 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -1,130 +1,141 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Provides a base class for content items. +/// +/// +/// Content items are documents, medias and members. +/// Content items have a content type, and properties. +/// +public interface IContentBase : IUmbracoEntity, IRememberBeingDirty { + /// + /// Integer Id of the default ContentType + /// + int ContentTypeId { get; } /// - /// Provides a base class for content items. + /// Gets the content type of this content. + /// + ISimpleContentType ContentType { get; } + + /// + /// Gets the identifier of the writer. + /// + int WriterId { get; set; } + + /// + /// Gets the version identifier. + /// + int VersionId { get; set; } + + /// + /// Gets culture infos of the content item. /// /// - /// Content items are documents, medias and members. - /// Content items have a content type, and properties. + /// + /// Because a dictionary key cannot be null this cannot contain the invariant + /// culture name, which must be get or set via the property. + /// /// - public interface IContentBase : IUmbracoEntity, IRememberBeingDirty - { - /// - /// Integer Id of the default ContentType - /// - int ContentTypeId { get; } + ContentCultureInfosCollection? CultureInfos { get; set; } - /// - /// Gets the content type of this content. - /// - ISimpleContentType ContentType { get; } + /// + /// Gets the available cultures. + /// + /// + /// Cannot contain the invariant culture, which is always available. + /// + IEnumerable AvailableCultures { get; } - /// - /// Gets the identifier of the writer. - /// - int WriterId { get; set; } + /// + /// List of properties, which make up all the data available for this Content object + /// + /// Properties are loaded as part of the Content object graph + IPropertyCollection Properties { get; set; } - /// - /// Gets the version identifier. - /// - int VersionId { get; set; } + /// + /// Sets the name of the content item for a specified culture. + /// + /// + /// + /// When is null, sets the invariant + /// culture name, which sets the property. + /// + /// + /// When is not null, throws if the content + /// type does not vary by culture. + /// + /// + void SetCultureName(string? value, string? culture); - /// - /// Sets the name of the content item for a specified culture. - /// - /// - /// When is null, sets the invariant - /// culture name, which sets the property. - /// When is not null, throws if the content - /// type does not vary by culture. - /// - void SetCultureName(string? value, string? culture); + /// + /// Gets the name of the content item for a specified language. + /// + /// + /// + /// When is null, gets the invariant + /// culture name, which is the value of the property. + /// + /// + /// When is not null, and the content type + /// does not vary by culture, returns null. + /// + /// + string? GetCultureName(string? culture); - /// - /// Gets the name of the content item for a specified language. - /// - /// - /// When is null, gets the invariant - /// culture name, which is the value of the property. - /// When is not null, and the content type - /// does not vary by culture, returns null. - /// - string? GetCultureName(string? culture); + /// + /// Gets a value indicating whether a given culture is available. + /// + /// + /// + /// A culture becomes available whenever the content name for this culture is + /// non-null, and it becomes unavailable whenever the content name is null. + /// + /// + /// Returns false for the invariant culture, in order to be consistent + /// with , even though the invariant culture is + /// always available. + /// + /// Does not support the '*' wildcard (returns false). + /// + bool IsCultureAvailable(string culture); - /// - /// Gets culture infos of the content item. - /// - /// - /// Because a dictionary key cannot be null this cannot contain the invariant - /// culture name, which must be get or set via the property. - /// - ContentCultureInfosCollection? CultureInfos { get; set; } + /// + /// Gets the date a culture was updated. + /// + /// + /// When is null, returns null. + /// If the specified culture is not available, returns null. + /// + DateTime? GetUpdateDate(string culture); - /// - /// Gets the available cultures. - /// - /// - /// Cannot contain the invariant culture, which is always available. - /// - IEnumerable AvailableCultures { get; } + /// + /// Gets a value indicating whether the content entity has a property with the supplied alias. + /// + /// + /// Indicates that the content entity has a property with the supplied alias, but + /// not necessarily that the content has a value for that property. Could be missing. + /// + bool HasProperty(string propertyTypeAlias); - /// - /// Gets a value indicating whether a given culture is available. - /// - /// - /// A culture becomes available whenever the content name for this culture is - /// non-null, and it becomes unavailable whenever the content name is null. - /// Returns false for the invariant culture, in order to be consistent - /// with , even though the invariant culture is - /// always available. - /// Does not support the '*' wildcard (returns false). - /// - bool IsCultureAvailable(string culture); + /// + /// Gets the value of a Property + /// + /// Values 'null' and 'empty' are equivalent for culture and segment. + object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false); - /// - /// Gets the date a culture was updated. - /// - /// - /// When is null, returns null. - /// If the specified culture is not available, returns null. - /// - DateTime? GetUpdateDate(string culture); + /// + /// Gets the typed value of a Property + /// + /// Values 'null' and 'empty' are equivalent for culture and segment. + TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false); - /// - /// List of properties, which make up all the data available for this Content object - /// - /// Properties are loaded as part of the Content object graph - IPropertyCollection Properties { get; set; } - - /// - /// Gets a value indicating whether the content entity has a property with the supplied alias. - /// - /// Indicates that the content entity has a property with the supplied alias, but - /// not necessarily that the content has a value for that property. Could be missing. - bool HasProperty(string propertyTypeAlias); - - /// - /// Gets the value of a Property - /// - /// Values 'null' and 'empty' are equivalent for culture and segment. - object? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false); - - /// - /// Gets the typed value of a Property - /// - /// Values 'null' and 'empty' are equivalent for culture and segment. - TValue? GetValue(string propertyTypeAlias, string? culture = null, string? segment = null, bool published = false); - - /// - /// Sets the (edited) value of a Property - /// - /// Values 'null' and 'empty' are equivalent for culture and segment. - void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null); - - } + /// + /// Sets the (edited) value of a Property + /// + /// Values 'null' and 'empty' are equivalent for culture and segment. + void SetValue(string propertyTypeAlias, object? value, string? culture = null, string? segment = null); } diff --git a/src/Umbraco.Core/Models/IContentModel.cs b/src/Umbraco.Core/Models/IContentModel.cs index 8aa8c18306..c7669dfbe4 100644 --- a/src/Umbraco.Core/Models/IContentModel.cs +++ b/src/Umbraco.Core/Models/IContentModel.cs @@ -1,29 +1,34 @@ using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The basic view model returned for front-end Umbraco controllers +/// +/// +/// +/// exists in order to unify all view models in Umbraco, whether it's a normal +/// template view or a partial view macro, or +/// a user's custom model that they have created when doing route hijacking or custom routes. +/// +/// +/// By default all front-end template views inherit from UmbracoViewPage which has a model of +/// but the model returned +/// from the controllers is which in normal circumstances would not work. This works +/// with UmbracoViewPage because it +/// performs model binding between IContentModel and IPublishedContent. This offers a lot of flexibility when +/// rendering views. In some cases if you +/// are route hijacking and returning a custom implementation of and your view is +/// strongly typed to this model, you can still +/// render partial views created in the back office that have the default model of IPublishedContent without having +/// to worry about explicitly passing +/// that model to the view. +/// +/// +public interface IContentModel { /// - /// The basic view model returned for front-end Umbraco controllers + /// Gets the /// - /// - /// - /// exists in order to unify all view models in Umbraco, whether it's a normal template view or a partial view macro, or - /// a user's custom model that they have created when doing route hijacking or custom routes. - /// - /// - /// By default all front-end template views inherit from UmbracoViewPage which has a model of but the model returned - /// from the controllers is which in normal circumstances would not work. This works with UmbracoViewPage because it - /// performs model binding between IContentModel and IPublishedContent. This offers a lot of flexibility when rendering views. In some cases if you - /// are route hijacking and returning a custom implementation of and your view is strongly typed to this model, you can still - /// render partial views created in the back office that have the default model of IPublishedContent without having to worry about explicitly passing - /// that model to the view. - /// - /// - public interface IContentModel - { - /// - /// Gets the - /// - IPublishedContent Content { get; } - } + IPublishedContent Content { get; } } diff --git a/src/Umbraco.Core/Models/IContentType.cs b/src/Umbraco.Core/Models/IContentType.cs index e3fd83fcd3..5d76c49b88 100644 --- a/src/Umbraco.Core/Models/IContentType.cs +++ b/src/Umbraco.Core/Models/IContentType.cs @@ -1,74 +1,71 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a content type that contains a history cleanup policy. +/// +[Obsolete("This will be merged into IContentType in Umbraco 10.")] +public interface IContentTypeWithHistoryCleanup : IContentType { /// - /// Defines a content type that contains a history cleanup policy. + /// Gets or sets the history cleanup configuration. /// - [Obsolete("This will be merged into IContentType in Umbraco 10.")] - public interface IContentTypeWithHistoryCleanup : IContentType - { - /// - /// Gets or sets the history cleanup configuration. - /// - /// The history cleanup configuration. - HistoryCleanup? HistoryCleanup { get; set; } - } + /// The history cleanup configuration. + HistoryCleanup? HistoryCleanup { get; set; } +} + +/// +/// Defines a ContentType, which Content is based on +/// +public interface IContentType : IContentTypeComposition +{ + /// + /// Internal property to store the Id of the default template + /// + int DefaultTemplateId { get; set; } /// - /// Defines a ContentType, which Content is based on + /// Gets the default Template of the ContentType /// - public interface IContentType : IContentTypeComposition - { - /// - /// Internal property to store the Id of the default template - /// - int DefaultTemplateId { get; set; } + ITemplate? DefaultTemplate { get; } - /// - /// Gets the default Template of the ContentType - /// - ITemplate? DefaultTemplate { get; } + /// + /// Gets or Sets a list of Templates which are allowed for the ContentType + /// + IEnumerable? AllowedTemplates { get; set; } - /// - /// Gets or Sets a list of Templates which are allowed for the ContentType - /// - IEnumerable? AllowedTemplates { get; set; } + /// + /// Determines if AllowedTemplates contains templateId + /// + /// The template id to check + /// True if AllowedTemplates contains the templateId else False + bool IsAllowedTemplate(int templateId); - /// - /// Determines if AllowedTemplates contains templateId - /// - /// The template id to check - /// True if AllowedTemplates contains the templateId else False - bool IsAllowedTemplate(int templateId); + /// + /// Determines if AllowedTemplates contains templateId + /// + /// The template alias to check + /// True if AllowedTemplates contains the templateAlias else False + bool IsAllowedTemplate(string templateAlias); - /// - /// Determines if AllowedTemplates contains templateId - /// - /// The template alias to check - /// True if AllowedTemplates contains the templateAlias else False - bool IsAllowedTemplate(string templateAlias); + /// + /// Sets the default template for the ContentType + /// + /// Default + void SetDefaultTemplate(ITemplate? template); - /// - /// Sets the default template for the ContentType - /// - /// Default - void SetDefaultTemplate(ITemplate? template); + /// + /// Removes a template from the list of allowed templates + /// + /// to remove + /// True if template was removed, otherwise False + bool RemoveTemplate(ITemplate template); - /// - /// Removes a template from the list of allowed templates - /// - /// to remove - /// True if template was removed, otherwise False - bool RemoveTemplate(ITemplate template); - - /// - /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset - /// - /// - /// - IContentType DeepCloneWithResetIdentities(string newAlias); - } + /// + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset + /// + /// + /// + IContentType DeepCloneWithResetIdentities(string newAlias); } diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index eb3d4489d4..adcb4074f9 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -1,175 +1,182 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines the base for a ContentType with properties that +/// are shared between ContentTypes and MediaTypes. +/// +public interface IContentTypeBase : IUmbracoEntity, IRememberBeingDirty { /// - /// Defines the base for a ContentType with properties that - /// are shared between ContentTypes and MediaTypes. + /// Gets or Sets the Alias of the ContentType /// - public interface IContentTypeBase : IUmbracoEntity, IRememberBeingDirty - { - /// - /// Gets or Sets the Alias of the ContentType - /// - string Alias { get; set; } + string Alias { get; set; } - /// - /// Gets or Sets the Description for the ContentType - /// - string? Description { get; set; } + /// + /// Gets or Sets the Description for the ContentType + /// + string? Description { get; set; } - /// - /// Gets or sets the icon for the content type. The value is a CSS class name representing - /// the icon (eg. icon-home) along with an optional CSS class name representing the - /// color (eg. icon-blue). Put together, the value for this scenario would be - /// icon-home color-blue. - /// - /// If a class name for the color isn't specified, the icon color will default to black. - /// - string? Icon { get; set; } + /// + /// Gets or sets the icon for the content type. The value is a CSS class name representing + /// the icon (eg. icon-home) along with an optional CSS class name representing the + /// color (eg. icon-blue). Put together, the value for this scenario would be + /// icon-home color-blue. + /// If a class name for the color isn't specified, the icon color will default to black. + /// + string? Icon { get; set; } - /// - /// Gets or Sets the Thumbnail for the ContentType - /// - string? Thumbnail { get; set; } + /// + /// Gets or Sets the Thumbnail for the ContentType + /// + string? Thumbnail { get; set; } - /// - /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root - /// - bool AllowedAsRoot { get; set; } + /// + /// Gets or Sets a boolean indicating whether this ContentType is allowed at the root + /// + bool AllowedAsRoot { get; set; } - /// - /// Gets or Sets a boolean indicating whether this ContentType is a Container - /// - /// - /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. - /// - bool IsContainer { get; set; } + /// + /// Gets or Sets a boolean indicating whether this ContentType is a Container + /// + /// + /// ContentType Containers doesn't show children in the tree, but rather in grid-type view. + /// + bool IsContainer { get; set; } - /// - /// Gets or sets a value indicating whether this content type is for an element. - /// - /// - /// By default a content type is for a true media, member or document, but - /// it can also be for an element, ie a subset that can for instance be used in - /// nested content. - /// - bool IsElement { get; set; } + /// + /// Gets or sets a value indicating whether this content type is for an element. + /// + /// + /// + /// By default a content type is for a true media, member or document, but + /// it can also be for an element, ie a subset that can for instance be used in + /// nested content. + /// + /// + bool IsElement { get; set; } - /// - /// Gets or sets the content variation of the content type. - /// - ContentVariation Variations { get; set; } + /// + /// Gets or sets the content variation of the content type. + /// + ContentVariation Variations { get; set; } - /// - /// Validates that a combination of culture and segment is valid for the content type. - /// - /// The culture. - /// The segment. - /// A value indicating whether wildcard are supported. - /// True if the combination is valid; otherwise false. - /// - /// The combination must match the content type variation exactly. For instance, if the content type varies by culture, - /// then an invariant culture would be invalid. - /// - bool SupportsVariation(string culture, string segment, bool wildcards = false); + /// + /// Gets or Sets a list of integer Ids of the ContentTypes allowed under the ContentType + /// + IEnumerable? AllowedContentTypes { get; set; } - /// - /// Validates that a combination of culture and segment is valid for the content type properties. - /// - /// The culture. - /// The segment. - /// A value indicating whether wildcard are supported. - /// True if the combination is valid; otherwise false. - /// - /// The combination must be valid for properties of the content type. For instance, if the content type varies by culture, - /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content type is invariant, - /// then a variant culture is invalid, because no property could possibly vary by culture. - /// - bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false); + /// + /// Gets or sets the local property groups. + /// + PropertyGroupCollection PropertyGroups { get; set; } - /// - /// Gets or Sets a list of integer Ids of the ContentTypes allowed under the ContentType - /// - IEnumerable? AllowedContentTypes { get; set; } + /// + /// Gets all local property types all local property groups or ungrouped. + /// + IEnumerable PropertyTypes { get; } - /// - /// Gets or sets the local property groups. - /// - PropertyGroupCollection PropertyGroups { get; set; } + /// + /// Gets or sets the local property types that do not belong to a group. + /// + IEnumerable NoGroupPropertyTypes { get; set; } - /// - /// Gets all local property types all local property groups or ungrouped. - /// - IEnumerable PropertyTypes { get; } + /// + /// Validates that a combination of culture and segment is valid for the content type. + /// + /// The culture. + /// The segment. + /// A value indicating whether wildcard are supported. + /// True if the combination is valid; otherwise false. + /// + /// + /// The combination must match the content type variation exactly. For instance, if the content type varies by + /// culture, + /// then an invariant culture would be invalid. + /// + /// + bool SupportsVariation(string culture, string segment, bool wildcards = false); - /// - /// Gets or sets the local property types that do not belong to a group. - /// - IEnumerable NoGroupPropertyTypes { get; set; } + /// + /// Validates that a combination of culture and segment is valid for the content type properties. + /// + /// The culture. + /// The segment. + /// A value indicating whether wildcard are supported. + /// True if the combination is valid; otherwise false. + /// + /// + /// The combination must be valid for properties of the content type. For instance, if the content type varies by + /// culture, + /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content + /// type is invariant, + /// then a variant culture is invalid, because no property could possibly vary by culture. + /// + /// + bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false); - /// - /// Removes a PropertyType from the current ContentType - /// - /// Alias of the to remove - void RemovePropertyType(string alias); + /// + /// Removes a PropertyType from the current ContentType + /// + /// Alias of the to remove + void RemovePropertyType(string alias); - /// - /// Removes a property group from the current content type. - /// - /// Alias of the to remove - void RemovePropertyGroup(string alias); + /// + /// Removes a property group from the current content type. + /// + /// Alias of the to remove + void RemovePropertyGroup(string alias); - /// - /// Checks whether a PropertyType with a given alias already exists - /// - /// Alias of the PropertyType - /// Returns True if a PropertyType with the passed in alias exists, otherwise False - bool PropertyTypeExists(string? alias); + /// + /// Checks whether a PropertyType with a given alias already exists + /// + /// Alias of the PropertyType + /// Returns True if a PropertyType with the passed in alias exists, otherwise False + bool PropertyTypeExists(string? alias); - /// - /// Adds the property type to the specified property group (creates a new group if not found and a name is specified). - /// - /// The property type to add. - /// The alias of the property group to add the property type to. - /// The name of the property group to create when not found. - /// - /// Returns true if the property type was added; otherwise, false. - /// - bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); + /// + /// Adds the property type to the specified property group (creates a new group if not found and a name is specified). + /// + /// The property type to add. + /// The alias of the property group to add the property type to. + /// The name of the property group to create when not found. + /// + /// Returns true if the property type was added; otherwise, false. + /// + bool AddPropertyType(IPropertyType propertyType, string propertyGroupAlias, string? propertyGroupName = null); - /// - /// Adds a PropertyType, which does not belong to a PropertyGroup. - /// - /// to add - /// Returns True if PropertyType was added, otherwise False - bool AddPropertyType(IPropertyType propertyType); + /// + /// Adds a PropertyType, which does not belong to a PropertyGroup. + /// + /// to add + /// Returns True if PropertyType was added, otherwise False + bool AddPropertyType(IPropertyType propertyType); - /// - /// Adds a property group with the specified and . - /// - /// The alias. - /// Name of the group. - /// - /// Returns true if a property group with specified was added; otherwise, false. - /// - /// - /// This method will also check if a group already exists with the same alias. - /// - bool AddPropertyGroup(string alias, string name); + /// + /// Adds a property group with the specified and . + /// + /// The alias. + /// Name of the group. + /// + /// Returns true if a property group with specified was added; otherwise, false + /// . + /// + /// + /// This method will also check if a group already exists with the same alias. + /// + bool AddPropertyGroup(string alias, string name); - /// - /// Moves a PropertyType to a specified PropertyGroup - /// - /// Alias of the PropertyType to move - /// Alias of the PropertyGroup to move the PropertyType to - /// - bool MovePropertyType(string propertyTypeAlias, string propertyGroupAlias); + /// + /// Moves a PropertyType to a specified PropertyGroup + /// + /// Alias of the PropertyType to move + /// Alias of the PropertyGroup to move the PropertyType to + /// + bool MovePropertyType(string propertyTypeAlias, string propertyGroupAlias); - /// - /// Gets an corresponding to this content type. - /// - ISimpleContentType ToSimple(); - } + /// + /// Gets an corresponding to this content type. + /// + ISimpleContentType ToSimple(); } diff --git a/src/Umbraco.Core/Models/IContentTypeComposition.cs b/src/Umbraco.Core/Models/IContentTypeComposition.cs index de3e8fb416..650328548e 100644 --- a/src/Umbraco.Core/Models/IContentTypeComposition.cs +++ b/src/Umbraco.Core/Models/IContentTypeComposition.cs @@ -1,72 +1,69 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Defines the Composition of a ContentType +/// +public interface IContentTypeComposition : IContentTypeBase { /// - /// Defines the Composition of a ContentType + /// Gets or sets the content types that compose this content type. /// - public interface IContentTypeComposition : IContentTypeBase - { - /// - /// Gets or sets the content types that compose this content type. - /// - // TODO: we should be storing key references, not the object else we are caching way too much - IEnumerable ContentTypeComposition { get; set; } + // TODO: we should be storing key references, not the object else we are caching way too much + IEnumerable ContentTypeComposition { get; set; } - /// - /// Gets the property groups for the entire composition. - /// - IEnumerable CompositionPropertyGroups { get; } + /// + /// Gets the property groups for the entire composition. + /// + IEnumerable CompositionPropertyGroups { get; } - /// - /// Gets the property types for the entire composition. - /// - IEnumerable CompositionPropertyTypes { get; } + /// + /// Gets the property types for the entire composition. + /// + IEnumerable CompositionPropertyTypes { get; } - /// - /// Adds a new ContentType to the list of composite ContentTypes - /// - /// to add - /// True if ContentType was added, otherwise returns False - bool AddContentType(IContentTypeComposition? contentType); + /// + /// Returns a list of content type ids that have been removed from this instance's composition + /// + IEnumerable RemovedContentTypes { get; } - /// - /// Removes a ContentType with the supplied alias from the list of composite ContentTypes - /// - /// Alias of a - /// True if ContentType was removed, otherwise returns False - bool RemoveContentType(string alias); + /// + /// Adds a new ContentType to the list of composite ContentTypes + /// + /// to add + /// True if ContentType was added, otherwise returns False + bool AddContentType(IContentTypeComposition? contentType); - /// - /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes - /// - /// Alias of a - /// True if ContentType with alias exists, otherwise returns False - bool ContentTypeCompositionExists(string alias); + /// + /// Removes a ContentType with the supplied alias from the list of composite ContentTypes + /// + /// Alias of a + /// True if ContentType was removed, otherwise returns False + bool RemoveContentType(string alias); - /// - /// Gets a list of ContentType aliases from the current composition - /// - /// An enumerable list of string aliases - IEnumerable CompositionAliases(); + /// + /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes + /// + /// Alias of a + /// True if ContentType with alias exists, otherwise returns False + bool ContentTypeCompositionExists(string alias); - /// - /// Gets a list of ContentType Ids from the current composition - /// - /// An enumerable list of integer ids - IEnumerable CompositionIds(); + /// + /// Gets a list of ContentType aliases from the current composition + /// + /// An enumerable list of string aliases + IEnumerable CompositionAliases(); - /// - /// Returns a list of content type ids that have been removed from this instance's composition - /// - IEnumerable RemovedContentTypes { get; } + /// + /// Gets a list of ContentType Ids from the current composition + /// + /// An enumerable list of integer ids + IEnumerable CompositionIds(); - /// - /// Gets the property types obtained via composition. - /// - /// - /// Gets them raw, ie with their original variation. - /// - IEnumerable GetOriginalComposedPropertyTypes(); - } + /// + /// Gets the property types obtained via composition. + /// + /// + /// Gets them raw, ie with their original variation. + /// + IEnumerable GetOriginalComposedPropertyTypes(); } diff --git a/src/Umbraco.Core/Models/IDataType.cs b/src/Umbraco.Core/Models/IDataType.cs index 2fdc67dfcc..6f0002c779 100644 --- a/src/Umbraco.Core/Models/IDataType.cs +++ b/src/Umbraco.Core/Models/IDataType.cs @@ -1,38 +1,41 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a data type. +/// +public interface IDataType : IUmbracoEntity, IRememberBeingDirty { /// - /// Represents a data type. + /// Gets or sets the property editor. /// - public interface IDataType : IUmbracoEntity, IRememberBeingDirty - { - /// - /// Gets or sets the property editor. - /// - IDataEditor? Editor { get; set; } + IDataEditor? Editor { get; set; } - /// - /// Gets the property editor alias. - /// - string EditorAlias { get; } + /// + /// Gets the property editor alias. + /// + string EditorAlias { get; } - /// - /// Gets or sets the database type for the data type values. - /// - /// In most cases this is imposed by the property editor, but some editors - /// may support storing different types. - ValueStorageType DatabaseType { get; set; } + /// + /// Gets or sets the database type for the data type values. + /// + /// + /// In most cases this is imposed by the property editor, but some editors + /// may support storing different types. + /// + ValueStorageType DatabaseType { get; set; } - /// - /// Gets or sets the configuration object. - /// - /// - /// The configuration object is serialized to Json and stored into the database. - /// The serialized Json is deserialized by the property editor, which by default should - /// return a Dictionary{string, object} but could return a typed object e.g. MyEditor.Configuration. - /// - object? Configuration { get; set; } - } + /// + /// Gets or sets the configuration object. + /// + /// + /// The configuration object is serialized to Json and stored into the database. + /// + /// The serialized Json is deserialized by the property editor, which by default should + /// return a Dictionary{string, object} but could return a typed object e.g. MyEditor.Configuration. + /// + /// + object? Configuration { get; set; } } diff --git a/src/Umbraco.Core/Models/IDataValueEditor.cs b/src/Umbraco.Core/Models/IDataValueEditor.cs index 8d4841a114..73b700e411 100644 --- a/src/Umbraco.Core/Models/IDataValueEditor.cs +++ b/src/Umbraco.Core/Models/IDataValueEditor.cs @@ -1,85 +1,82 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Xml.Linq; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents an editor for editing data values. +/// +/// This is the base interface for parameter and property value editors. +public interface IDataValueEditor { + /// + /// Gets the editor view. + /// + string? View { get; } /// - /// Represents an editor for editing data values. + /// Gets the type of the value. /// - /// This is the base interface for parameter and property value editors. - public interface IDataValueEditor - { - /// - /// Gets the editor view. - /// - string? View { get; } + /// The value has to be a valid value. + string ValueType { get; set; } - /// - /// Gets the type of the value. - /// - /// The value has to be a valid value. - string ValueType { get; set; } + /// + /// Gets a value indicating whether the edited value is read-only. + /// + bool IsReadOnly { get; } - /// - /// Gets a value indicating whether the edited value is read-only. - /// - bool IsReadOnly { get; } + /// + /// Gets a value indicating whether to display the associated label. + /// + bool HideLabel { get; } - /// - /// Gets a value indicating whether to display the associated label. - /// - bool HideLabel { get; } + /// + /// Gets the validators to use to validate the edited value. + /// + /// + /// Use this property to add validators, not to validate. Use instead. + /// TODO: replace with AddValidator? WithValidator? + /// + List Validators { get; } - /// - /// Validates a property value. - /// - /// The property value. - /// A value indicating whether the property value is required. - /// A specific format (regex) that the property value must respect. - IEnumerable Validate(object? value, bool required, string? format); + /// + /// Validates a property value. + /// + /// The property value. + /// A value indicating whether the property value is required. + /// A specific format (regex) that the property value must respect. + IEnumerable Validate(object? value, bool required, string? format); - /// - /// Gets the validators to use to validate the edited value. - /// - /// - /// Use this property to add validators, not to validate. Use instead. - /// TODO: replace with AddValidator? WithValidator? - /// - List Validators { get; } + /// + /// Converts a value posted by the editor to a property value. + /// + object? FromEditor(ContentPropertyData editorValue, object? currentValue); - /// - /// Converts a value posted by the editor to a property value. - /// - object? FromEditor(ContentPropertyData editorValue, object? currentValue); + /// + /// Converts a property value to a value for the editor. + /// + object? ToEditor(IProperty property, string? culture = null, string? segment = null); - /// - /// Converts a property value to a value for the editor. - /// - object? ToEditor(IProperty property, string? culture = null, string? segment = null); + // TODO: / deal with this when unplugging the xml cache + // why property vs propertyType? services should be injected! etc... - // TODO: / deal with this when unplugging the xml cache - // why property vs propertyType? services should be injected! etc... + /// + /// Used for serializing an item for packaging + /// + /// + /// + /// + IEnumerable ConvertDbToXml(IProperty property, bool published); - /// - /// Used for serializing an item for packaging - /// - /// - /// - /// - IEnumerable ConvertDbToXml(IProperty property, bool published); + /// + /// Used for serializing an item for packaging + /// + /// + /// + /// + XNode ConvertDbToXml(IPropertyType propertyType, object value); - /// - /// Used for serializing an item for packaging - /// - /// - /// - /// - XNode ConvertDbToXml(IPropertyType propertyType, object value); - - string ConvertDbToString(IPropertyType propertyType, object? value); - } + string ConvertDbToString(IPropertyType propertyType, object? value); } diff --git a/src/Umbraco.Core/Models/IDeepCloneable.cs b/src/Umbraco.Core/Models/IDeepCloneable.cs index a7568b7e81..171c2a1f4e 100644 --- a/src/Umbraco.Core/Models/IDeepCloneable.cs +++ b/src/Umbraco.Core/Models/IDeepCloneable.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Provides a mean to deep-clone an object. +/// +public interface IDeepCloneable { - /// - /// Provides a mean to deep-clone an object. - /// - public interface IDeepCloneable - { - object DeepClone(); - } + object DeepClone(); } diff --git a/src/Umbraco.Core/Models/IDictionaryItem.cs b/src/Umbraco.Core/Models/IDictionaryItem.cs index f299ce2ac5..e47502199b 100644 --- a/src/Umbraco.Core/Models/IDictionaryItem.cs +++ b/src/Umbraco.Core/Models/IDictionaryItem.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IDictionaryItem : IEntity, IRememberBeingDirty { - public interface IDictionaryItem : IEntity, IRememberBeingDirty - { - /// - /// Gets or Sets the Parent Id of the Dictionary Item - /// - [DataMember] - Guid? ParentId { get; set; } + /// + /// Gets or Sets the Parent Id of the Dictionary Item + /// + [DataMember] + Guid? ParentId { get; set; } - /// - /// Gets or sets the Key for the Dictionary Item - /// - [DataMember] - string ItemKey { get; set; } + /// + /// Gets or sets the Key for the Dictionary Item + /// + [DataMember] + string ItemKey { get; set; } - /// - /// Gets or sets a list of translations for the Dictionary Item - /// - [DataMember] - IEnumerable Translations { get; set; } - } + /// + /// Gets or sets a list of translations for the Dictionary Item + /// + [DataMember] + IEnumerable Translations { get; set; } } diff --git a/src/Umbraco.Core/Models/IDictionaryTranslation.cs b/src/Umbraco.Core/Models/IDictionaryTranslation.cs index 445bafd4ba..37579151bc 100644 --- a/src/Umbraco.Core/Models/IDictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/IDictionaryTranslation.cs @@ -1,22 +1,21 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IDictionaryTranslation : IEntity, IRememberBeingDirty { - public interface IDictionaryTranslation : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the for the translation - /// - [DataMember] - ILanguage? Language { get; set; } + /// + /// Gets or sets the for the translation + /// + [DataMember] + ILanguage? Language { get; set; } - int LanguageId { get; } + int LanguageId { get; } - /// - /// Gets or sets the translated text - /// - [DataMember] - string Value { get; set; } - } + /// + /// Gets or sets the translated text + /// + [DataMember] + string Value { get; set; } } diff --git a/src/Umbraco.Core/Models/IDomain.cs b/src/Umbraco.Core/Models/IDomain.cs index f9d90dd9eb..2d4845c9a6 100644 --- a/src/Umbraco.Core/Models/IDomain.cs +++ b/src/Umbraco.Core/Models/IDomain.cs @@ -1,17 +1,19 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IDomain : IEntity, IRememberBeingDirty { - public interface IDomain : IEntity, IRememberBeingDirty - { - int? LanguageId { get; set; } - string DomainName { get; set; } - int? RootContentId { get; set; } - bool IsWildcard { get; } + int? LanguageId { get; set; } - /// - /// Readonly value of the language ISO code for the domain - /// - string? LanguageIsoCode { get; } - } + string DomainName { get; set; } + + int? RootContentId { get; set; } + + bool IsWildcard { get; } + + /// + /// Readonly value of the language ISO code for the domain + /// + string? LanguageIsoCode { get; } } diff --git a/src/Umbraco.Core/Models/IFile.cs b/src/Umbraco.Core/Models/IFile.cs index 216d45d277..ed52997c84 100644 --- a/src/Umbraco.Core/Models/IFile.cs +++ b/src/Umbraco.Core/Models/IFile.cs @@ -1,47 +1,45 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a File +/// +/// Used for Scripts, Stylesheets and Templates +public interface IFile : IEntity, IRememberBeingDirty { /// - /// Defines a File + /// Gets the Name of the File including extension /// - /// Used for Scripts, Stylesheets and Templates - public interface IFile : IEntity, IRememberBeingDirty - { - /// - /// Gets the Name of the File including extension - /// - string? Name { get; } + string? Name { get; } - /// - /// Gets the Alias of the File, which is the name without the extension - /// - string Alias { get; } + /// + /// Gets the Alias of the File, which is the name without the extension + /// + string Alias { get; } - /// - /// Gets or sets the Path to the File from the root of the file's associated IFileSystem - /// - string Path { get; set; } + /// + /// Gets or sets the Path to the File from the root of the file's associated IFileSystem + /// + string Path { get; set; } - /// - /// Gets the original path of the file - /// - string OriginalPath { get; } + /// + /// Gets the original path of the file + /// + string OriginalPath { get; } - /// - /// Called to re-set the OriginalPath to the Path - /// - void ResetOriginalPath(); + /// + /// Gets or sets the Content of a File + /// + string? Content { get; set; } - /// - /// Gets or sets the Content of a File - /// - string? Content { get; set; } + /// + /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) + /// + string? VirtualPath { get; set; } - /// - /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) - /// - string? VirtualPath { get; set; } - - } + /// + /// Called to re-set the OriginalPath to the Path + /// + void ResetOriginalPath(); } diff --git a/src/Umbraco.Core/Models/IKeyValue.cs b/src/Umbraco.Core/Models/IKeyValue.cs index 2a8c6528bf..b893aabf35 100644 --- a/src/Umbraco.Core/Models/IKeyValue.cs +++ b/src/Umbraco.Core/Models/IKeyValue.cs @@ -1,11 +1,10 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IKeyValue : IEntity { - public interface IKeyValue : IEntity - { - string Identifier { get; set; } + string Identifier { get; set; } - string? Value { get; set; } - } + string? Value { get; set; } } diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index de5170cff6..5f48bc363e 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -2,56 +2,59 @@ using System.Globalization; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a language. +/// +public interface ILanguage : IEntity, IRememberBeingDirty { /// - /// Represents a language. + /// Gets or sets the ISO code of the language. /// - public interface ILanguage : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the ISO code of the language. - /// - [DataMember] - string IsoCode { get; set; } + [DataMember] + string IsoCode { get; set; } - /// - /// Gets or sets the culture name of the language. - /// - [DataMember] - string CultureName { get; set; } + /// + /// Gets or sets the culture name of the language. + /// + [DataMember] + string CultureName { get; set; } - /// - /// Gets the object for the language. - /// - [IgnoreDataMember] - CultureInfo? CultureInfo { get; } + /// + /// Gets the object for the language. + /// + [IgnoreDataMember] + CultureInfo? CultureInfo { get; } - /// - /// Gets or sets a value indicating whether the language is the default language. - /// - [DataMember] - bool IsDefault { get; set; } + /// + /// Gets or sets a value indicating whether the language is the default language. + /// + [DataMember] + bool IsDefault { get; set; } - /// - /// Gets or sets a value indicating whether the language is mandatory. - /// - /// - /// When a language is mandatory, a multi-lingual document cannot be published - /// without that language being published, and unpublishing that language unpublishes - /// the entire document. - /// - [DataMember] - bool IsMandatory { get; set; } + /// + /// Gets or sets a value indicating whether the language is mandatory. + /// + /// + /// + /// When a language is mandatory, a multi-lingual document cannot be published + /// without that language being published, and unpublishing that language unpublishes + /// the entire document. + /// + /// + [DataMember] + bool IsMandatory { get; set; } - /// - /// Gets or sets the identifier of a fallback language. - /// - /// - /// The fallback language can be used in multi-lingual scenarios, to help - /// define fallback strategies when a value does not exist for a requested language. - /// - [DataMember] - int? FallbackLanguageId { get; set; } - } + /// + /// Gets or sets the identifier of a fallback language. + /// + /// + /// + /// The fallback language can be used in multi-lingual scenarios, to help + /// define fallback strategies when a value does not exist for a requested language. + /// + /// + [DataMember] + int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Core/Models/ILogViewerQuery.cs b/src/Umbraco.Core/Models/ILogViewerQuery.cs index 59a567a635..372fddc3d0 100644 --- a/src/Umbraco.Core/Models/ILogViewerQuery.cs +++ b/src/Umbraco.Core/Models/ILogViewerQuery.cs @@ -1,10 +1,10 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface ILogViewerQuery : IEntity { - public interface ILogViewerQuery : IEntity - { - string? Name { get; set; } - string? Query { get; set; } - } + string? Name { get; set; } + + string? Query { get; set; } } diff --git a/src/Umbraco.Core/Models/IMacro.cs b/src/Umbraco.Core/Models/IMacro.cs index e8102b7768..bc979804a7 100644 --- a/src/Umbraco.Core/Models/IMacro.cs +++ b/src/Umbraco.Core/Models/IMacro.cs @@ -1,65 +1,64 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a Macro +/// +public interface IMacro : IEntity, IRememberBeingDirty { /// - /// Defines a Macro + /// Gets or sets the alias of the Macro /// - public interface IMacro : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the alias of the Macro - /// - [DataMember] - string Alias { get; set; } + [DataMember] + string Alias { get; set; } - /// - /// Gets or sets the name of the Macro - /// - [DataMember] - string? Name { get; set; } + /// + /// Gets or sets the name of the Macro + /// + [DataMember] + string? Name { get; set; } - /// - /// Gets or sets a boolean indicating whether the Macro can be used in an Editor - /// - [DataMember] - bool UseInEditor { get; set; } + /// + /// Gets or sets a boolean indicating whether the Macro can be used in an Editor + /// + [DataMember] + bool UseInEditor { get; set; } - /// - /// Gets or sets the Cache Duration for the Macro - /// - [DataMember] - int CacheDuration { get; set; } + /// + /// Gets or sets the Cache Duration for the Macro + /// + [DataMember] + int CacheDuration { get; set; } - /// - /// Gets or sets a boolean indicating whether the Macro should be Cached by Page - /// - [DataMember] - bool CacheByPage { get; set; } + /// + /// Gets or sets a boolean indicating whether the Macro should be Cached by Page + /// + [DataMember] + bool CacheByPage { get; set; } - /// - /// Gets or sets a boolean indicating whether the Macro should be Cached Personally - /// - [DataMember] - bool CacheByMember { get; set; } + /// + /// Gets or sets a boolean indicating whether the Macro should be Cached Personally + /// + [DataMember] + bool CacheByMember { get; set; } - /// - /// Gets or sets a boolean indicating whether the Macro should be rendered in an Editor - /// - [DataMember] - bool DontRender { get; set; } + /// + /// Gets or sets a boolean indicating whether the Macro should be rendered in an Editor + /// + [DataMember] + bool DontRender { get; set; } - /// - /// Gets or set the path to the macro source to render - /// - [DataMember] - string MacroSource { get; set; } + /// + /// Gets or set the path to the macro source to render + /// + [DataMember] + string MacroSource { get; set; } - /// - /// Gets or sets a list of Macro Properties - /// - [DataMember] - MacroPropertyCollection Properties { get; } - } + /// + /// Gets or sets a list of Macro Properties + /// + [DataMember] + MacroPropertyCollection Properties { get; } } diff --git a/src/Umbraco.Core/Models/IMacroProperty.cs b/src/Umbraco.Core/Models/IMacroProperty.cs index d3d589a31e..e1b27b6483 100644 --- a/src/Umbraco.Core/Models/IMacroProperty.cs +++ b/src/Umbraco.Core/Models/IMacroProperty.cs @@ -1,42 +1,40 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a Property for a Macro +/// +public interface IMacroProperty : IValueObject, IDeepCloneable, IRememberBeingDirty { + [DataMember] + int Id { get; set; } + + [DataMember] + Guid Key { get; set; } + /// - /// Defines a Property for a Macro + /// Gets or sets the Alias of the Property /// - public interface IMacroProperty : IValueObject, IDeepCloneable, IRememberBeingDirty - { - [DataMember] - int Id { get; set; } + [DataMember] + string Alias { get; set; } - [DataMember] - Guid Key { get; set; } + /// + /// Gets or sets the Name of the Property + /// + [DataMember] + string? Name { get; set; } - /// - /// Gets or sets the Alias of the Property - /// - [DataMember] - string Alias { get; set; } + /// + /// Gets or sets the Sort Order of the Property + /// + [DataMember] + int SortOrder { get; set; } - /// - /// Gets or sets the Name of the Property - /// - [DataMember] - string? Name { get; set; } - - /// - /// Gets or sets the Sort Order of the Property - /// - [DataMember] - int SortOrder { get; set; } - - /// - /// Gets or sets the parameter editor alias - /// - [DataMember] - string EditorAlias { get; set; } - } + /// + /// Gets or sets the parameter editor alias + /// + [DataMember] + string EditorAlias { get; set; } } diff --git a/src/Umbraco.Core/Models/IMedia.cs b/src/Umbraco.Core/Models/IMedia.cs index cbb80fdd59..08f206f664 100644 --- a/src/Umbraco.Core/Models/IMedia.cs +++ b/src/Umbraco.Core/Models/IMedia.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IMedia : IContentBase { - public interface IMedia : IContentBase - { } } diff --git a/src/Umbraco.Core/Models/IMediaType.cs b/src/Umbraco.Core/Models/IMediaType.cs index 13655f0f55..0be980ae62 100644 --- a/src/Umbraco.Core/Models/IMediaType.cs +++ b/src/Umbraco.Core/Models/IMediaType.cs @@ -1,16 +1,14 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a ContentType, which Media is based on +/// +public interface IMediaType : IContentTypeComposition { /// - /// Defines a ContentType, which Media is based on + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset /// - public interface IMediaType : IContentTypeComposition - { - - /// - /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset - /// - /// - /// - IMediaType DeepCloneWithResetIdentities(string newAlias); - } + /// + /// + IMediaType DeepCloneWithResetIdentities(string newAlias); } diff --git a/src/Umbraco.Core/Models/IMediaUrlGenerator.cs b/src/Umbraco.Core/Models/IMediaUrlGenerator.cs index 4565117dfd..a0af9dcc0e 100644 --- a/src/Umbraco.Core/Models/IMediaUrlGenerator.cs +++ b/src/Umbraco.Core/Models/IMediaUrlGenerator.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Used to generate paths to media items for a specified property editor alias +/// +public interface IMediaUrlGenerator { /// - /// Used to generate paths to media items for a specified property editor alias + /// Tries to get a media path for a given property editor alias /// - public interface IMediaUrlGenerator - { - /// - /// Tries to get a media path for a given property editor alias - /// - /// The property editor alias - /// The value of the property - /// - /// True if a media path was returned - /// - bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath); - } + /// The property editor alias + /// The value of the property + /// + /// True if a media path was returned + /// + bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath); } diff --git a/src/Umbraco.Core/Models/IMember.cs b/src/Umbraco.Core/Models/IMember.cs index 0dba1d8049..6085b84f01 100644 --- a/src/Umbraco.Core/Models/IMember.cs +++ b/src/Umbraco.Core/Models/IMember.cs @@ -1,64 +1,67 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models -{ - public interface IMember : IContentBase, IMembershipUser, IHaveAdditionalData - { - /// - /// String alias of the default ContentType - /// - string ContentTypeAlias { get; } +namespace Umbraco.Cms.Core.Models; - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - string? LongStringPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - string? ShortStringPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - int IntegerPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - bool BoolPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - DateTime DateTimePropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - string? PropertyTypeAlias { get; set; } - } +public interface IMember : IContentBase, IMembershipUser, IHaveAdditionalData +{ + /// + /// String alias of the default ContentType + /// + string ContentTypeAlias { get; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + string? LongStringPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + string? ShortStringPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + int IntegerPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + bool BoolPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + DateTime DateTimePropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + string? PropertyTypeAlias { get; set; } } diff --git a/src/Umbraco.Core/Models/IMemberGroup.cs b/src/Umbraco.Core/Models/IMemberGroup.cs index 80d4a16ad6..904d60cf8c 100644 --- a/src/Umbraco.Core/Models/IMemberGroup.cs +++ b/src/Umbraco.Core/Models/IMemberGroup.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a member type +/// +public interface IMemberGroup : IEntity, IRememberBeingDirty, IHaveAdditionalData { /// - /// Represents a member type + /// The name of the member group /// - public interface IMemberGroup : IEntity, IRememberBeingDirty, IHaveAdditionalData - { - /// - /// The name of the member group - /// - string? Name { get; set; } + string? Name { get; set; } - /// - /// Profile of the user who created this Entity - /// - int CreatorId { get; set; } - } + /// + /// Profile of the user who created this Entity + /// + int CreatorId { get; set; } } diff --git a/src/Umbraco.Core/Models/IMemberType.cs b/src/Umbraco.Core/Models/IMemberType.cs index 324601efde..993e956df9 100644 --- a/src/Umbraco.Core/Models/IMemberType.cs +++ b/src/Umbraco.Core/Models/IMemberType.cs @@ -1,50 +1,49 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines a MemberType, which Member is based on +/// +public interface IMemberType : IContentTypeComposition { /// - /// Defines a MemberType, which Member is based on + /// Gets a boolean indicating whether a Property is editable by the Member. /// - public interface IMemberType : IContentTypeComposition - { - /// - /// Gets a boolean indicating whether a Property is editable by the Member. - /// - /// PropertyType Alias of the Property to check - /// - bool MemberCanEditProperty(string? propertyTypeAlias); + /// PropertyType Alias of the Property to check + /// + bool MemberCanEditProperty(string? propertyTypeAlias); - /// - /// Gets a boolean indicating whether a Property is visible on the Members profile. - /// - /// PropertyType Alias of the Property to check - /// - bool MemberCanViewProperty(string propertyTypeAlias); + /// + /// Gets a boolean indicating whether a Property is visible on the Members profile. + /// + /// PropertyType Alias of the Property to check + /// + bool MemberCanViewProperty(string propertyTypeAlias); - /// - /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile. - /// - /// PropertyType Alias of the Property to check - /// - bool IsSensitiveProperty(string propertyTypeAlias); + /// + /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile. + /// + /// PropertyType Alias of the Property to check + /// + bool IsSensitiveProperty(string propertyTypeAlias); - /// - /// Sets a boolean indicating whether a Property is editable by the Member. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - void SetMemberCanEditProperty(string propertyTypeAlias, bool value); + /// + /// Sets a boolean indicating whether a Property is editable by the Member. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + void SetMemberCanEditProperty(string propertyTypeAlias, bool value); - /// - /// Sets a boolean indicating whether a Property is visible on the Members profile. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - void SetMemberCanViewProperty(string propertyTypeAlias, bool value); + /// + /// Sets a boolean indicating whether a Property is visible on the Members profile. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + void SetMemberCanViewProperty(string propertyTypeAlias, bool value); - /// - /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - void SetIsSensitiveProperty(string propertyTypeAlias, bool value); - } + /// + /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + void SetIsSensitiveProperty(string propertyTypeAlias, bool value); } diff --git a/src/Umbraco.Core/Models/IMigrationEntry.cs b/src/Umbraco.Core/Models/IMigrationEntry.cs index a3d11e851a..392eb17097 100644 --- a/src/Umbraco.Core/Models/IMigrationEntry.cs +++ b/src/Umbraco.Core/Models/IMigrationEntry.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IMigrationEntry : IEntity, IRememberBeingDirty { - public interface IMigrationEntry : IEntity, IRememberBeingDirty - { - string? MigrationName { get; set; } - SemVersion? Version { get; set; } - } + string? MigrationName { get; set; } + + SemVersion? Version { get; set; } } diff --git a/src/Umbraco.Core/Models/IPartialView.cs b/src/Umbraco.Core/Models/IPartialView.cs index c45b76534d..a19cc65c7a 100644 --- a/src/Umbraco.Core/Models/IPartialView.cs +++ b/src/Umbraco.Core/Models/IPartialView.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IPartialView : IFile { - public interface IPartialView : IFile - { - PartialViewType ViewType { get; } - } + PartialViewType ViewType { get; } } diff --git a/src/Umbraco.Core/Models/IProperty.cs b/src/Umbraco.Core/Models/IProperty.cs index 9ed37c34e1..54f1e8581f 100644 --- a/src/Umbraco.Core/Models/IProperty.cs +++ b/src/Umbraco.Core/Models/IProperty.cs @@ -1,40 +1,39 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IProperty : IEntity, IRememberBeingDirty { - public interface IProperty : IEntity, IRememberBeingDirty - { + ValueStorageType ValueStorageType { get; } - ValueStorageType ValueStorageType { get; } - /// - /// Returns the PropertyType, which this Property is based on - /// - IPropertyType PropertyType { get; } + /// + /// Returns the PropertyType, which this Property is based on + /// + IPropertyType PropertyType { get; } - /// - /// Gets the list of values. - /// - IReadOnlyCollection Values { get; set; } + /// + /// Gets the list of values. + /// + IReadOnlyCollection Values { get; set; } - /// - /// Returns the Alias of the PropertyType, which this Property is based on - /// - string Alias { get; } + /// + /// Returns the Alias of the PropertyType, which this Property is based on + /// + string Alias { get; } - /// - /// Gets the value. - /// - object? GetValue(string? culture = null, string? segment = null, bool published = false); + int PropertyTypeId { get; } - /// - /// Sets a value. - /// - void SetValue(object? value, string? culture = null, string? segment = null); + /// + /// Gets the value. + /// + object? GetValue(string? culture = null, string? segment = null, bool published = false); - int PropertyTypeId { get; } - void PublishValues(string? culture = "*", string segment = "*"); - void UnpublishValues(string? culture = "*", string segment = "*"); + /// + /// Sets a value. + /// + void SetValue(object? value, string? culture = null, string? segment = null); - } + void PublishValues(string? culture = "*", string segment = "*"); + + void UnpublishValues(string? culture = "*", string segment = "*"); } diff --git a/src/Umbraco.Core/Models/IPropertyCollection.cs b/src/Umbraco.Core/Models/IPropertyCollection.cs index d39a214fdd..535756fad8 100644 --- a/src/Umbraco.Core/Models/IPropertyCollection.cs +++ b/src/Umbraco.Core/Models/IPropertyCollection.cs @@ -1,40 +1,40 @@ -using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IPropertyCollection : IEnumerable, IDeepCloneable, INotifyCollectionChanged { - public interface IPropertyCollection : IEnumerable, IDeepCloneable, INotifyCollectionChanged - { - bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property); - bool Contains(string key); + int Count { get; } - /// - /// Ensures that the collection contains properties for the specified property types. - /// - void EnsurePropertyTypes(IEnumerable propertyTypes); + /// + /// Gets the property with the specified alias. + /// + IProperty? this[string name] { get; } - /// - /// Ensures that the collection does not contain properties not in the specified property types. - /// - void EnsureCleanPropertyTypes(IEnumerable propertyTypes); + /// + /// Gets the property at the specified index. + /// + IProperty? this[int index] { get; } - /// - /// Gets the property with the specified alias. - /// - IProperty? this[string name] { get; } + bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property); - /// - /// Gets the property at the specified index. - /// - IProperty? this[int index] { get; } + bool Contains(string key); - /// - /// Adds or updates a property. - /// - void Add(IProperty property); + /// + /// Ensures that the collection contains properties for the specified property types. + /// + void EnsurePropertyTypes(IEnumerable propertyTypes); - int Count { get; } - void ClearCollectionChangedEvents(); - } + /// + /// Ensures that the collection does not contain properties not in the specified property types. + /// + void EnsureCleanPropertyTypes(IEnumerable propertyTypes); + + /// + /// Adds or updates a property. + /// + void Add(IProperty property); + + void ClearCollectionChangedEvents(); } diff --git a/src/Umbraco.Core/Models/IPropertyType.cs b/src/Umbraco.Core/Models/IPropertyType.cs index b820c1d7aa..a48f8e01ae 100644 --- a/src/Umbraco.Core/Models/IPropertyType.cs +++ b/src/Umbraco.Core/Models/IPropertyType.cs @@ -1,91 +1,89 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IPropertyType : IEntity, IRememberBeingDirty { - public interface IPropertyType : IEntity, IRememberBeingDirty - { - /// - /// Gets of sets the name of the property type. - /// - string Name { get; set; } + /// + /// Gets of sets the name of the property type. + /// + string Name { get; set; } - /// - /// Gets of sets the alias of the property type. - /// - string Alias { get; set; } + /// + /// Gets of sets the alias of the property type. + /// + string Alias { get; set; } - /// - /// Gets of sets the description of the property type. - /// - string? Description { get; set; } + /// + /// Gets of sets the description of the property type. + /// + string? Description { get; set; } - /// - /// Gets or sets the identifier of the datatype for this property type. - /// - int DataTypeId { get; set; } + /// + /// Gets or sets the identifier of the datatype for this property type. + /// + int DataTypeId { get; set; } - Guid DataTypeKey { get; set; } + Guid DataTypeKey { get; set; } - /// - /// Gets or sets the alias of the property editor for this property type. - /// - string PropertyEditorAlias { get; set; } + /// + /// Gets or sets the alias of the property editor for this property type. + /// + string PropertyEditorAlias { get; set; } - /// - /// Gets or sets the database type for storing value for this property type. - /// - ValueStorageType ValueStorageType { get; set; } + /// + /// Gets or sets the database type for storing value for this property type. + /// + ValueStorageType ValueStorageType { get; set; } - /// - /// Gets or sets the identifier of the property group this property type belongs to. - /// - /// For generic properties, the value is null. - Lazy? PropertyGroupId { get; set; } + /// + /// Gets or sets the identifier of the property group this property type belongs to. + /// + /// For generic properties, the value is null. + Lazy? PropertyGroupId { get; set; } - /// - /// Gets of sets a value indicating whether a value for this property type is required. - /// - bool Mandatory { get; set; } + /// + /// Gets of sets a value indicating whether a value for this property type is required. + /// + bool Mandatory { get; set; } - /// - /// Gets or sets a value indicating whether the label of this property type should be displayed on top. - /// - bool LabelOnTop { get; set; } + /// + /// Gets or sets a value indicating whether the label of this property type should be displayed on top. + /// + bool LabelOnTop { get; set; } - /// - /// Gets of sets the sort order of the property type. - /// - int SortOrder { get; set; } + /// + /// Gets of sets the sort order of the property type. + /// + int SortOrder { get; set; } - /// - /// Gets or sets the regular expression validating the property values. - /// - string? ValidationRegExp { get; set; } + /// + /// Gets or sets the regular expression validating the property values. + /// + string? ValidationRegExp { get; set; } - bool SupportsPublishing { get; set; } + bool SupportsPublishing { get; set; } - /// - /// Gets or sets the content variation of the property type. - /// - ContentVariation Variations { get; set; } + /// + /// Gets or sets the content variation of the property type. + /// + ContentVariation Variations { get; set; } - /// - /// Determines whether the property type supports a combination of culture and segment. - /// - /// The culture. - /// The segment. - /// A value indicating whether wildcards are valid. - bool SupportsVariation(string? culture, string? segment, bool wildcards = false); + /// + /// Gets or sets the custom validation message used when a value for this PropertyType is required + /// + string? MandatoryMessage { get; set; } - /// - /// Gets or sets the custom validation message used when a value for this PropertyType is required - /// - string? MandatoryMessage { get; set; } + /// + /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched + /// + string? ValidationRegExpMessage { get; set; } - /// - /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched - /// - string? ValidationRegExpMessage { get; set; } - } + /// + /// Determines whether the property type supports a combination of culture and segment. + /// + /// The culture. + /// The segment. + /// A value indicating whether wildcards are valid. + bool SupportsVariation(string? culture, string? segment, bool wildcards = false); } diff --git a/src/Umbraco.Core/Models/IPropertyValue.cs b/src/Umbraco.Core/Models/IPropertyValue.cs index 77e9e1dc25..ef95cd2a01 100644 --- a/src/Umbraco.Core/Models/IPropertyValue.cs +++ b/src/Umbraco.Core/Models/IPropertyValue.cs @@ -1,34 +1,37 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IPropertyValue { - public interface IPropertyValue - { - /// - /// Gets or sets the culture of the property. - /// - /// The culture is either null (invariant) or a non-empty string. If the property is - /// set with an empty or whitespace value, its value is converted to null. - string? Culture { get; set; } + /// + /// Gets or sets the culture of the property. + /// + /// + /// The culture is either null (invariant) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. + /// + string? Culture { get; set; } - /// - /// Gets or sets the segment of the property. - /// - /// The segment is either null (neutral) or a non-empty string. If the property is - /// set with an empty or whitespace value, its value is converted to null. - string? Segment { get; set; } + /// + /// Gets or sets the segment of the property. + /// + /// + /// The segment is either null (neutral) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. + /// + string? Segment { get; set; } - /// - /// Gets or sets the edited value of the property. - /// - object? EditedValue { get; set; } + /// + /// Gets or sets the edited value of the property. + /// + object? EditedValue { get; set; } - /// - /// Gets or sets the published value of the property. - /// - object? PublishedValue { get; set; } + /// + /// Gets or sets the published value of the property. + /// + object? PublishedValue { get; set; } - /// - /// Clones the property value. - /// - IPropertyValue Clone(); - } + /// + /// Clones the property value. + /// + IPropertyValue Clone(); } diff --git a/src/Umbraco.Core/Models/IReadOnlyContentBase.cs b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs index f7518140f5..37b5a5ddea 100644 --- a/src/Umbraco.Core/Models/IReadOnlyContentBase.cs +++ b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs @@ -1,72 +1,69 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public interface IReadOnlyContentBase { - public interface IReadOnlyContentBase - { - /// - /// Gets the integer identifier of the entity. - /// - int Id { get; } + /// + /// Gets the integer identifier of the entity. + /// + int Id { get; } - /// - /// Gets the Guid unique identifier of the entity. - /// - Guid Key { get; } + /// + /// Gets the Guid unique identifier of the entity. + /// + Guid Key { get; } - /// - /// Gets the creation date. - /// - DateTime CreateDate { get; } + /// + /// Gets the creation date. + /// + DateTime CreateDate { get; } - /// - /// Gets the last update date. - /// - DateTime UpdateDate { get; } + /// + /// Gets the last update date. + /// + DateTime UpdateDate { get; } - /// - /// Gets the name of the entity. - /// - string? Name { get; } + /// + /// Gets the name of the entity. + /// + string? Name { get; } - /// - /// Gets the identifier of the user who created this entity. - /// - int CreatorId { get; } + /// + /// Gets the identifier of the user who created this entity. + /// + int CreatorId { get; } - /// - /// Gets the identifier of the parent entity. - /// - int ParentId { get; } + /// + /// Gets the identifier of the parent entity. + /// + int ParentId { get; } - /// - /// Gets the level of the entity. - /// - int Level { get; } + /// + /// Gets the level of the entity. + /// + int Level { get; } - /// - /// Gets the path to the entity. - /// - string? Path { get; } + /// + /// Gets the path to the entity. + /// + string? Path { get; } - /// - /// Gets the sort order of the entity. - /// - int SortOrder { get; } + /// + /// Gets the sort order of the entity. + /// + int SortOrder { get; } - /// - /// Gets the content type id - /// - int ContentTypeId { get; } + /// + /// Gets the content type id + /// + int ContentTypeId { get; } - /// - /// Gets the identifier of the writer. - /// - int WriterId { get; } + /// + /// Gets the identifier of the writer. + /// + int WriterId { get; } - /// - /// Gets the version identifier. - /// - int VersionId { get; } - } + /// + /// Gets the version identifier. + /// + int VersionId { get; } } diff --git a/src/Umbraco.Core/Models/IRedirectUrl.cs b/src/Umbraco.Core/Models/IRedirectUrl.cs index 18498837b4..cbd12eb0b8 100644 --- a/src/Umbraco.Core/Models/IRedirectUrl.cs +++ b/src/Umbraco.Core/Models/IRedirectUrl.cs @@ -1,44 +1,41 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a redirect URL. +/// +public interface IRedirectUrl : IEntity, IRememberBeingDirty { /// - /// Represents a redirect URL. + /// Gets or sets the identifier of the content item. /// - public interface IRedirectUrl : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the identifier of the content item. - /// - [DataMember] - int ContentId { get; set; } + [DataMember] + int ContentId { get; set; } - /// - /// Gets or sets the unique key identifying the content item. - /// - [DataMember] - Guid ContentKey { get; set; } + /// + /// Gets or sets the unique key identifying the content item. + /// + [DataMember] + Guid ContentKey { get; set; } - /// - /// Gets or sets the redirect URL creation date. - /// - [DataMember] - DateTime CreateDateUtc { get; set; } + /// + /// Gets or sets the redirect URL creation date. + /// + [DataMember] + DateTime CreateDateUtc { get; set; } - /// - /// Gets or sets the culture. - /// - [DataMember] - string? Culture { get; set; } + /// + /// Gets or sets the culture. + /// + [DataMember] + string? Culture { get; set; } - /// - /// Gets or sets the redirect URL route. - /// - /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. - [DataMember] - string Url { get; set; } - - } + /// + /// Gets or sets the redirect URL route. + /// + /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo. + [DataMember] + string Url { get; set; } } diff --git a/src/Umbraco.Core/Models/IRelation.cs b/src/Umbraco.Core/Models/IRelation.cs index 0370bbe61f..468a51b897 100644 --- a/src/Umbraco.Core/Models/IRelation.cs +++ b/src/Umbraco.Core/Models/IRelation.cs @@ -1,45 +1,43 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IRelation : IEntity, IRememberBeingDirty { - public interface IRelation : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the Parent Id of the Relation (Source) - /// - [DataMember] - int ParentId { get; set; } + /// + /// Gets or sets the Parent Id of the Relation (Source) + /// + [DataMember] + int ParentId { get; set; } - [DataMember] - Guid ParentObjectType { get; set; } + [DataMember] + Guid ParentObjectType { get; set; } - /// - /// Gets or sets the Child Id of the Relation (Destination) - /// - [DataMember] - int ChildId { get; set; } + /// + /// Gets or sets the Child Id of the Relation (Destination) + /// + [DataMember] + int ChildId { get; set; } - [DataMember] - Guid ChildObjectType { get; set; } + [DataMember] + Guid ChildObjectType { get; set; } - /// - /// Gets or sets the for the Relation - /// - [DataMember] - IRelationType RelationType { get; set; } + /// + /// Gets or sets the for the Relation + /// + [DataMember] + IRelationType RelationType { get; set; } - /// - /// Gets or sets a comment for the Relation - /// - [DataMember] - string? Comment { get; set; } + /// + /// Gets or sets a comment for the Relation + /// + [DataMember] + string? Comment { get; set; } - /// - /// Gets the Id of the that this Relation is based on. - /// - [IgnoreDataMember] - int RelationTypeId { get; } - } + /// + /// Gets the Id of the that this Relation is based on. + /// + [IgnoreDataMember] + int RelationTypeId { get; } } diff --git a/src/Umbraco.Core/Models/IRelationType.cs b/src/Umbraco.Core/Models/IRelationType.cs index cbc485f64b..7675a1c49e 100644 --- a/src/Umbraco.Core/Models/IRelationType.cs +++ b/src/Umbraco.Core/Models/IRelationType.cs @@ -1,50 +1,48 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IRelationTypeWithIsDependency : IRelationType { - public interface IRelationTypeWithIsDependency : IRelationType - { - /// - /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. - /// - [DataMember] - bool IsDependency { get; set; } - } - - public interface IRelationType : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the Name of the RelationType - /// - [DataMember] - string? Name { get; set; } - - /// - /// Gets or sets the Alias of the RelationType - /// - [DataMember] - string Alias { get; set; } - - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember] - bool IsBidirectional { get; set; } - - /// - /// Gets or sets the Parents object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember] - Guid? ParentObjectType { get; set; } - - /// - /// Gets or sets the Childs object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember] - Guid? ChildObjectType { get; set; } - } + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember] + bool IsDependency { get; set; } +} + +public interface IRelationType : IEntity, IRememberBeingDirty +{ + /// + /// Gets or sets the Name of the RelationType + /// + [DataMember] + string? Name { get; set; } + + /// + /// Gets or sets the Alias of the RelationType + /// + [DataMember] + string Alias { get; set; } + + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember] + bool IsBidirectional { get; set; } + + /// + /// Gets or sets the Parents object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember] + Guid? ParentObjectType { get; set; } + + /// + /// Gets or sets the Childs object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember] + Guid? ChildObjectType { get; set; } } diff --git a/src/Umbraco.Core/Models/IScript.cs b/src/Umbraco.Core/Models/IScript.cs index 6a07d2aa25..f52bdc0286 100644 --- a/src/Umbraco.Core/Models/IScript.cs +++ b/src/Umbraco.Core/Models/IScript.cs @@ -1,7 +1,5 @@ -namespace Umbraco.Cms.Core.Models -{ - public interface IScript : IFile - { +namespace Umbraco.Cms.Core.Models; - } +public interface IScript : IFile +{ } diff --git a/src/Umbraco.Core/Models/IServerRegistration.cs b/src/Umbraco.Core/Models/IServerRegistration.cs index 7d8c0f58c1..525ed30163 100644 --- a/src/Umbraco.Core/Models/IServerRegistration.cs +++ b/src/Umbraco.Core/Models/IServerRegistration.cs @@ -1,36 +1,34 @@ -using System; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IServerRegistration : IServerAddress, IEntity, IRememberBeingDirty { - public interface IServerRegistration : IServerAddress, IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the server unique identity. - /// - string? ServerIdentity { get; set; } + /// + /// Gets or sets the server unique identity. + /// + string? ServerIdentity { get; set; } - new string? ServerAddress { get; set; } + new string? ServerAddress { get; set; } - /// - /// Gets or sets a value indicating whether the server is active. - /// - bool IsActive { get; set; } + /// + /// Gets or sets a value indicating whether the server is active. + /// + bool IsActive { get; set; } - /// - /// Gets or sets a value indicating whether the server is has the SchedulingPublisher role. - /// - bool IsSchedulingPublisher { get; set; } + /// + /// Gets or sets a value indicating whether the server is has the SchedulingPublisher role. + /// + bool IsSchedulingPublisher { get; set; } - /// - /// Gets the date and time the registration was created. - /// - DateTime Registered { get; set; } + /// + /// Gets the date and time the registration was created. + /// + DateTime Registered { get; set; } - /// - /// Gets the date and time the registration was last accessed. - /// - DateTime Accessed { get; set; } - } + /// + /// Gets the date and time the registration was last accessed. + /// + DateTime Accessed { get; set; } } diff --git a/src/Umbraco.Core/Models/ISimpleContentType.cs b/src/Umbraco.Core/Models/ISimpleContentType.cs index 503946ba96..8246b50ca0 100644 --- a/src/Umbraco.Core/Models/ISimpleContentType.cs +++ b/src/Umbraco.Core/Models/ISimpleContentType.cs @@ -1,63 +1,66 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a simplified view of a content type. +/// +public interface ISimpleContentType { + int Id { get; } + + Guid Key { get; } + + string? Name { get; } + /// - /// Represents a simplified view of a content type. + /// Gets the alias of the content type. /// - public interface ISimpleContentType - { - int Id { get; } - Guid Key { get; } - string? Name { get; } + string Alias { get; } - /// - /// Gets the alias of the content type. - /// - string Alias { get; } + /// + /// Gets the default template of the content type. + /// + ITemplate? DefaultTemplate { get; } - /// - /// Gets the default template of the content type. - /// - ITemplate? DefaultTemplate { get; } + /// + /// Gets the content variation of the content type. + /// + ContentVariation Variations { get; } - /// - /// Gets the content variation of the content type. - /// - ContentVariation Variations { get; } + /// + /// Gets the icon of the content type. + /// + string? Icon { get; } - /// - /// Gets the icon of the content type. - /// - string? Icon { get; } + /// + /// Gets a value indicating whether the content type is a container. + /// + bool IsContainer { get; } - /// - /// Gets a value indicating whether the content type is a container. - /// - bool IsContainer { get; } + /// + /// Gets a value indicating whether content of that type can be created at the root of the tree. + /// + bool AllowedAsRoot { get; } - /// - /// Gets a value indicating whether content of that type can be created at the root of the tree. - /// - bool AllowedAsRoot { get; } + /// + /// Gets a value indicating whether the content type is an element content type. + /// + bool IsElement { get; } - /// - /// Gets a value indicating whether the content type is an element content type. - /// - bool IsElement { get; } - - /// - /// Validates that a combination of culture and segment is valid for the content type properties. - /// - /// The culture. - /// The segment. - /// A value indicating whether wildcard are supported. - /// True if the combination is valid; otherwise false. - /// - /// The combination must be valid for properties of the content type. For instance, if the content type varies by culture, - /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content type is invariant, - /// then a variant culture is invalid, because no property could possibly vary by culture. - /// - bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false); - } + /// + /// Validates that a combination of culture and segment is valid for the content type properties. + /// + /// The culture. + /// The segment. + /// A value indicating whether wildcard are supported. + /// True if the combination is valid; otherwise false. + /// + /// + /// The combination must be valid for properties of the content type. For instance, if the content type varies by + /// culture, + /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content + /// type is invariant, + /// then a variant culture is invalid, because no property could possibly vary by culture. + /// + /// + bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false); } diff --git a/src/Umbraco.Core/Models/IStylesheet.cs b/src/Umbraco.Core/Models/IStylesheet.cs index e7710f26df..fbe9a1652b 100644 --- a/src/Umbraco.Core/Models/IStylesheet.cs +++ b/src/Umbraco.Core/Models/IStylesheet.cs @@ -1,29 +1,25 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public interface IStylesheet : IFile { - public interface IStylesheet : IFile - { - /// - /// Returns a list of umbraco back office enabled stylesheet properties - /// - /// - /// An umbraco back office enabled stylesheet property has a special prefix, for example: - /// - /// /** umb_name: MyPropertyName */ p { font-size: 1em; } - /// - IEnumerable? Properties { get; } + /// + /// Returns a list of umbraco back office enabled stylesheet properties + /// + /// + /// An umbraco back office enabled stylesheet property has a special prefix, for example: + /// /** umb_name: MyPropertyName */ p { font-size: 1em; } + /// + IEnumerable? Properties { get; } - /// - /// Adds an Umbraco stylesheet property for use in the back office - /// - /// - void AddProperty(IStylesheetProperty property); + /// + /// Adds an Umbraco stylesheet property for use in the back office + /// + /// + void AddProperty(IStylesheetProperty property); - /// - /// Removes an Umbraco stylesheet property - /// - /// - void RemoveProperty(string name); - } + /// + /// Removes an Umbraco stylesheet property + /// + /// + void RemoveProperty(string name); } diff --git a/src/Umbraco.Core/Models/IStylesheetProperty.cs b/src/Umbraco.Core/Models/IStylesheetProperty.cs index 781fb474b2..c2bb81060d 100644 --- a/src/Umbraco.Core/Models/IStylesheetProperty.cs +++ b/src/Umbraco.Core/Models/IStylesheetProperty.cs @@ -1,11 +1,12 @@ -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface IStylesheetProperty : IRememberBeingDirty { - public interface IStylesheetProperty : IRememberBeingDirty - { - string Alias { get; set; } - string Name { get; } - string Value { get; set; } - } + string Alias { get; set; } + + string Name { get; } + + string Value { get; set; } } diff --git a/src/Umbraco.Core/Models/ITag.cs b/src/Umbraco.Core/Models/ITag.cs index 79840481bb..9824ee5ed2 100644 --- a/src/Umbraco.Core/Models/ITag.cs +++ b/src/Umbraco.Core/Models/ITag.cs @@ -1,35 +1,34 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a tag entity. +/// +public interface ITag : IEntity, IRememberBeingDirty { /// - /// Represents a tag entity. + /// Gets or sets the tag group. /// - public interface ITag : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the tag group. - /// - [DataMember] - string Group { get; set; } + [DataMember] + string Group { get; set; } - /// - /// Gets or sets the tag text. - /// - [DataMember] - string Text { get; set; } + /// + /// Gets or sets the tag text. + /// + [DataMember] + string Text { get; set; } - /// - /// Gets or sets the tag language. - /// - [DataMember] - int? LanguageId { get; set; } + /// + /// Gets or sets the tag language. + /// + [DataMember] + int? LanguageId { get; set; } - /// - /// Gets the number of nodes tagged with this tag. - /// - /// Only when returning from queries. - int NodeCount { get; } - } + /// + /// Gets the number of nodes tagged with this tag. + /// + /// Only when returning from queries. + int NodeCount { get; } } diff --git a/src/Umbraco.Core/Models/ITemplate.cs b/src/Umbraco.Core/Models/ITemplate.cs index e20dcc55fa..321fff2831 100644 --- a/src/Umbraco.Core/Models/ITemplate.cs +++ b/src/Umbraco.Core/Models/ITemplate.cs @@ -1,36 +1,33 @@ -using Umbraco.Cms.Core.Models.Entities; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Defines a Template File (Mvc View) +/// +public interface ITemplate : IFile { /// - /// Defines a Template File (Mvc View) + /// Gets the Name of the File including extension /// - public interface ITemplate : IFile, IRememberBeingDirty, ICanBeDirty - { - /// - /// Gets the Name of the File including extension - /// - new string? Name { get; set; } + new string? Name { get; set; } - /// - /// Gets the Alias of the File, which is the name without the extension - /// - new string Alias { get; set; } + /// + /// Gets the Alias of the File, which is the name without the extension + /// + new string Alias { get; set; } - /// - /// Returns true if the template is used as a layout for other templates (i.e. it has 'children') - /// - bool IsMasterTemplate { get; set; } + /// + /// Returns true if the template is used as a layout for other templates (i.e. it has 'children') + /// + bool IsMasterTemplate { get; set; } - /// - /// returns the master template alias - /// - string? MasterTemplateAlias { get; } + /// + /// returns the master template alias + /// + string? MasterTemplateAlias { get; } - /// - /// Set the mastertemplate - /// - /// - void SetMasterTemplate(ITemplate? masterTemplate); - } + /// + /// Set the mastertemplate + /// + /// + void SetMasterTemplate(ITemplate? masterTemplate); } diff --git a/src/Umbraco.Core/Models/ITwoFactorLogin.cs b/src/Umbraco.Core/Models/ITwoFactorLogin.cs index ca005309b2..3840dcb174 100644 --- a/src/Umbraco.Core/Models/ITwoFactorLogin.cs +++ b/src/Umbraco.Core/Models/ITwoFactorLogin.cs @@ -1,12 +1,12 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public interface ITwoFactorLogin : IEntity, IRememberBeingDirty { - public interface ITwoFactorLogin: IEntity, IRememberBeingDirty - { - string ProviderName { get; } - string Secret { get; } - Guid UserOrMemberKey { get; } - } + string ProviderName { get; } + + string Secret { get; } + + Guid UserOrMemberKey { get; } } diff --git a/src/Umbraco.Core/Models/IconModel.cs b/src/Umbraco.Core/Models/IconModel.cs index 6b09c08602..8fd9005ac3 100644 --- a/src/Umbraco.Core/Models/IconModel.cs +++ b/src/Umbraco.Core/Models/IconModel.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class IconModel { - public class IconModel - { - public string Name { get; set; } = null!; - public string SvgString { get; set; } = null!; - } + public string Name { get; set; } = null!; + + public string SvgString { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/ImageCropAnchor.cs b/src/Umbraco.Core/Models/ImageCropAnchor.cs index 118f7348ae..68544289c6 100644 --- a/src/Umbraco.Core/Models/ImageCropAnchor.cs +++ b/src/Umbraco.Core/Models/ImageCropAnchor.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public enum ImageCropAnchor { - public enum ImageCropAnchor - { - Center, - Top, - Right, - Bottom, - Left, - TopLeft, - TopRight, - BottomLeft, - BottomRight - } + Center, + Top, + Right, + Bottom, + Left, + TopLeft, + TopRight, + BottomLeft, + BottomRight, } diff --git a/src/Umbraco.Core/Models/ImageCropMode.cs b/src/Umbraco.Core/Models/ImageCropMode.cs index 1cd7294a58..3ce2f4bfb9 100644 --- a/src/Umbraco.Core/Models/ImageCropMode.cs +++ b/src/Umbraco.Core/Models/ImageCropMode.cs @@ -1,35 +1,41 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public enum ImageCropMode { - public enum ImageCropMode - { - /// - /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original image then the output is cropped to match the new aspect ratio. - /// - Crop, + /// + /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original + /// image then the output is cropped to match the new aspect ratio. + /// + Crop, - /// - /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original image then the output is resized to the maximum possible value in each direction while maintaining the original aspect ratio. - /// - Max, + /// + /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original + /// image then the output is resized to the maximum possible value in each direction while maintaining the original + /// aspect ratio. + /// + Max, - /// - /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original image then the output is stretched to match the new aspect ratio. - /// - Stretch, + /// + /// Resizes the image to the given dimensions. If the set dimensions do not match the aspect ratio of the original + /// image then the output is stretched to match the new aspect ratio. + /// + Stretch, - /// - /// Passing a single dimension will automatically preserve the aspect ratio of the original image. If the requested aspect ratio is different then the image will be padded to fit. - /// - Pad, + /// + /// Passing a single dimension will automatically preserve the aspect ratio of the original image. If the requested + /// aspect ratio is different then the image will be padded to fit. + /// + Pad, - /// - /// When upscaling an image the image pixels themselves are not resized, rather the image is padded to fit the given dimensions. - /// - BoxPad, + /// + /// When upscaling an image the image pixels themselves are not resized, rather the image is padded to fit the given + /// dimensions. + /// + BoxPad, - /// - /// Resizes the image until the shortest side reaches the set given dimension. This will maintain the aspect ratio of the original image. Upscaling is disabled in this mode and the original image will be returned if attempted. - /// - Min - } + /// + /// Resizes the image until the shortest side reaches the set given dimension. This will maintain the aspect ratio of + /// the original image. Upscaling is disabled in this mode and the original image will be returned if attempted. + /// + Min, } diff --git a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs index 876b2bfddb..9fd00ac2ab 100644 --- a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs +++ b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs @@ -1,124 +1,122 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated. +/// +public class ImageUrlGenerationOptions : IEquatable { - /// - /// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated. - /// - public class ImageUrlGenerationOptions : IEquatable + public ImageUrlGenerationOptions(string? imageUrl) => ImageUrl = imageUrl; + + public string? ImageUrl { get; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public int? Quality { get; set; } + + public ImageCropMode? ImageCropMode { get; set; } + + public ImageCropAnchor? ImageCropAnchor { get; set; } + + public FocalPointPosition? FocalPoint { get; set; } + + public CropCoordinates? Crop { get; set; } + + public string? CacheBusterValue { get; set; } + + public string? FurtherOptions { get; set; } + + public bool Equals(ImageUrlGenerationOptions? other) + => other != null && + ImageUrl == other.ImageUrl && + Width == other.Width && + Height == other.Height && + Quality == other.Quality && + ImageCropMode == other.ImageCropMode && + ImageCropAnchor == other.ImageCropAnchor && + EqualityComparer.Default.Equals(FocalPoint, other.FocalPoint) && + EqualityComparer.Default.Equals(Crop, other.Crop) && + CacheBusterValue == other.CacheBusterValue && + FurtherOptions == other.FurtherOptions; + + public override bool Equals(object? obj) => Equals(obj as ImageUrlGenerationOptions); + + public override int GetHashCode() { - public ImageUrlGenerationOptions(string? imageUrl) => ImageUrl = imageUrl; + var hash = default(HashCode); - public string? ImageUrl { get; } + hash.Add(ImageUrl); + hash.Add(Width); + hash.Add(Height); + hash.Add(Quality); + hash.Add(ImageCropMode); + hash.Add(ImageCropAnchor); + hash.Add(FocalPoint); + hash.Add(Crop); + hash.Add(CacheBusterValue); + hash.Add(FurtherOptions); - public int? Width { get; set; } + return hash.ToHashCode(); + } - public int? Height { get; set; } + /// + /// The focal point position, in whatever units the registered IImageUrlGenerator uses, typically a percentage of the + /// total image from 0.0 to 1.0. + /// + public class FocalPointPosition : IEquatable + { + public FocalPointPosition(decimal left, decimal top) + { + Left = left; + Top = top; + } - public int? Quality { get; set; } + public decimal Left { get; } - public ImageCropMode? ImageCropMode { get; set; } + public decimal Top { get; } - public ImageCropAnchor? ImageCropAnchor { get; set; } - - public FocalPointPosition? FocalPoint { get; set; } - - public CropCoordinates? Crop { get; set; } - - public string? CacheBusterValue { get; set; } - - public string? FurtherOptions { get; set; } - - public override bool Equals(object? obj) => Equals(obj as ImageUrlGenerationOptions); - - public bool Equals(ImageUrlGenerationOptions? other) + public bool Equals(FocalPointPosition? other) => other != null && - ImageUrl == other.ImageUrl && - Width == other.Width && - Height == other.Height && - Quality == other.Quality && - ImageCropMode == other.ImageCropMode && - ImageCropAnchor == other.ImageCropAnchor && - EqualityComparer.Default.Equals(FocalPoint, other.FocalPoint) && - EqualityComparer.Default.Equals(Crop, other.Crop) && - CacheBusterValue == other.CacheBusterValue && - FurtherOptions == other.FurtherOptions; + Left == other.Left && + Top == other.Top; - public override int GetHashCode() + public override bool Equals(object? obj) => Equals(obj as FocalPointPosition); + + public override int GetHashCode() => HashCode.Combine(Left, Top); + } + + /// + /// The bounds of the crop within the original image, in whatever units the registered IImageUrlGenerator uses, + /// typically a percentage between 0.0 and 1.0. + /// + public class CropCoordinates : IEquatable + { + public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) { - var hash = new HashCode(); - - hash.Add(ImageUrl); - hash.Add(Width); - hash.Add(Height); - hash.Add(Quality); - hash.Add(ImageCropMode); - hash.Add(ImageCropAnchor); - hash.Add(FocalPoint); - hash.Add(Crop); - hash.Add(CacheBusterValue); - hash.Add(FurtherOptions); - - return hash.ToHashCode(); + Left = left; + Top = top; + Right = right; + Bottom = bottom; } - /// - /// The focal point position, in whatever units the registered IImageUrlGenerator uses, typically a percentage of the total image from 0.0 to 1.0. - /// - public class FocalPointPosition : IEquatable - { - public FocalPointPosition(decimal left, decimal top) - { - Left = left; - Top = top; - } + public decimal Left { get; } - public decimal Left { get; } + public decimal Top { get; } - public decimal Top { get; } + public decimal Right { get; } - public override bool Equals(object? obj) => Equals(obj as FocalPointPosition); + public decimal Bottom { get; } - public bool Equals(FocalPointPosition? other) - => other != null && - Left == other.Left && - Top == other.Top; + public bool Equals(CropCoordinates? other) + => other != null && + Left == other.Left && + Top == other.Top && + Right == other.Right && + Bottom == other.Bottom; - public override int GetHashCode() => HashCode.Combine(Left, Top); - } + public override bool Equals(object? obj) => Equals(obj as CropCoordinates); - /// - /// The bounds of the crop within the original image, in whatever units the registered IImageUrlGenerator uses, typically a percentage between 0.0 and 1.0. - /// - public class CropCoordinates : IEquatable - { - public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) - { - Left = left; - Top = top; - Right = right; - Bottom = bottom; - } - - public decimal Left { get; } - - public decimal Top { get; } - - public decimal Right { get; } - - public decimal Bottom { get; } - - public override bool Equals(object? obj) => Equals(obj as CropCoordinates); - - public bool Equals(CropCoordinates? other) - => other != null && - Left == other.Left && - Top == other.Top && - Right == other.Right && - Bottom == other.Bottom; - - public override int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom); - } + public override int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom); } } diff --git a/src/Umbraco.Core/Models/KeyValue.cs b/src/Umbraco.Core/Models/KeyValue.cs index 4e38ee3390..bf5b26dbee 100644 --- a/src/Umbraco.Core/Models/KeyValue.cs +++ b/src/Umbraco.Core/Models/KeyValue.cs @@ -1,33 +1,31 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Implements . +/// +[Serializable] +[DataContract(IsReference = true)] +public class KeyValue : EntityBase, IKeyValue { - /// - /// Implements . - /// - [Serializable] - [DataContract(IsReference = true)] - public class KeyValue : EntityBase, IKeyValue, IEntity + private string _identifier = null!; + private string? _value; + + /// + public string Identifier { - private string _identifier = null!; - private string? _value; - - /// - public string Identifier - { - get => _identifier; - set => SetPropertyValueAndDetectChanges(value, ref _identifier!, nameof(Identifier)); - } - - /// - public string? Value - { - get => _value; - set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value)); - } - - bool IEntity.HasIdentity => !string.IsNullOrEmpty(Identifier); + get => _identifier; + set => SetPropertyValueAndDetectChanges(value, ref _identifier!, nameof(Identifier)); } + + /// + public string? Value + { + get => _value; + set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value)); + } + + bool IEntity.HasIdentity => !string.IsNullOrEmpty(Identifier); } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 20d936af61..9299665755 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -3,88 +3,88 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Language. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Language : EntityBase, ILanguage { + private string _cultureName; + private int? _fallbackLanguageId; + private bool _isDefaultVariantLanguage; + private string _isoCode; + private bool _mandatory; + /// - /// Represents a Language. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class Language : EntityBase, ILanguage + /// The ISO code of the language. + /// The name of the language. + public Language(string isoCode, string cultureName) { - private string _isoCode; - private string _cultureName; - private bool _isDefaultVariantLanguage; - private bool _mandatory; - private int? _fallbackLanguageId; + _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); + _cultureName = cultureName ?? throw new ArgumentNullException(nameof(cultureName)); + } - /// - /// Initializes a new instance of the class. - /// - /// The ISO code of the language. - /// The name of the language. - public Language(string isoCode, string cultureName) + [Obsolete( + "Use the constructor not requiring global settings and accepting an explicit name instead, scheduled for removal in V11.")] + public Language(GlobalSettings globalSettings, string isoCode) + { + _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); + _cultureName = CultureInfo.GetCultureInfo(isoCode).EnglishName; + } + + /// + [DataMember] + public string IsoCode + { + get => _isoCode; + set { - _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); - _cultureName = cultureName ?? throw new ArgumentNullException(nameof(cultureName)); - } + ArgumentNullException.ThrowIfNull(value); - [Obsolete("Use the constructor not requiring global settings and accepting an explicit name instead, scheduled for removal in V11.")] - public Language(GlobalSettings globalSettings, string isoCode) - { - _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); - _cultureName = CultureInfo.GetCultureInfo(isoCode).EnglishName; - } - - /// - [DataMember] - public string IsoCode - { - get => _isoCode; - set - { - ArgumentNullException.ThrowIfNull(value); - - SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); - } - } - - /// - [DataMember] - public string CultureName - { - get => _cultureName; - set - { - ArgumentNullException.ThrowIfNull(value); - - SetPropertyValueAndDetectChanges(value, ref _cultureName!, nameof(CultureName)); - } - } - - /// - [IgnoreDataMember] - public CultureInfo? CultureInfo => IsoCode is not null ? CultureInfo.GetCultureInfo(IsoCode) : null; - - /// - public bool IsDefault - { - get => _isDefaultVariantLanguage; - set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, nameof(IsDefault)); - } - - /// - public bool IsMandatory - { - get => _mandatory; - set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(IsMandatory)); - } - - /// - public int? FallbackLanguageId - { - get => _fallbackLanguageId; - set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, nameof(FallbackLanguageId)); + SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); } } + + /// + [DataMember] + public string CultureName + { + get => _cultureName; + set + { + ArgumentNullException.ThrowIfNull(value); + + SetPropertyValueAndDetectChanges(value, ref _cultureName!, nameof(CultureName)); + } + } + + /// + [IgnoreDataMember] + public CultureInfo? CultureInfo => IsoCode is not null ? CultureInfo.GetCultureInfo(IsoCode) : null; + + /// + public bool IsDefault + { + get => _isDefaultVariantLanguage; + set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, nameof(IsDefault)); + } + + /// + public bool IsMandatory + { + get => _mandatory; + set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(IsMandatory)); + } + + /// + public int? FallbackLanguageId + { + get => _fallbackLanguageId; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, nameof(FallbackLanguageId)); + } } diff --git a/src/Umbraco.Core/Models/Link.cs b/src/Umbraco.Core/Models/Link.cs index 3bfc9c5a0d..7047b54555 100644 --- a/src/Umbraco.Core/Models/Link.cs +++ b/src/Umbraco.Core/Models/Link.cs @@ -1,11 +1,14 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class Link { - public class Link - { - public string? Name { get; set; } - public string? Target { get; set; } - public LinkType Type { get; set; } - public Udi? Udi { get; set; } - public string? Url { get; set; } - } + public string? Name { get; set; } + + public string? Target { get; set; } + + public LinkType Type { get; set; } + + public Udi? Udi { get; set; } + + public string? Url { get; set; } } diff --git a/src/Umbraco.Core/Models/LinkType.cs b/src/Umbraco.Core/Models/LinkType.cs index e4879249d8..5003805043 100644 --- a/src/Umbraco.Core/Models/LinkType.cs +++ b/src/Umbraco.Core/Models/LinkType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public enum LinkType { - public enum LinkType - { - Content, - Media, - External - } + Content, + Media, + External, } diff --git a/src/Umbraco.Core/Models/LogViewerQuery.cs b/src/Umbraco.Core/Models/LogViewerQuery.cs index e9c0dc3180..5941763e24 100644 --- a/src/Umbraco.Core/Models/LogViewerQuery.cs +++ b/src/Umbraco.Core/Models/LogViewerQuery.cs @@ -1,34 +1,32 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class LogViewerQuery : EntityBase, ILogViewerQuery { - [Serializable] - [DataContract(IsReference = true)] - public class LogViewerQuery : EntityBase, ILogViewerQuery + private string? _name; + private string? _query; + + public LogViewerQuery(string? name, string? query) { - private string? _name; - private string? _query; + Name = name; + _query = query; + } - public LogViewerQuery(string? name, string? query) - { - Name = name; - _query = query; - } + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - [DataMember] - public string? Query - { - get => _query; - set => SetPropertyValueAndDetectChanges(value, ref _query, nameof(Query)); - } + [DataMember] + public string? Query + { + get => _query; + set => SetPropertyValueAndDetectChanges(value, ref _query, nameof(Query)); } } diff --git a/src/Umbraco.Core/Models/Macro.cs b/src/Umbraco.Core/Models/Macro.cs index 1e395c2158..ea03750e32 100644 --- a/src/Umbraco.Core/Models/Macro.cs +++ b/src/Umbraco.Core/Models/Macro.cs @@ -1,275 +1,287 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Macro +/// +[Serializable] +[DataContract(IsReference = true)] +public class Macro : EntityBase, IMacro { - /// - /// Represents a Macro - /// - [Serializable] - [DataContract(IsReference = true)] - public class Macro : EntityBase, IMacro + private readonly IShortStringHelper _shortStringHelper; + private List _addedProperties; + + private string _alias; + private bool _cacheByMember; + private bool _cacheByPage; + private int _cacheDuration; + private bool _dontRender; + private string _macroSource; + private string? _name; + private List _removedProperties; + private bool _useInEditor; + + public Macro(IShortStringHelper shortStringHelper) { - private readonly IShortStringHelper _shortStringHelper; + _alias = string.Empty; + _shortStringHelper = shortStringHelper; + Properties = new MacroPropertyCollection(); + Properties.CollectionChanged += PropertiesChanged; + _addedProperties = new List(); + _removedProperties = new List(); + _macroSource = string.Empty; + } - public Macro(IShortStringHelper shortStringHelper) + /// + /// Creates an item with pre-filled properties + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Macro( + IShortStringHelper shortStringHelper, + int id, + Guid key, + bool useInEditor, + int cacheDuration, + string alias, + string? name, + bool cacheByPage, + bool cacheByMember, + bool dontRender, + string macroSource) + : this(shortStringHelper) + { + Id = id; + Key = key; + UseInEditor = useInEditor; + CacheDuration = cacheDuration; + Alias = alias.ToCleanString(shortStringHelper, CleanStringType.Alias); + Name = name; + CacheByPage = cacheByPage; + CacheByMember = cacheByMember; + DontRender = dontRender; + MacroSource = macroSource; + } + + /// + /// Creates an instance for persisting a new item + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public Macro( + IShortStringHelper shortStringHelper, + string alias, + string? name, + string macroSource, + bool cacheByPage = false, + bool cacheByMember = false, + bool dontRender = true, + bool useInEditor = false, + int cacheDuration = 0) + : this(shortStringHelper) + { + UseInEditor = useInEditor; + CacheDuration = cacheDuration; + Alias = alias.ToCleanString(shortStringHelper, CleanStringType.Alias); + Name = name; + CacheByPage = cacheByPage; + CacheByMember = cacheByMember; + DontRender = dontRender; + MacroSource = macroSource; + } + + /// + /// Gets or sets the alias of the Macro + /// + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges( + value.ToCleanString(_shortStringHelper, CleanStringType.Alias), + ref _alias!, + nameof(Alias)); + } + + /// + /// Used internally to check if we need to add a section in the repository to the db + /// + internal IEnumerable AddedProperties => _addedProperties; + + /// + /// Used internally to check if we need to remove a section in the repository to the db + /// + internal IEnumerable RemovedProperties => _removedProperties; + + public override void ResetDirtyProperties(bool rememberDirty) + { + base.ResetDirtyProperties(rememberDirty); + + _addedProperties.Clear(); + _removedProperties.Clear(); + + foreach (IMacroProperty prop in Properties) { - _alias = string.Empty; - _shortStringHelper = shortStringHelper; - _properties = new MacroPropertyCollection(); - _properties.CollectionChanged += PropertiesChanged; - _addedProperties = new List(); - _removedProperties = new List(); - _macroSource = string.Empty; - } - - /// - /// Creates an item with pre-filled properties - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public Macro(IShortStringHelper shortStringHelper, int id, Guid key, bool useInEditor, int cacheDuration, string @alias, string? name, bool cacheByPage, bool cacheByMember, bool dontRender, string macroSource) - : this(shortStringHelper) - { - Id = id; - Key = key; - UseInEditor = useInEditor; - CacheDuration = cacheDuration; - Alias = alias.ToCleanString(shortStringHelper,CleanStringType.Alias); - Name = name; - CacheByPage = cacheByPage; - CacheByMember = cacheByMember; - DontRender = dontRender; - MacroSource = macroSource; - } - - /// - /// Creates an instance for persisting a new item - /// - /// - /// - /// - /// - /// - /// - /// - /// - public Macro(IShortStringHelper shortStringHelper, string @alias, string? name, - string macroSource, - bool cacheByPage = false, - bool cacheByMember = false, - bool dontRender = true, - bool useInEditor = false, - int cacheDuration = 0) - : this(shortStringHelper) - { - UseInEditor = useInEditor; - CacheDuration = cacheDuration; - Alias = alias.ToCleanString(shortStringHelper, CleanStringType.Alias); - Name = name; - CacheByPage = cacheByPage; - CacheByMember = cacheByMember; - DontRender = dontRender; - MacroSource = macroSource; - } - - private string _alias; - private string? _name; - private bool _useInEditor; - private int _cacheDuration; - private bool _cacheByPage; - private bool _cacheByMember; - private bool _dontRender; - private string _macroSource; - private MacroPropertyCollection _properties; - private List _addedProperties; - private List _removedProperties; - - void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(Properties)); - - if (e.Action == NotifyCollectionChangedAction.Add) - { - //listen for changes - MacroProperty? prop = e.NewItems?.Cast().First(); - if (prop is not null) - { - prop.PropertyChanged += PropertyDataChanged; - - var alias = prop.Alias; - - if (_addedProperties.Contains(alias) == false) - { - //add to the added props - _addedProperties.Add(alias); - } - } - } - else if (e.Action == NotifyCollectionChangedAction.Remove) - { - //remove listening for changes - var prop = e.OldItems?.Cast().First(); - if (prop is not null) - { - prop.PropertyChanged -= PropertyDataChanged; - - var alias = prop.Alias; - - if (_removedProperties.Contains(alias) == false) - { - _removedProperties.Add(alias); - } - } - } - } - - /// - /// When some data of a property has changed ensure our Properties flag is dirty - /// - /// - /// - void PropertyDataChanged(object? sender, PropertyChangedEventArgs e) - { - OnPropertyChanged(nameof(Properties)); - } - - public override void ResetDirtyProperties(bool rememberDirty) - { - base.ResetDirtyProperties(rememberDirty); - - _addedProperties.Clear(); - _removedProperties.Clear(); - - foreach (var prop in Properties) - { - prop.ResetDirtyProperties(rememberDirty); - } - } - - /// - /// Used internally to check if we need to add a section in the repository to the db - /// - internal IEnumerable AddedProperties => _addedProperties; - - /// - /// Used internally to check if we need to remove a section in the repository to the db - /// - internal IEnumerable RemovedProperties => _removedProperties; - - /// - /// Gets or sets the alias of the Macro - /// - [DataMember] - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value.ToCleanString(_shortStringHelper, CleanStringType.Alias), ref _alias!, nameof(Alias)); - } - - /// - /// Gets or sets the name of the Macro - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - /// - /// Gets or sets a boolean indicating whether the Macro can be used in an Editor - /// - [DataMember] - public bool UseInEditor - { - get => _useInEditor; - set => SetPropertyValueAndDetectChanges(value, ref _useInEditor, nameof(UseInEditor)); - } - - /// - /// Gets or sets the Cache Duration for the Macro - /// - [DataMember] - public int CacheDuration - { - get => _cacheDuration; - set => SetPropertyValueAndDetectChanges(value, ref _cacheDuration, nameof(CacheDuration)); - } - - /// - /// Gets or sets a boolean indicating whether the Macro should be Cached by Page - /// - [DataMember] - public bool CacheByPage - { - get => _cacheByPage; - set => SetPropertyValueAndDetectChanges(value, ref _cacheByPage, nameof(CacheByPage)); - } - - /// - /// Gets or sets a boolean indicating whether the Macro should be Cached Personally - /// - [DataMember] - public bool CacheByMember - { - get => _cacheByMember; - set => SetPropertyValueAndDetectChanges(value, ref _cacheByMember, nameof(CacheByMember)); - } - - /// - /// Gets or sets a boolean indicating whether the Macro should be rendered in an Editor - /// - [DataMember] - public bool DontRender - { - get => _dontRender; - set => SetPropertyValueAndDetectChanges(value, ref _dontRender, nameof(DontRender)); - } - - /// - /// Gets or set the path to the Partial View to render - /// - [DataMember] - public string MacroSource - { - get => _macroSource; - set => SetPropertyValueAndDetectChanges(value, ref _macroSource!, nameof(MacroSource)); - } - - /// - /// Gets or sets a list of Macro Properties - /// - [DataMember] - public MacroPropertyCollection Properties => _properties; - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (Macro)clone; - - clonedEntity._addedProperties = new List(); - clonedEntity._removedProperties = new List(); - clonedEntity._properties = (MacroPropertyCollection)Properties.DeepClone(); - //re-assign the event handler - clonedEntity._properties.CollectionChanged += clonedEntity.PropertiesChanged; - + prop.ResetDirtyProperties(rememberDirty); } } + + /// + /// Gets or sets the name of the Macro + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets a boolean indicating whether the Macro can be used in an Editor + /// + [DataMember] + public bool UseInEditor + { + get => _useInEditor; + set => SetPropertyValueAndDetectChanges(value, ref _useInEditor, nameof(UseInEditor)); + } + + /// + /// Gets or sets the Cache Duration for the Macro + /// + [DataMember] + public int CacheDuration + { + get => _cacheDuration; + set => SetPropertyValueAndDetectChanges(value, ref _cacheDuration, nameof(CacheDuration)); + } + + /// + /// Gets or sets a boolean indicating whether the Macro should be Cached by Page + /// + [DataMember] + public bool CacheByPage + { + get => _cacheByPage; + set => SetPropertyValueAndDetectChanges(value, ref _cacheByPage, nameof(CacheByPage)); + } + + /// + /// Gets or sets a boolean indicating whether the Macro should be Cached Personally + /// + [DataMember] + public bool CacheByMember + { + get => _cacheByMember; + set => SetPropertyValueAndDetectChanges(value, ref _cacheByMember, nameof(CacheByMember)); + } + + /// + /// Gets or sets a boolean indicating whether the Macro should be rendered in an Editor + /// + [DataMember] + public bool DontRender + { + get => _dontRender; + set => SetPropertyValueAndDetectChanges(value, ref _dontRender, nameof(DontRender)); + } + + /// + /// Gets or set the path to the Partial View to render + /// + [DataMember] + public string MacroSource + { + get => _macroSource; + set => SetPropertyValueAndDetectChanges(value, ref _macroSource!, nameof(MacroSource)); + } + + /// + /// Gets or sets a list of Macro Properties + /// + [DataMember] + public MacroPropertyCollection Properties { get; private set; } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (Macro)clone; + + clonedEntity._addedProperties = new List(); + clonedEntity._removedProperties = new List(); + clonedEntity.Properties = (MacroPropertyCollection)Properties.DeepClone(); + + // re-assign the event handler + clonedEntity.Properties.CollectionChanged += clonedEntity.PropertiesChanged; + } + + private void PropertiesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(Properties)); + + if (e.Action == NotifyCollectionChangedAction.Add) + { + // listen for changes + MacroProperty? prop = e.NewItems?.Cast().First(); + if (prop is not null) + { + prop.PropertyChanged += PropertyDataChanged; + + var alias = prop.Alias; + + if (_addedProperties.Contains(alias) == false) + { + // add to the added props + _addedProperties.Add(alias); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + // remove listening for changes + MacroProperty? prop = e.OldItems?.Cast().First(); + if (prop is not null) + { + prop.PropertyChanged -= PropertyDataChanged; + + var alias = prop.Alias; + + if (_removedProperties.Contains(alias) == false) + { + _removedProperties.Add(alias); + } + } + } + } + + /// + /// When some data of a property has changed ensure our Properties flag is dirty + /// + /// + /// + private void PropertyDataChanged(object? sender, PropertyChangedEventArgs e) => + OnPropertyChanged(nameof(Properties)); } diff --git a/src/Umbraco.Core/Models/MacroProperty.cs b/src/Umbraco.Core/Models/MacroProperty.cs index 659334258e..2a6f041fc0 100644 --- a/src/Umbraco.Core/Models/MacroProperty.cs +++ b/src/Umbraco.Core/Models/MacroProperty.cs @@ -1,159 +1,168 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Macro Property +/// +[Serializable] +[DataContract(IsReference = true)] +public class MacroProperty : BeingDirtyBase, IMacroProperty { - /// - /// Represents a Macro Property - /// - [Serializable] - [DataContract(IsReference = true)] - public class MacroProperty : BeingDirtyBase, IMacroProperty + private string _alias; + private string _editorAlias; + private int _id; + + private Guid _key; + private string? _name; + private int _sortOrder; + + public MacroProperty() { - public MacroProperty() + _editorAlias = string.Empty; + _alias = string.Empty; + _key = Guid.NewGuid(); + } + + /// + /// Ctor for creating a new property + /// + /// + /// + /// + /// + public MacroProperty(string alias, string? name, int sortOrder, string editorAlias) + { + _alias = alias; + _name = name; + _sortOrder = sortOrder; + _key = Guid.NewGuid(); + _editorAlias = editorAlias; + } + + /// + /// Ctor for creating an existing property + /// + /// + /// + /// + /// + /// + /// + public MacroProperty(int id, Guid key, string alias, string? name, int sortOrder, string editorAlias) + { + _id = id; + _alias = alias; + _name = name; + _sortOrder = sortOrder; + _key = key; + _editorAlias = editorAlias; + } + + /// + /// Gets or sets the Key of the Property + /// + [DataMember] + public Guid Key + { + get => _key; + set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); + } + + /// + /// Gets or sets the Alias of the Property + /// + [DataMember] + public int Id + { + get => _id; + set => SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); + } + + /// + /// Gets or sets the Alias of the Property + /// + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + } + + /// + /// Gets or sets the Name of the Property + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets the Sort Order of the Property + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } + + /// + /// Gets or sets the Type for this Property + /// + /// + /// The MacroPropertyTypes acts as a plugin for Macros. + /// All types was previously contained in the database, but has been ported to code. + /// + [DataMember] + public string EditorAlias + { + get => _editorAlias; + set => SetPropertyValueAndDetectChanges(value, ref _editorAlias!, nameof(EditorAlias)); + } + + public object DeepClone() + { + // Memberwise clone on MacroProperty will work since it doesn't have any deep elements + // for any sub class this will work for standard properties as well that aren't complex object's themselves. + var clone = (MacroProperty)MemberwiseClone(); + + // Automatically deep clone ref properties that are IDeepCloneable + DeepCloneHelper.DeepCloneRefProperties(this, clone); + clone.ResetDirtyProperties(false); + return clone; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - _editorAlias = string.Empty; - _alias = string.Empty; - _key = Guid.NewGuid(); + return false; } - /// - /// Ctor for creating a new property - /// - /// - /// - /// - /// - public MacroProperty(string @alias, string? name, int sortOrder, string editorAlias) + if (ReferenceEquals(this, obj)) { - _alias = alias; - _name = name; - _sortOrder = sortOrder; - _key = Guid.NewGuid(); - _editorAlias = editorAlias; + return true; } - /// - /// Ctor for creating an existing property - /// - /// - /// - /// - /// - /// - /// - public MacroProperty(int id, Guid key, string @alias, string? name, int sortOrder, string editorAlias) + if (obj.GetType() != GetType()) { - _id = id; - _alias = alias; - _name = name; - _sortOrder = sortOrder; - _key = key; - _editorAlias = editorAlias; + return false; } - private Guid _key; - private string _alias; - private string? _name; - private int _sortOrder; - private int _id; - private string _editorAlias; + return Equals((MacroProperty)obj); + } - /// - /// Gets or sets the Key of the Property - /// - [DataMember] - public Guid Key - { - get => _key; - set => SetPropertyValueAndDetectChanges(value, ref _key, nameof(Key)); - } + protected bool Equals(MacroProperty other) => string.Equals(_alias, other._alias) && _id == other._id; - /// - /// Gets or sets the Alias of the Property - /// - [DataMember] - public int Id + public override int GetHashCode() + { + unchecked { - get => _id; - set => SetPropertyValueAndDetectChanges(value, ref _id, nameof(Id)); - } - - /// - /// Gets or sets the Alias of the Property - /// - [DataMember] - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); - } - - /// - /// Gets or sets the Name of the Property - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - /// - /// Gets or sets the Sort Order of the Property - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } - - /// - /// Gets or sets the Type for this Property - /// - /// - /// The MacroPropertyTypes acts as a plugin for Macros. - /// All types was previously contained in the database, but has been ported to code. - /// - [DataMember] - public string EditorAlias - { - get => _editorAlias; - set => SetPropertyValueAndDetectChanges(value, ref _editorAlias!, nameof(EditorAlias)); - } - - public object DeepClone() - { - //Memberwise clone on MacroProperty will work since it doesn't have any deep elements - // for any sub class this will work for standard properties as well that aren't complex object's themselves. - var clone = (MacroProperty)MemberwiseClone(); - //Automatically deep clone ref properties that are IDeepCloneable - DeepCloneHelper.DeepCloneRefProperties(this, clone); - clone.ResetDirtyProperties(false); - return clone; - } - - protected bool Equals(MacroProperty other) - { - return string.Equals(_alias, other._alias) && _id == other._id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MacroProperty) obj); - } - - public override int GetHashCode() - { - unchecked - { - return ((_alias != null ? _alias.GetHashCode() : 0)*397) ^ _id; - } + return ((_alias != null ? _alias.GetHashCode() : 0) * 397) ^ _id; } } } diff --git a/src/Umbraco.Core/Models/MacroPropertyCollection.cs b/src/Umbraco.Core/Models/MacroPropertyCollection.cs index cda46d2af7..c31cc8cbc1 100644 --- a/src/Umbraco.Core/Models/MacroPropertyCollection.cs +++ b/src/Umbraco.Core/Models/MacroPropertyCollection.cs @@ -1,66 +1,66 @@ -using System; using Umbraco.Cms.Core.Collections; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A macro's property collection +/// +public class MacroPropertyCollection : ObservableDictionary, IDeepCloneable { - /// - /// A macro's property collection - /// - public class MacroPropertyCollection : ObservableDictionary, IDeepCloneable + public MacroPropertyCollection() + : base(property => property.Alias) { - public MacroPropertyCollection() - : base(property => property.Alias) - { - } - - public object DeepClone() - { - var clone = new MacroPropertyCollection(); - foreach (var item in this) - { - clone.Add((IMacroProperty)item.DeepClone()); - } - return clone; - } - - /// - /// Used to update an existing macro property - /// - /// - /// - /// - /// - /// The existing property alias - /// - /// - public void UpdateProperty(string currentAlias, string? name = null, int? sortOrder = null, string? editorAlias = null, string? newAlias = null) - { - var prop = this[currentAlias]; - if (prop == null) - { - throw new InvalidOperationException("No property exists with alias " + currentAlias); - } - - if (name.IsNullOrWhiteSpace() == false) - { - prop.Name = name; - } - if (sortOrder.HasValue) - { - prop.SortOrder = sortOrder.Value; - } - if (name.IsNullOrWhiteSpace() == false && editorAlias is not null) - { - prop.EditorAlias = editorAlias; - } - - if (newAlias.IsNullOrWhiteSpace() == false && currentAlias != newAlias && newAlias is not null) - { - prop.Alias = newAlias; - ChangeKey(currentAlias, newAlias); - } - } } + public object DeepClone() + { + var clone = new MacroPropertyCollection(); + foreach (IMacroProperty item in this) + { + clone.Add((IMacroProperty)item.DeepClone()); + } + + return clone; + } + + /// + /// Used to update an existing macro property + /// + /// + /// + /// + /// + /// The existing property alias + /// + /// + public void UpdateProperty(string currentAlias, string? name = null, int? sortOrder = null, string? editorAlias = null, string? newAlias = null) + { + IMacroProperty prop = this[currentAlias]; + if (prop == null) + { + throw new InvalidOperationException("No property exists with alias " + currentAlias); + } + + if (name.IsNullOrWhiteSpace() == false) + { + prop.Name = name; + } + + if (sortOrder.HasValue) + { + prop.SortOrder = sortOrder.Value; + } + + if (name.IsNullOrWhiteSpace() == false && editorAlias is not null) + { + prop.EditorAlias = editorAlias; + } + + if (newAlias.IsNullOrWhiteSpace() == false && currentAlias != newAlias && newAlias is not null) + { + prop.Alias = newAlias; + ChangeKey(currentAlias, newAlias); + } + } } diff --git a/src/Umbraco.Core/Models/Mapping/AuditMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/AuditMapDefinition.cs index 072611da4c..02095596e7 100644 --- a/src/Umbraco.Core/Models/Mapping/AuditMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/AuditMapDefinition.cs @@ -1,25 +1,22 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.Mapping -{ - public class AuditMapDefinition : IMapDefinition - { - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new AuditLog(), Map); - } +namespace Umbraco.Cms.Core.Models.Mapping; - // Umbraco.Code.MapAll -UserAvatars -UserName - private void Map(IAuditItem source, AuditLog target, MapperContext context) - { - target.UserId = source.UserId; - target.NodeId = source.Id; - target.Timestamp = source.CreateDate; - target.LogType = source.AuditType.ToString(); - target.EntityType = source.EntityType; - target.Comment = source.Comment; - target.Parameters = source.Parameters; - } +public class AuditMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) => + mapper.Define((source, context) => new AuditLog(), Map); + + // Umbraco.Code.MapAll -UserAvatars -UserName + private void Map(IAuditItem source, AuditLog target, MapperContext context) + { + target.UserId = source.UserId; + target.NodeId = source.Id; + target.Timestamp = source.CreateDate; + target.LogType = source.AuditType.ToString(); + target.EntityType = source.EntityType; + target.Comment = source.Comment; + target.Parameters = source.Parameters; } } diff --git a/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs index b185bb586e..e9ba018f9c 100644 --- a/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs @@ -1,100 +1,98 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class CodeFileMapDefinition : IMapDefinition { - public class CodeFileMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) { - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new CodeFileDisplay(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new CodeFileDisplay(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new CodeFileDisplay(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), Map); - mapper.Define(Map); - mapper.Define(Map); + mapper.Define(Map); + mapper.Define(Map); + } - } + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(IStylesheet source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } - // Umbraco.Code.MapAll -Trashed -Udi -Icon - private static void Map(IStylesheet source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - } + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(IScript source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } - // Umbraco.Code.MapAll -Trashed -Udi -Icon - private static void Map(IScript source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - } + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(IPartialView source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } - // Umbraco.Code.MapAll -Trashed -Udi -Icon - private static void Map(IPartialView source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - } + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(IPartialView source, CodeFileDisplay target, MapperContext context) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } - // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet - private static void Map(IPartialView source, CodeFileDisplay target, MapperContext context) - { - target.Content = source.Content; - target.Id = source.Id.ToString(); - target.Name = source.Name; - target.VirtualPath = source.VirtualPath; - } + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(IScript source, CodeFileDisplay target, MapperContext context) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } - // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet - private static void Map(IScript source, CodeFileDisplay target, MapperContext context) - { - target.Content = source.Content; - target.Id = source.Id.ToString(); - target.Name = source.Name; - target.VirtualPath = source.VirtualPath; - } + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet + private static void Map(IStylesheet source, CodeFileDisplay target, MapperContext context) + { + target.Content = source.Content; + target.Id = source.Id.ToString(); + target.Name = source.Name; + target.VirtualPath = source.VirtualPath; + } - // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet - private static void Map(IStylesheet source, CodeFileDisplay target, MapperContext context) - { - target.Content = source.Content; - target.Id = source.Id.ToString(); - target.Name = source.Name; - target.VirtualPath = source.VirtualPath; - } + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate + // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path + private static void Map(CodeFileDisplay source, IPartialView target, MapperContext context) + { + target.Content = source.Content; + target.VirtualPath = source.VirtualPath; + } - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path - private static void Map(CodeFileDisplay source, IPartialView target, MapperContext context) - { - target.Content = source.Content; - target.VirtualPath = source.VirtualPath; - } - - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate -GetFileContent - // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path - private static void Map(CodeFileDisplay source, IScript target, MapperContext context) - { - target.Content = source.Content; - target.VirtualPath = source.VirtualPath; - } + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate -GetFileContent + // Umbraco.Code.MapAll -Id -Key -Alias -Name -OriginalPath -Path + private static void Map(CodeFileDisplay source, IScript target, MapperContext context) + { + target.Content = source.Content; + target.VirtualPath = source.VirtualPath; } } diff --git a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs index 3832654f45..017ac1eb22 100644 --- a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.ContentApps; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; @@ -10,63 +7,62 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; using UserProfile = Umbraco.Cms.Core.Models.ContentEditing.UserProfile; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class CommonMapper { - public class CommonMapper + private readonly ContentAppFactoryCollection _contentAppDefinitions; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly ILocalizedTextService _localizedTextService; + private readonly IUserService _userService; + + public CommonMapper( + IUserService userService, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + ContentAppFactoryCollection contentAppDefinitions, + ILocalizedTextService localizedTextService) { - private readonly IUserService _userService; - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - private readonly ContentAppFactoryCollection _contentAppDefinitions; - private readonly ILocalizedTextService _localizedTextService; + _userService = userService; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _contentAppDefinitions = contentAppDefinitions; + _localizedTextService = localizedTextService; + } - public CommonMapper(IUserService userService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - ContentAppFactoryCollection contentAppDefinitions, ILocalizedTextService localizedTextService) + public UserProfile? GetOwner(IContentBase source, MapperContext context) + { + IProfile? profile = source.GetCreatorProfile(_userService); + return profile == null ? null : context.Map(profile); + } + + public UserProfile? GetCreator(IContent source, MapperContext context) + { + IProfile? profile = source.GetWriterProfile(_userService); + return profile == null ? null : context.Map(profile); + } + + public ContentTypeBasic? GetContentType(IContentBase source, MapperContext context) + { + IContentTypeComposition? contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); + ContentTypeBasic? contentTypeBasic = context.Map(contentType); + return contentTypeBasic; + } + + public IEnumerable GetContentApps(IUmbracoEntity source) => GetContentAppsForEntity(source); + + public IEnumerable GetContentAppsForEntity(IEntity source) + { + ContentApp[] apps = _contentAppDefinitions.GetContentAppsFor(source).ToArray(); + + // localize content app names + foreach (ContentApp app in apps) { - _userService = userService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _contentAppDefinitions = contentAppDefinitions; - _localizedTextService = localizedTextService; - } - - public UserProfile? GetOwner(IContentBase source, MapperContext context) - { - var profile = source.GetCreatorProfile(_userService); - return profile == null ? null : context.Map(profile); - } - - public UserProfile? GetCreator(IContent source, MapperContext context) - { - var profile = source.GetWriterProfile(_userService); - return profile == null ? null : context.Map(profile); - } - - public ContentTypeBasic? GetContentType(IContentBase source, MapperContext context) - { - var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); - var contentTypeBasic = context.Map(contentType); - return contentTypeBasic; - } - - public IEnumerable GetContentApps(IUmbracoEntity source) - { - return GetContentAppsForEntity(source); - } - - public IEnumerable GetContentAppsForEntity(IEntity source) - { - var apps = _contentAppDefinitions.GetContentAppsFor(source).ToArray(); - - // localize content app names - foreach (var app in apps) + var localizedAppName = _localizedTextService.Localize("apps", app.Alias); + if (localizedAppName.Equals($"[{app.Alias}]", StringComparison.OrdinalIgnoreCase) == false) { - var localizedAppName = _localizedTextService.Localize("apps", app.Alias); - if (localizedAppName.Equals($"[{app.Alias}]", StringComparison.OrdinalIgnoreCase) == false) - { - app.Name = localizedAppName; - } + app.Name = localizedAppName; } - - return apps; } + + return apps; } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyBasicMapper.cs index 4becc8f21a..d3502cf887 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyBasicMapper.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; @@ -7,79 +5,91 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Creates a base generic ContentPropertyBasic from a Property +/// +internal class ContentPropertyBasicMapper + where TDestination : ContentPropertyBasic, new() { - /// - /// Creates a base generic ContentPropertyBasic from a Property - /// - internal class ContentPropertyBasicMapper - where TDestination : ContentPropertyBasic, new() + private readonly IEntityService _entityService; + private readonly ILogger> _logger; + private readonly PropertyEditorCollection _propertyEditors; + + public ContentPropertyBasicMapper( + IDataTypeService dataTypeService, + IEntityService entityService, + ILogger> logger, + PropertyEditorCollection propertyEditors) { - private readonly IEntityService _entityService; - private readonly ILogger> _logger; - private readonly PropertyEditorCollection _propertyEditors; - protected IDataTypeService DataTypeService { get; } + _logger = logger; + _propertyEditors = propertyEditors; + DataTypeService = dataTypeService; + _entityService = entityService; + } - public ContentPropertyBasicMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger> logger, PropertyEditorCollection propertyEditors) - { - _logger = logger; - _propertyEditors = propertyEditors; - DataTypeService = dataTypeService; - _entityService = entityService; - } + protected IDataTypeService DataTypeService { get; } - /// - /// Assigns the PropertyEditor, Id, Alias and Value to the property - /// - /// - public virtual void Map(IProperty property, TDestination dest, MapperContext context) + /// + /// Assigns the PropertyEditor, Id, Alias and Value to the property + /// + /// + public virtual void Map(IProperty property, TDestination dest, MapperContext context) + { + IDataEditor? editor = property.PropertyType is not null ? _propertyEditors[property.PropertyType.PropertyEditorAlias] : null; + if (editor == null) { - var editor = property.PropertyType is not null ? _propertyEditors[property.PropertyType.PropertyEditorAlias] : null; + _logger.LogError( + new NullReferenceException("The property editor with alias " + + property.PropertyType?.PropertyEditorAlias + " does not exist"), + "No property editor '{PropertyEditorAlias}' found, converting to a Label", + property.PropertyType?.PropertyEditorAlias); + + editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label]; + if (editor == null) { - _logger.LogError( - new NullReferenceException("The property editor with alias " + property.PropertyType?.PropertyEditorAlias + " does not exist"), - "No property editor '{PropertyEditorAlias}' found, converting to a Label", - property.PropertyType?.PropertyEditorAlias); - - editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label]; - - if (editor == null) - throw new InvalidOperationException($"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}"); + throw new InvalidOperationException( + $"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}"); } - - dest.Id = property.Id; - dest.Alias = property.Alias; - dest.PropertyEditor = editor; - dest.Editor = editor.Alias; - dest.DataTypeKey = property.PropertyType!.DataTypeKey; - - // if there's a set of property aliases specified, we will check if the current property's value should be mapped. - // if it isn't one of the ones specified in 'includeProperties', we will just return the result without mapping the Value. - var includedProperties = context.GetIncludedProperties(); - if (includedProperties != null && !includedProperties.Contains(property.Alias)) - return; - - //Get the culture from the context which will be set during the mapping operation for each property - var culture = context.GetCulture(); - - //a culture needs to be in the context for a property type that can vary - if (culture == null && property.PropertyType.VariesByCulture()) - throw new InvalidOperationException($"No culture found in mapping operation when one is required for the culture variant property type {property.PropertyType.Alias}"); - - //set the culture to null if it's an invariant property type - culture = !property.PropertyType.VariesByCulture() ? null : culture; - - dest.Culture = culture; - - // Get the segment, which is always allowed to be null even if the propertyType *can* be varied by segment. - // There is therefore no need to perform the null check like with culture above. - var segment = !property.PropertyType.VariesBySegment() ? null : context.GetSegment(); - dest.Segment = segment; - - // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. - dest.Value = editor.GetValueEditor().ToEditor(property, culture, segment); - } + + dest.Id = property.Id; + dest.Alias = property.Alias; + dest.PropertyEditor = editor; + dest.Editor = editor.Alias; + dest.DataTypeKey = property.PropertyType!.DataTypeKey; + + // if there's a set of property aliases specified, we will check if the current property's value should be mapped. + // if it isn't one of the ones specified in 'includeProperties', we will just return the result without mapping the Value. + var includedProperties = context.GetIncludedProperties(); + if (includedProperties != null && !includedProperties.Contains(property.Alias)) + { + return; + } + + // Get the culture from the context which will be set during the mapping operation for each property + var culture = context.GetCulture(); + + // a culture needs to be in the context for a property type that can vary + if (culture == null && property.PropertyType.VariesByCulture()) + { + throw new InvalidOperationException( + $"No culture found in mapping operation when one is required for the culture variant property type {property.PropertyType.Alias}"); + } + + // set the culture to null if it's an invariant property type + culture = !property.PropertyType.VariesByCulture() ? null : culture; + + dest.Culture = culture; + + // Get the segment, which is always allowed to be null even if the propertyType *can* be varied by segment. + // There is therefore no need to perform the null check like with culture above. + var segment = !property.PropertyType.VariesBySegment() ? null : context.GetSegment(); + dest.Segment = segment; + + // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. + dest.Value = editor.GetValueEditor().ToEditor(property, culture, segment); } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs index a31d9e9c27..eb6c6d92e0 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Microsoft.Extensions.Logging; @@ -9,67 +9,75 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Creates a ContentPropertyDisplay from a Property +/// +internal class ContentPropertyDisplayMapper : ContentPropertyBasicMapper { - /// - /// Creates a ContentPropertyDisplay from a Property - /// - internal class ContentPropertyDisplayMapper : ContentPropertyBasicMapper + private readonly ICultureDictionary _cultureDictionary; + private readonly ILocalizedTextService _textService; + + public ContentPropertyDisplayMapper( + ICultureDictionary cultureDictionary, + IDataTypeService dataTypeService, + IEntityService entityService, + ILocalizedTextService textService, + ILogger logger, + PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) { - private readonly ICultureDictionary _cultureDictionary; - private readonly ILocalizedTextService _textService; + _cultureDictionary = cultureDictionary; + _textService = textService; + } - public ContentPropertyDisplayMapper(ICultureDictionary cultureDictionary, IDataTypeService dataTypeService, IEntityService entityService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) - : base(dataTypeService, entityService, logger, propertyEditors) + public override void Map(IProperty originalProp, ContentPropertyDisplay dest, MapperContext context) + { + base.Map(originalProp, dest, context); + + var config = originalProp.PropertyType is null + ? null + : DataTypeService.GetDataType(originalProp.PropertyType.DataTypeId)?.Configuration; + + // TODO: IDataValueEditor configuration - general issue + // GetValueEditor() returns a non-configured IDataValueEditor + // - for richtext and nested, configuration determines HideLabel, so we need to configure the value editor + // - could configuration also determines ValueType, everywhere? + // - does it make any sense to use a IDataValueEditor without configuring it? + + // configure the editor for display with configuration + IDataValueEditor? valEditor = dest.PropertyEditor?.GetValueEditor(config); + + // set the display properties after mapping + dest.Alias = originalProp.Alias; + dest.Description = originalProp.PropertyType?.Description; + dest.Label = originalProp.PropertyType?.Name; + dest.HideLabel = valEditor?.HideLabel ?? false; + dest.LabelOnTop = originalProp.PropertyType?.LabelOnTop; + + // add the validation information + dest.Validation.Mandatory = originalProp.PropertyType?.Mandatory ?? false; + dest.Validation.MandatoryMessage = originalProp.PropertyType?.MandatoryMessage; + dest.Validation.Pattern = originalProp.PropertyType?.ValidationRegExp; + dest.Validation.PatternMessage = originalProp.PropertyType?.ValidationRegExpMessage; + + if (dest.PropertyEditor == null) { - _cultureDictionary = cultureDictionary; - _textService = textService; + // display.Config = PreValueCollection.AsDictionary(preVals); + // if there is no property editor it means that it is a legacy data type + // we cannot support editing with that so we'll just render the readonly value view. + dest.View = "views/propertyeditors/readonlyvalue/readonlyvalue.html"; } - public override void Map(IProperty originalProp, ContentPropertyDisplay dest, MapperContext context) + else { - base.Map(originalProp, dest, context); - - var config = originalProp.PropertyType is null ? null : DataTypeService.GetDataType(originalProp.PropertyType.DataTypeId)?.Configuration; - - // TODO: IDataValueEditor configuration - general issue - // GetValueEditor() returns a non-configured IDataValueEditor - // - for richtext and nested, configuration determines HideLabel, so we need to configure the value editor - // - could configuration also determines ValueType, everywhere? - // - does it make any sense to use a IDataValueEditor without configuring it? - - // configure the editor for display with configuration - var valEditor = dest.PropertyEditor?.GetValueEditor(config); - - //set the display properties after mapping - dest.Alias = originalProp.Alias; - dest.Description = originalProp.PropertyType?.Description; - dest.Label = originalProp.PropertyType?.Name; - dest.HideLabel = valEditor?.HideLabel ?? false; - dest.LabelOnTop = originalProp.PropertyType?.LabelOnTop; - - //add the validation information - dest.Validation.Mandatory = originalProp.PropertyType?.Mandatory ?? false; - dest.Validation.MandatoryMessage = originalProp.PropertyType?.MandatoryMessage; - dest.Validation.Pattern = originalProp.PropertyType?.ValidationRegExp; - dest.Validation.PatternMessage = originalProp.PropertyType?.ValidationRegExpMessage; - - if (dest.PropertyEditor == null) - { - //display.Config = PreValueCollection.AsDictionary(preVals); - //if there is no property editor it means that it is a legacy data type - // we cannot support editing with that so we'll just render the readonly value view. - dest.View = "views/propertyeditors/readonlyvalue/readonlyvalue.html"; - } - else - { - //let the property editor format the pre-values - dest.Config = dest.PropertyEditor.GetConfigurationEditor().ToValueEditor(config); - dest.View = valEditor?.View; - } - - //Translate - dest.Label = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Label); - dest.Description = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Description); + // let the property editor format the pre-values + dest.Config = dest.PropertyEditor.GetConfigurationEditor().ToValueEditor(config); + dest.View = valEditor?.View; } + + // Translate + dest.Label = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Label); + dest.Description = _textService.UmbracoDictionaryTranslate(_cultureDictionary, dest.Description); } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs index fe1eff99ca..5836317b5c 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDtoMapper.cs @@ -1,32 +1,32 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Creates a ContentPropertyDto from a Property +/// +internal class ContentPropertyDtoMapper : ContentPropertyBasicMapper { - /// - /// Creates a ContentPropertyDto from a Property - /// - internal class ContentPropertyDtoMapper : ContentPropertyBasicMapper + public ContentPropertyDtoMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger logger, PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) { - public ContentPropertyDtoMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger logger, PropertyEditorCollection propertyEditors) - : base(dataTypeService, entityService, logger, propertyEditors) - { } + } - public override void Map(IProperty property, ContentPropertyDto dest, MapperContext context) - { - base.Map(property, dest, context); + public override void Map(IProperty property, ContentPropertyDto dest, MapperContext context) + { + base.Map(property, dest, context); - dest.IsRequired = property.PropertyType?.Mandatory; - dest.IsRequiredMessage = property.PropertyType?.MandatoryMessage; - dest.ValidationRegExp = property.PropertyType?.ValidationRegExp; - dest.ValidationRegExpMessage = property.PropertyType?.ValidationRegExpMessage; - dest.Description = property.PropertyType?.Description; - dest.Label = property.PropertyType?.Name; - dest.DataType = property.PropertyType is null ? null : DataTypeService.GetDataType(property.PropertyType.DataTypeId); - dest.LabelOnTop = property.PropertyType?.LabelOnTop; - } + dest.IsRequired = property.PropertyType.Mandatory; + dest.IsRequiredMessage = property.PropertyType.MandatoryMessage; + dest.ValidationRegExp = property.PropertyType.ValidationRegExp; + dest.ValidationRegExpMessage = property.PropertyType.ValidationRegExpMessage; + dest.Description = property.PropertyType.Description; + dest.Label = property.PropertyType.Name; + dest.DataType = DataTypeService.GetDataType(property.PropertyType.DataTypeId); + dest.LabelOnTop = property.PropertyType.LabelOnTop; } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs index 270d821380..1e27389ebf 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs @@ -5,60 +5,78 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// A mapper which declares how to map content properties. These mappings are shared among media (and probably members) +/// which is +/// why they are in their own mapper +/// +public class ContentPropertyMapDefinition : IMapDefinition { - /// - /// A mapper which declares how to map content properties. These mappings are shared among media (and probably members) which is - /// why they are in their own mapper - /// - public class ContentPropertyMapDefinition : IMapDefinition + private readonly ContentPropertyBasicMapper _contentPropertyBasicConverter; + private readonly ContentPropertyDisplayMapper _contentPropertyDisplayMapper; + private readonly ContentPropertyDtoMapper _contentPropertyDtoConverter; + + public ContentPropertyMapDefinition( + ICultureDictionary cultureDictionary, + IDataTypeService dataTypeService, + IEntityService entityService, + ILocalizedTextService textService, + ILoggerFactory loggerFactory, + PropertyEditorCollection propertyEditors) { - private readonly ContentPropertyBasicMapper _contentPropertyBasicConverter; - private readonly ContentPropertyDtoMapper _contentPropertyDtoConverter; - private readonly ContentPropertyDisplayMapper _contentPropertyDisplayMapper; - - public ContentPropertyMapDefinition(ICultureDictionary cultureDictionary, IDataTypeService dataTypeService, IEntityService entityService, ILocalizedTextService textService, ILoggerFactory loggerFactory, PropertyEditorCollection propertyEditors) - { - _contentPropertyBasicConverter = new ContentPropertyBasicMapper(dataTypeService, entityService, loggerFactory.CreateLogger>(), propertyEditors); - _contentPropertyDtoConverter = new ContentPropertyDtoMapper(dataTypeService, entityService, loggerFactory.CreateLogger(), propertyEditors); - _contentPropertyDisplayMapper = new ContentPropertyDisplayMapper(cultureDictionary, dataTypeService, entityService, textService, loggerFactory.CreateLogger(), propertyEditors); - } - - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define>((source, context) => new Tab(), Map); - mapper.Define((source, context) => new ContentPropertyBasic(), Map); - mapper.Define((source, context) => new ContentPropertyDto(), Map); - mapper.Define((source, context) => new ContentPropertyDisplay(), Map); - } - - // Umbraco.Code.MapAll -Properties -Alias -Expanded - private void Map(PropertyGroup source, Tab target, MapperContext mapper) - { - target.Id = source.Id; - target.Key = source.Key; - target.Type = source.Type.ToString(); - target.Label = source.Name; - target.Alias = source.Alias; - target.IsActive = true; - } - - private void Map(IProperty source, ContentPropertyBasic target, MapperContext context) - { - // assume this is mapping everything and no MapAll is required - _contentPropertyBasicConverter.Map(source, target, context); - } - - private void Map(IProperty source, ContentPropertyDto target, MapperContext context) - { - // assume this is mapping everything and no MapAll is required - _contentPropertyDtoConverter.Map(source, target, context); - } - - private void Map(IProperty source, ContentPropertyDisplay target, MapperContext context) - { - // assume this is mapping everything and no MapAll is required - _contentPropertyDisplayMapper.Map(source, target, context); - } + _contentPropertyBasicConverter = new ContentPropertyBasicMapper( + dataTypeService, + entityService, + loggerFactory.CreateLogger>(), + propertyEditors); + _contentPropertyDtoConverter = new ContentPropertyDtoMapper( + dataTypeService, + entityService, + loggerFactory.CreateLogger(), + propertyEditors); + _contentPropertyDisplayMapper = new ContentPropertyDisplayMapper( + cultureDictionary, + dataTypeService, + entityService, + textService, + loggerFactory.CreateLogger(), + propertyEditors); } + + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define>( + (source, context) => new Tab(), Map); + mapper.Define((source, context) => new ContentPropertyBasic(), Map); + mapper.Define((source, context) => new ContentPropertyDto(), Map); + mapper.Define((source, context) => new ContentPropertyDisplay(), Map); + } + + // Umbraco.Code.MapAll -Properties -Alias -Expanded + private void Map(PropertyGroup source, Tab target, MapperContext mapper) + { + target.Id = source.Id; + target.Key = source.Key; + target.Type = source.Type.ToString(); + target.Label = source.Name; + target.Alias = source.Alias; + target.IsActive = true; + } + + private void Map(IProperty source, ContentPropertyBasic target, MapperContext context) => + + // assume this is mapping everything and no MapAll is required + _contentPropertyBasicConverter.Map(source, target, context); + + private void Map(IProperty source, ContentPropertyDto target, MapperContext context) => + + // assume this is mapping everything and no MapAll is required + _contentPropertyDtoConverter.Map(source, target, context); + + private void Map(IProperty source, ContentPropertyDisplay target, MapperContext context) => + + // assume this is mapping everything and no MapAll is required + _contentPropertyDisplayMapper.Map(source, target, context); } diff --git a/src/Umbraco.Core/Models/Mapping/ContentSavedStateMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentSavedStateMapper.cs index a087ce0d3e..ba06dae711 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentSavedStateMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentSavedStateMapper.cs @@ -1,76 +1,83 @@ -using System; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Returns the for an item +/// +/// +public class ContentBasicSavedStateMapper + where T : ContentPropertyBasic { - /// - /// Returns the for an item - /// - /// - public class ContentBasicSavedStateMapper - where T : ContentPropertyBasic + private readonly ContentSavedStateMapper _inner = new(); + + public ContentSavedState? Map(IContent source, MapperContext context) => _inner.Map(source, context); +} + +/// +/// Returns the for an item +/// +/// +public class ContentSavedStateMapper + where T : ContentPropertyBasic +{ + public ContentSavedState Map(IContent source, MapperContext context) { - private readonly ContentSavedStateMapper _inner = new ContentSavedStateMapper(); + PublishedState publishedState; + bool isEdited; + bool isCreated; - public ContentSavedState? Map(IContent source, MapperContext context) + if (source.ContentType.VariesByCulture()) { - return _inner.Map(source, context); - } - } + // Get the culture from the context which will be set during the mapping operation for each variant + var culture = context.GetCulture(); - /// - /// Returns the for an item - /// - /// - public class ContentSavedStateMapper - where T : ContentPropertyBasic - { - public ContentSavedState Map(IContent source, MapperContext context) - { - PublishedState publishedState; - bool isEdited; - bool isCreated; - - if (source.ContentType.VariesByCulture()) + // a culture needs to be in the context for a variant content item + if (culture == null) { - //Get the culture from the context which will be set during the mapping operation for each variant - var culture = context.GetCulture(); + throw new InvalidOperationException( + "No culture found in mapping operation when one is required for a culture variant"); + } - //a culture needs to be in the context for a variant content item - if (culture == null) - throw new InvalidOperationException($"No culture found in mapping operation when one is required for a culture variant"); - - publishedState = source.PublishedState == PublishedState.Unpublished //if the entire document is unpublished, then flag every variant as unpublished + publishedState = + source.PublishedState == + PublishedState + .Unpublished // if the entire document is unpublished, then flag every variant as unpublished ? PublishedState.Unpublished : source.IsCulturePublished(culture) ? PublishedState.Published : PublishedState.Unpublished; - isEdited = source.IsCultureEdited(culture); - isCreated = source.Id > 0 && source.IsCultureAvailable(culture); - } - else - { - publishedState = source.PublishedState == PublishedState.Unpublished - ? PublishedState.Unpublished - : PublishedState.Published; - - isEdited = source.Edited; - isCreated = source.Id > 0; - } - - if (!isCreated) - return ContentSavedState.NotCreated; - - if (publishedState == PublishedState.Unpublished) - return ContentSavedState.Draft; - - if (publishedState == PublishedState.Published) - return isEdited ? ContentSavedState.PublishedPendingChanges : ContentSavedState.Published; - - throw new NotSupportedException($"PublishedState {publishedState} is not supported."); + isEdited = source.IsCultureEdited(culture); + isCreated = source.Id > 0 && source.IsCultureAvailable(culture); } + else + { + publishedState = source.PublishedState == PublishedState.Unpublished + ? PublishedState.Unpublished + : PublishedState.Published; + + isEdited = source.Edited; + isCreated = source.Id > 0; + } + + if (!isCreated) + { + return ContentSavedState.NotCreated; + } + + if (publishedState == PublishedState.Unpublished) + { + return ContentSavedState.Draft; + } + + if (publishedState == PublishedState.Published) + { + return isEdited ? ContentSavedState.PublishedPendingChanges : ContentSavedState.Published; + } + + throw new NotSupportedException($"PublishedState {publishedState} is not supported."); } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs index bacab0b7cf..31c2e86b5b 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,899 +13,945 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// Defines mappings for content/media/members type mappings +/// +public class ContentTypeMapDefinition : IMapDefinition { - /// - /// Defines mappings for content/media/members type mappings - /// - public class ContentTypeMapDefinition : IMapDefinition + private readonly CommonMapper _commonMapper; + private readonly IContentTypeService _contentTypeService; + private readonly IDataTypeService _dataTypeService; + private readonly IFileService _fileService; + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IShortStringHelper _shortStringHelper; + private ContentSettings _contentSettings; + + [Obsolete("Use ctor with all params injected")] + public ContentTypeMapDefinition( + CommonMapper commonMapper, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IFileService fileService, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + ILoggerFactory loggerFactory, + IShortStringHelper shortStringHelper, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment) + : this( + commonMapper, + propertyEditors, + dataTypeService, + fileService, + contentTypeService, + mediaTypeService, + memberTypeService, + loggerFactory, + shortStringHelper, + globalSettings, + hostingEnvironment, + StaticServiceProvider.Instance.GetRequiredService>()) { - private readonly CommonMapper _commonMapper; - private readonly IContentTypeService _contentTypeService; - private readonly IDataTypeService _dataTypeService; - private readonly IFileService _fileService; - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IMediaTypeService _mediaTypeService; - private readonly IMemberTypeService _memberTypeService; - private readonly PropertyEditorCollection _propertyEditors; - private readonly IShortStringHelper _shortStringHelper; - private ContentSettings _contentSettings; + } + public ContentTypeMapDefinition( + CommonMapper commonMapper, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IFileService fileService, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + ILoggerFactory loggerFactory, + IShortStringHelper shortStringHelper, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IOptionsMonitor contentSettings) + { + _commonMapper = commonMapper; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _fileService = fileService; + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger(); + _shortStringHelper = shortStringHelper; + _globalSettings = globalSettings.Value; + _hostingEnvironment = hostingEnvironment; - [Obsolete("Use ctor with all params injected")] - public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, IFileService fileService, - IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, - IMemberTypeService memberTypeService, - ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings, - IHostingEnvironment hostingEnvironment) - : this(commonMapper, propertyEditors, dataTypeService, fileService, contentTypeService, mediaTypeService, - memberTypeService, loggerFactory, shortStringHelper, globalSettings, hostingEnvironment, - StaticServiceProvider.Instance.GetRequiredService>()) + _contentSettings = contentSettings.CurrentValue; + contentSettings.OnChange(x => _contentSettings = x); + } + + public static Udi? MapContentTypeUdi(IContentTypeComposition source) + { + if (source == null) { + return null; } - public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, IFileService fileService, - IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, - IMemberTypeService memberTypeService, - ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings, - IHostingEnvironment hostingEnvironment, IOptionsMonitor contentSettings) + string udiType; + switch (source) { - _commonMapper = commonMapper; - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _fileService = fileService; - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _memberTypeService = memberTypeService; - _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(); - _shortStringHelper = shortStringHelper; - _globalSettings = globalSettings.Value; - _hostingEnvironment = hostingEnvironment; - - _contentSettings = contentSettings.CurrentValue; - contentSettings.OnChange(x => _contentSettings = x); + case IMemberType _: + udiType = Constants.UdiEntityType.MemberType; + break; + case IMediaType _: + udiType = Constants.UdiEntityType.MediaType; + break; + case IContentType _: + udiType = Constants.UdiEntityType.DocumentType; + break; + default: + throw new PanicException($"Source is of type {source.GetType()} which isn't supported here"); } - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define( - (source, context) => new ContentType(_shortStringHelper, source.ParentId), Map); - mapper.Define( - (source, context) => new MediaType(_shortStringHelper, source.ParentId), Map); - mapper.Define( - (source, context) => new MemberType(_shortStringHelper, source.ParentId), Map); + return Udi.Create(udiType, source.Key); + } - mapper.Define((source, context) => new DocumentTypeDisplay(), Map); - mapper.Define((source, context) => new MediaTypeDisplay(), Map); - mapper.Define((source, context) => new MemberTypeDisplay(), Map); + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define( + (source, context) => new ContentType(_shortStringHelper, source.ParentId), Map); + mapper.Define( + (source, context) => new MediaType(_shortStringHelper, source.ParentId), Map); + mapper.Define( + (source, context) => new MemberType(_shortStringHelper, source.ParentId), Map); - mapper.Define( - (source, context) => - { - IDataType? dataType = _dataTypeService.GetDataType(source.DataTypeId); - if (dataType == null) - { - throw new NullReferenceException("No data type found with id " + source.DataTypeId); - } + mapper.Define((source, context) => new DocumentTypeDisplay(), Map); + mapper.Define((source, context) => new MediaTypeDisplay(), Map); + mapper.Define((source, context) => new MemberTypeDisplay(), Map); - return new PropertyType(_shortStringHelper, dataType, source.Alias); - }, Map); - - // TODO: isPublishing in ctor? - mapper.Define, PropertyGroup>( - (source, context) => new PropertyGroup(false), Map); - mapper.Define, PropertyGroup>( - (source, context) => new PropertyGroup(false), Map); - - mapper.Define((source, context) => new ContentTypeBasic(), Map); - mapper.Define((source, context) => new ContentTypeBasic(), Map); - mapper.Define((source, context) => new ContentTypeBasic(), Map); - mapper.Define((source, context) => new ContentTypeBasic(), Map); - - mapper.Define((source, context) => new DocumentTypeDisplay(), Map); - mapper.Define((source, context) => new MediaTypeDisplay(), Map); - mapper.Define((source, context) => new MemberTypeDisplay(), Map); - - mapper.Define, PropertyGroupDisplay>( - (source, context) => new PropertyGroupDisplay(), Map); - mapper.Define, PropertyGroupDisplay>( - (source, context) => new PropertyGroupDisplay(), Map); - - mapper.Define((source, context) => new PropertyTypeDisplay(), Map); - mapper.Define( - (source, context) => new MemberPropertyTypeDisplay(), Map); - } - - // no MapAll - take care - private void Map(DocumentTypeSave source, IContentType target, MapperContext context) - { - MapSaveToTypeBase(source, target, context); - MapComposition(source, target, alias => _contentTypeService.Get(alias)); - - if (target is IContentTypeWithHistoryCleanup targetWithHistoryCleanup) + mapper.Define( + (source, context) => { - MapHistoryCleanup(source, targetWithHistoryCleanup); + IDataType? dataType = _dataTypeService.GetDataType(source.DataTypeId); + if (dataType == null) + { + throw new NullReferenceException("No data type found with id " + source.DataTypeId); + } + + return new PropertyType(_shortStringHelper, dataType, source.Alias); + }, + Map); + + // TODO: isPublishing in ctor? + mapper.Define, PropertyGroup>( + (source, context) => new PropertyGroup(false), Map); + mapper.Define, PropertyGroup>( + (source, context) => new PropertyGroup(false), Map); + + mapper.Define((source, context) => new ContentTypeBasic(), Map); + mapper.Define((source, context) => new ContentTypeBasic(), Map); + mapper.Define((source, context) => new ContentTypeBasic(), Map); + mapper.Define((source, context) => new ContentTypeBasic(), Map); + + mapper.Define((source, context) => new DocumentTypeDisplay(), Map); + mapper.Define((source, context) => new MediaTypeDisplay(), Map); + mapper.Define((source, context) => new MemberTypeDisplay(), Map); + + mapper.Define, PropertyGroupDisplay>( + (source, context) => new PropertyGroupDisplay(), Map); + mapper.Define, PropertyGroupDisplay>( + (source, context) => new PropertyGroupDisplay(), Map); + + mapper.Define((source, context) => new PropertyTypeDisplay(), Map); + mapper.Define( + (source, context) => new MemberPropertyTypeDisplay(), Map); + } + + private static void MapHistoryCleanup(DocumentTypeSave source, IContentTypeWithHistoryCleanup target) + { + // If source history cleanup is null we don't have to map all properties + if (source.HistoryCleanup is null) + { + target.HistoryCleanup = null; + return; + } + + // We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties + target.HistoryCleanup!.ResetDirtyProperties(false); + if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup) + { + target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup; + } + + if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays) + { + target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays; + } + + if (target.HistoryCleanup.KeepLatestVersionPerDayForDays != + source.HistoryCleanup.KeepLatestVersionPerDayForDays) + { + target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays; + } + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate + // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations + private static void Map(PropertyTypeBasic source, IPropertyType target, MapperContext context) + { + target.Name = source.Label; + target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; + target.Mandatory = source.Validation?.Mandatory ?? false; + target.MandatoryMessage = source.Validation?.MandatoryMessage; + target.ValidationRegExp = source.Validation?.Pattern; + target.ValidationRegExpMessage = source.Validation?.PatternMessage; + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); + + if (source.Id > 0) + { + target.Id = source.Id; + } + + if (source.GroupId > 0) + { + if (target.PropertyGroupId?.Value != source.GroupId) + { + target.PropertyGroupId = new Lazy(() => source.GroupId, false); + } + } + + target.Alias = source.Alias; + target.Description = source.Description; + target.SortOrder = source.SortOrder; + target.LabelOnTop = source.LabelOnTop; + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes + private static void Map(PropertyGroupBasic source, PropertyGroup target, MapperContext context) + { + if (source.Id > 0) + { + target.Id = source.Id; + } + + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + } + + // no MapAll - take care + private void Map(DocumentTypeSave source, IContentType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _contentTypeService.Get(alias)); + + if (target is IContentTypeWithHistoryCleanup targetWithHistoryCleanup) + { + MapHistoryCleanup(source, targetWithHistoryCleanup); + } + + target.AllowedTemplates = source.AllowedTemplates? + .Where(x => x != null) + .Select(_fileService.GetTemplate) + .WhereNotNull() + .ToArray(); + + target.SetDefaultTemplate(source.DefaultTemplate == null + ? null + : _fileService.GetTemplate(source.DefaultTemplate)); + } + + // no MapAll - take care + private void Map(MediaTypeSave source, IMediaType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _mediaTypeService.Get(alias)); + } + + // no MapAll - take care + private void Map(MemberTypeSave source, IMemberType target, MapperContext context) + { + MapSaveToTypeBase(source, target, context); + MapComposition(source, target, alias => _memberTypeService.Get(alias)); + + foreach (MemberPropertyTypeBasic propertyType in source.Groups.SelectMany(x => x.Properties)) + { + MemberPropertyTypeBasic localCopy = propertyType; + IPropertyType? destProp = + target.PropertyTypes.SingleOrDefault(x => x.Alias?.InvariantEquals(localCopy.Alias) ?? false); + if (destProp == null) + { + continue; } - target.AllowedTemplates = source.AllowedTemplates? - .Where(x => x != null) - .Select(_fileService.GetTemplate) + target.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); + target.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); + target.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData); + } + } + + // no MapAll - take care + private void Map(IContentType source, DocumentTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); + + if (source is IContentTypeWithHistoryCleanup sourceWithHistoryCleanup) + { + target.HistoryCleanup = new HistoryCleanupViewModel + { + PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, + KeepAllVersionsNewerThanDays = + sourceWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = + sourceWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays, + GlobalKeepAllVersionsNewerThanDays = + _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays, + GlobalKeepLatestVersionPerDayForDays = + _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays, + GlobalEnableCleanup = _contentSettings.ContentVersionCleanupPolicy.EnableCleanup, + }; + } + + target.AllowCultureVariant = source.VariesByCulture(); + target.AllowSegmentVariant = source.VariesBySegment(); + target.ContentApps = _commonMapper.GetContentAppsForEntity(source); + + // sync templates + if (source.AllowedTemplates is not null) + { + target.AllowedTemplates = + context.MapEnumerable(source.AllowedTemplates).WhereNotNull(); + } + + if (source.DefaultTemplate != null) + { + target.DefaultTemplate = context.Map(source.DefaultTemplate); + } + + // default listview + target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; + + if (string.IsNullOrEmpty(source.Alias)) + { + return; + } + + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; + if (_dataTypeService.GetDataType(name) != null) + { + target.ListViewEditorName = name; + } + } + + // no MapAll - take care + private void Map(IMediaType source, MediaTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); + + // default listview + target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; + target.IsSystemMediaType = source.IsSystemMediaType(); + + if (string.IsNullOrEmpty(source.Name)) + { + return; + } + + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; + if (_dataTypeService.GetDataType(name) != null) + { + target.ListViewEditorName = name; + } + } + + // no MapAll - take care + private void Map(IMemberType source, MemberTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase(source, target); + + // map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData + foreach (IPropertyType propertyType in source.PropertyTypes) + { + IPropertyType localCopy = propertyType; + MemberPropertyTypeDisplay? displayProp = target.Groups.SelectMany(dest => dest.Properties) + .SingleOrDefault(dest => dest.Alias?.InvariantEquals(localCopy.Alias) ?? false); + if (displayProp == null) + { + continue; + } + + displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias); + displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias); + displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias); + } + } + + // Umbraco.Code.MapAll -Blueprints + private void Map(IContentTypeBase source, ContentTypeBasic target, string entityType) + { + target.Udi = Udi.Create(entityType, source.Key); + target.Alias = source.Alias; + target.CreateDate = source.CreateDate; + target.Description = source.Description; + target.Icon = source.Icon; + target.IconFilePath = target.IconIsClass + ? string.Empty + : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; + + target.Trashed = source.Trashed; + target.Id = source.Id; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Thumbnail = source.Thumbnail; + target.ThumbnailFilePath = target.ThumbnailIsClass + ? string.Empty + : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); + target.UpdateDate = source.UpdateDate; + } + + // no MapAll - uses the IContentTypeBase map method, which has MapAll + private void Map(IContentTypeComposition source, ContentTypeBasic target, MapperContext context) => + Map(source, target, Constants.UdiEntityType.MemberType); + + // no MapAll - uses the IContentTypeBase map method, which has MapAll + private void Map(IContentType source, ContentTypeBasic target, MapperContext context) => + Map(source, target, Constants.UdiEntityType.DocumentType); + + // no MapAll - uses the IContentTypeBase map method, which has MapAll + private void Map(IMediaType source, ContentTypeBasic target, MapperContext context) => + Map(source, target, Constants.UdiEntityType.MediaType); + + // no MapAll - uses the IContentTypeBase map method, which has MapAll + private void Map(IMemberType source, ContentTypeBasic target, MapperContext context) => + Map(source, target, Constants.UdiEntityType.MemberType); + + // no MapAll - take care + private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperContext context) + { + MapTypeToDisplayBase( + source, + target, + context); + + // sync templates + IEnumerable destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias); + + // if the dest is set and it's the same as the source, then don't change + if (source.AllowedTemplates is not null && + destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) + { + IEnumerable? templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray()); + target.AllowedTemplates = source.AllowedTemplates + .Select(x => + { + ITemplate? template = templates?.SingleOrDefault(t => t.Alias == x); + return template != null + ? context.Map(template) + : null; + }) + .WhereNotNull() + .ToArray(); + } + + if (source.DefaultTemplate.IsNullOrWhiteSpace() == false) + { + // if the dest is set and it's the same as the source, then don't change + if (target.DefaultTemplate == null || source.DefaultTemplate != target.DefaultTemplate.Alias) + { + ITemplate? template = _fileService.GetTemplate(source.DefaultTemplate); + target.DefaultTemplate = template == null ? null : context.Map(template); + } + } + else + { + target.DefaultTemplate = null; + } + } + + // no MapAll - take care + private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) => + MapTypeToDisplayBase( + source, + target, + context); + + // no MapAll - take care + private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) => + MapTypeToDisplayBase( + source, target, context); + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes + private static void Map(PropertyGroupBasic source, PropertyGroup target, MapperContext context) + { + if (source.Id > 0) + { + target.Id = source.Id; + } + + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + } + + // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames + private static void Map( + PropertyGroupBasic source, + PropertyGroupDisplay target, + MapperContext context) + { + target.Inherited = source.Inherited; + if (source.Id > 0) + { + target.Id = source.Id; + } + + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + target.Properties = context.MapEnumerable(source.Properties) + .WhereNotNull(); + } + + // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames + private static void Map( + PropertyGroupBasic source, + PropertyGroupDisplay target, + MapperContext context) + { + target.Inherited = source.Inherited; + if (source.Id > 0) + { + target.Id = source.Id; + } + + target.Key = source.Key; + target.Type = source.Type; + target.Name = source.Name; + target.Alias = source.Alias; + target.SortOrder = source.SortOrder; + target.Properties = + context.MapEnumerable(source.Properties).WhereNotNull(); + } + + // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName + private static void Map(PropertyTypeBasic source, PropertyTypeDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; + target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; + target.Description = source.Description; + target.GroupId = source.GroupId; + target.Id = source.Id; + target.Inherited = source.Inherited; + target.Label = source.Label; + target.SortOrder = source.SortOrder; + target.Validation = source.Validation; + target.LabelOnTop = source.LabelOnTop; + } + + // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName + private static void Map(MemberPropertyTypeBasic source, MemberPropertyTypeDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; + target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; + target.Description = source.Description; + target.GroupId = source.GroupId; + target.Id = source.Id; + target.Inherited = source.Inherited; + target.IsSensitiveData = source.IsSensitiveData; + target.Label = source.Label; + target.MemberCanEditProperty = source.MemberCanEditProperty; + target.MemberCanViewProperty = source.MemberCanViewProperty; + target.SortOrder = source.SortOrder; + target.Validation = source.Validation; + target.LabelOnTop = source.LabelOnTop; + } + + // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) + private static void MapSaveToTypeBase( + TSource source, + IContentTypeComposition target, + MapperContext context) + where TSource : ContentTypeSave + where TSourcePropertyType : PropertyTypeBasic + { + // TODO: not so clean really + var isPublishing = target is IContentType; + + var id = Convert.ToInt32(source.Id); + if (id > 0) + { + target.Id = id; + } + + target.Alias = source.Alias; + target.Description = source.Description; + target.Icon = source.Icon; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Thumbnail = source.Thumbnail; + + target.AllowedAsRoot = source.AllowAsRoot; + + var allowedContentTypesUnchanged = target.AllowedContentTypes?.Select(x => x.Id.Value) + .SequenceEqual(source.AllowedContentTypes) ?? false; + + if (allowedContentTypesUnchanged is false) + { + target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); + } + + if (!(target is IMemberType)) + { + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); + } + + // handle property groups and property types + // note that ContentTypeSave has + // - all groups, inherited and local; only *one* occurrence per group *name* + // - potentially including the generic properties group + // - all properties, inherited and local + // + // also, see PropertyTypeGroupResolver.ResolveCore: + // - if a group is local *and* inherited, then Inherited is true + // and the identifier is the identifier of the *local* group + // + // IContentTypeComposition AddPropertyGroup, AddPropertyType methods do some + // unique-alias-checking, etc that is *not* compatible with re-mapping everything + // the way we do it here, so we should exclusively do it by + // - managing a property group's PropertyTypes collection + // - managing the content type's PropertyTypes collection (for generic properties) + + // handle actual groups (non-generic-properties) + PropertyGroup[] destOrigGroups = target.PropertyGroups.ToArray(); // local groups + IPropertyType[] destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not + var destGroups = new List(); + PropertyGroupBasic[] sourceGroups = + source.Groups.Where(x => x.IsGenericProperties == false).ToArray(); + var sourceGroupParentAliases = sourceGroups.Select(x => x.GetParentAlias()).Distinct().ToArray(); + foreach (PropertyGroupBasic sourceGroup in sourceGroups) + { + // get the dest group + PropertyGroup? destGroup = MapSaveGroup(sourceGroup, destOrigGroups, context); + + // handle local properties + IPropertyType[] destProperties = sourceGroup.Properties + .Where(x => x.Inherited == false) + .Select(x => MapSaveProperty(x, destOrigProperties, context)) .WhereNotNull() .ToArray(); - target.SetDefaultTemplate(source.DefaultTemplate == null - ? null - : _fileService.GetTemplate(source.DefaultTemplate)); - } - - private static void MapHistoryCleanup(DocumentTypeSave source, IContentTypeWithHistoryCleanup target) - { - // If source history cleanup is null we don't have to map all properties - if (source.HistoryCleanup is null) + // if the group has no local properties and is not used as parent, skip it, ie sort-of garbage-collect + // local groups which would not have local properties anymore + if (destProperties.Length == 0 && !sourceGroupParentAliases.Contains(sourceGroup.Alias)) { - target.HistoryCleanup = null; - return; + continue; } - // We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties - target.HistoryCleanup!.ResetDirtyProperties(false); - if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup) + // ensure no duplicate alias, then assign the group properties collection + EnsureUniqueAliases(destProperties); + + if (destGroup is not null) { - target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup; - } - - if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays) - { - target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays; - } - - if (target.HistoryCleanup.KeepLatestVersionPerDayForDays != - source.HistoryCleanup.KeepLatestVersionPerDayForDays) - { - target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays; - } - } - - // no MapAll - take care - private void Map(MediaTypeSave source, IMediaType target, MapperContext context) - { - MapSaveToTypeBase(source, target, context); - MapComposition(source, target, alias => _mediaTypeService.Get(alias)); - } - - // no MapAll - take care - private void Map(MemberTypeSave source, IMemberType target, MapperContext context) - { - MapSaveToTypeBase(source, target, context); - MapComposition(source, target, alias => _memberTypeService.Get(alias)); - - foreach (MemberPropertyTypeBasic propertyType in source.Groups.SelectMany(x => x.Properties)) - { - MemberPropertyTypeBasic localCopy = propertyType; - IPropertyType? destProp = - target.PropertyTypes.SingleOrDefault(x => x.Alias?.InvariantEquals(localCopy.Alias) ?? false); - if (destProp == null) + if (destGroup.PropertyTypes?.SupportsPublishing != isPublishing || + destGroup.PropertyTypes.SequenceEqual(destProperties) is false) { - continue; + destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); } - target.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); - target.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); - target.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData); + destGroups.Add(destGroup); } } - // no MapAll - take care - private void Map(IContentType source, DocumentTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target); + // ensure no duplicate name, then assign the groups collection + EnsureUniqueAliases(destGroups); - if (source is IContentTypeWithHistoryCleanup sourceWithHistoryCleanup) + if (target.PropertyGroups.SequenceEqual(destGroups) is false) + { + target.PropertyGroups = new PropertyGroupCollection(destGroups); + } + + // because the property groups collection was rebuilt, there is no need to remove + // the old groups - they are just gone and will be cleared by the repository + + // handle non-grouped (ie generic) properties + PropertyGroupBasic? genericPropertiesGroup = + source.Groups.FirstOrDefault(x => x.IsGenericProperties); + if (genericPropertiesGroup != null) + { + // handle local properties + IPropertyType[] destProperties = genericPropertiesGroup.Properties + .Where(x => x.Inherited == false) + .Select(x => MapSaveProperty(x, destOrigProperties, context)) + .WhereNotNull() + .ToArray(); + + // ensure no duplicate alias, then assign the generic properties collection + EnsureUniqueAliases(destProperties); + target.NoGroupPropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); + } + + // because all property collections were rebuilt, there is no need to remove + // some old properties, they are just gone and will be cleared by the repository + } + + // Umbraco.Code.MapAll -Blueprints -Errors -ListViewEditorName -Trashed + private void MapTypeToDisplayBase(IContentTypeComposition source, ContentTypeCompositionDisplay target) + { + target.Alias = source.Alias; + target.AllowAsRoot = source.AllowedAsRoot; + target.CreateDate = source.CreateDate; + target.Description = source.Description; + target.Icon = source.Icon; + target.IconFilePath = target.IconIsClass + ? string.Empty + : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; + target.Id = source.Id; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Thumbnail = source.Thumbnail; + target.ThumbnailFilePath = target.ThumbnailIsClass + ? string.Empty + : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); + target.Udi = MapContentTypeUdi(source); + target.UpdateDate = source.UpdateDate; + + target.AllowedContentTypes = source.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); + target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); + target.LockedCompositeContentTypes = MapLockedCompositions(source); + } + + // no MapAll - relies on the non-generic method + private void MapTypeToDisplayBase(IContentTypeComposition source, TTarget target) + where TTarget : ContentTypeCompositionDisplay + where TTargetPropertyType : PropertyTypeDisplay, new() + { + MapTypeToDisplayBase(source, target); + + var groupsMapper = new PropertyTypeGroupMapper( + _propertyEditors, + _dataTypeService, + _shortStringHelper, + _loggerFactory.CreateLogger>()); + target.Groups = groupsMapper.Map(source); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -ListViewEditorName -Errors -LockedCompositeContentTypes + private void MapTypeToDisplayBase(ContentTypeSave source, ContentTypeCompositionDisplay target) + { + target.Alias = source.Alias; + target.AllowAsRoot = source.AllowAsRoot; + target.AllowedContentTypes = source.AllowedContentTypes; + target.Blueprints = source.Blueprints; + target.CompositeContentTypes = source.CompositeContentTypes; + target.Description = source.Description; + target.Icon = source.Icon; + target.IconFilePath = target.IconIsClass + ? string.Empty + : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; + target.Id = source.Id; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Thumbnail = source.Thumbnail; + target.ThumbnailFilePath = target.ThumbnailIsClass + ? string.Empty + : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); + target.Trashed = source.Trashed; + target.Udi = source.Udi; + } + + // no MapAll - relies on the non-generic method + private void MapTypeToDisplayBase( + TSource source, + TTarget target, + MapperContext context) + where TSource : ContentTypeSave + where TSourcePropertyType : PropertyTypeBasic + where TTarget : ContentTypeCompositionDisplay + where TTargetPropertyType : PropertyTypeDisplay + { + MapTypeToDisplayBase(source, target); + + target.Groups = + context + .MapEnumerable, PropertyGroupDisplay>( + source.Groups).WhereNotNull(); + } + + private IEnumerable MapLockedCompositions(IContentTypeComposition source) + { + // get ancestor ids from path of parent if not root + if (source.ParentId == Constants.System.Root) + { + return Enumerable.Empty(); + } + + IContentType? parent = _contentTypeService.Get(source.ParentId); + if (parent == null) + { + return Enumerable.Empty(); + } + + var aliases = new List(); + IEnumerable? ancestorIds = parent.Path?.Split(Constants.CharArrays.Comma) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)); + + // loop through all content types and return ordered aliases of ancestors + IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray(); + if (ancestorIds is not null) + { + foreach (var ancestorId in ancestorIds) { - target.HistoryCleanup = new HistoryCleanupViewModel + IContentType? ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + if (ancestor is not null && ancestor.Alias is not null) { - PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, - KeepAllVersionsNewerThanDays = sourceWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, - KeepLatestVersionPerDayForDays = sourceWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays, - GlobalKeepAllVersionsNewerThanDays = _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays, - GlobalKeepLatestVersionPerDayForDays = _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays, - GlobalEnableCleanup = _contentSettings.ContentVersionCleanupPolicy.EnableCleanup - }; - } - - target.AllowCultureVariant = source.VariesByCulture(); - target.AllowSegmentVariant = source.VariesBySegment(); - target.ContentApps = _commonMapper.GetContentAppsForEntity(source); - - //sync templates - if (source.AllowedTemplates is not null) - { - target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates).WhereNotNull(); - } - - if (source.DefaultTemplate != null) - { - target.DefaultTemplate = context.Map(source.DefaultTemplate); - } - - //default listview - target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; - - if (string.IsNullOrEmpty(source.Alias)) - { - return; - } - - var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; - if (_dataTypeService.GetDataType(name) != null) - { - target.ListViewEditorName = name; - } - } - - // no MapAll - take care - private void Map(IMediaType source, MediaTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target); - - //default listview - target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; - target.IsSystemMediaType = source.IsSystemMediaType(); - - if (string.IsNullOrEmpty(source.Name)) - { - return; - } - - var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; - if (_dataTypeService.GetDataType(name) != null) - { - target.ListViewEditorName = name; - } - } - - // no MapAll - take care - private void Map(IMemberType source, MemberTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target); - - //map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData - foreach (IPropertyType propertyType in source.PropertyTypes) - { - IPropertyType localCopy = propertyType; - MemberPropertyTypeDisplay? displayProp = target.Groups.SelectMany(dest => dest.Properties) - .SingleOrDefault(dest => dest.Alias?.InvariantEquals(localCopy.Alias) ?? false); - if (displayProp == null) - { - continue; - } - - displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias); - displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias); - displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias); - } - } - - // Umbraco.Code.MapAll -Blueprints - private void Map(IContentTypeBase source, ContentTypeBasic target, string entityType) - { - target.Udi = Udi.Create(entityType, source.Key); - target.Alias = source.Alias; - target.CreateDate = source.CreateDate; - target.Description = source.Description; - target.Icon = source.Icon; - target.IconFilePath = target.IconIsClass - ? string.Empty - : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; - - target.Trashed = source.Trashed; - target.Id = source.Id; - target.IsContainer = source.IsContainer; - target.IsElement = source.IsElement; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Thumbnail = source.Thumbnail; - target.ThumbnailFilePath = target.ThumbnailIsClass - ? string.Empty - : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); - target.UpdateDate = source.UpdateDate; - } - - // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IContentTypeComposition source, ContentTypeBasic target, MapperContext context) => - Map(source, target, Constants.UdiEntityType.MemberType); - - // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IContentType source, ContentTypeBasic target, MapperContext context) => - Map(source, target, Constants.UdiEntityType.DocumentType); - - // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IMediaType source, ContentTypeBasic target, MapperContext context) => - Map(source, target, Constants.UdiEntityType.MediaType); - - // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IMemberType source, ContentTypeBasic target, MapperContext context) => - Map(source, target, Constants.UdiEntityType.MemberType); - - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations - private static void Map(PropertyTypeBasic source, IPropertyType target, MapperContext context) - { - target.Name = source.Label; - target.DataTypeId = source.DataTypeId; - target.DataTypeKey = source.DataTypeKey; - target.Mandatory = source.Validation?.Mandatory ?? false; - target.MandatoryMessage = source.Validation?.MandatoryMessage; - target.ValidationRegExp = source.Validation?.Pattern; - target.ValidationRegExpMessage = source.Validation?.PatternMessage; - target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); - target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); - - if (source.Id > 0) - { - target.Id = source.Id; - } - - if (source.GroupId > 0) - { - if (target.PropertyGroupId?.Value != source.GroupId) - { - target.PropertyGroupId = new Lazy(() => source.GroupId, false); + aliases.Add(ancestor.Alias); } } - - target.Alias = source.Alias; - target.Description = source.Description; - target.SortOrder = source.SortOrder; - target.LabelOnTop = source.LabelOnTop; } - // no MapAll - take care - private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperContext context) + return aliases.OrderBy(x => x); + } + + private static PropertyGroup? MapSaveGroup( + PropertyGroupBasic sourceGroup, + IEnumerable destOrigGroups, + MapperContext context) + where TPropertyType : PropertyTypeBasic + { + PropertyGroup? destGroup; + if (sourceGroup.Id > 0) { - MapTypeToDisplayBase(source, - target, context); - - //sync templates - IEnumerable destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias); - //if the dest is set and it's the same as the source, then don't change - if (source.AllowedTemplates is not null && destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) + // update an existing group + // ensure it is still there, then map/update + destGroup = destOrigGroups.FirstOrDefault(x => x.Id == sourceGroup.Id); + if (destGroup != null) { - IEnumerable? templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray()); - target.AllowedTemplates = source.AllowedTemplates - .Select(x => - { - ITemplate? template = templates?.SingleOrDefault(t => t.Alias == x); - return template != null - ? context.Map(template) - : null; - }) - .WhereNotNull() - .ToArray(); + context.Map(sourceGroup, destGroup); + return destGroup; } - if (source.DefaultTemplate.IsNullOrWhiteSpace() == false) - { - //if the dest is set and it's the same as the source, then don't change - if (target.DefaultTemplate == null || source.DefaultTemplate != target.DefaultTemplate.Alias) - { - ITemplate? template = _fileService.GetTemplate(source.DefaultTemplate); - target.DefaultTemplate = template == null ? null : context.Map(template); - } - } - else - { - target.DefaultTemplate = null; - } + // force-clear the ID as it does not match anything + sourceGroup.Id = 0; } - // no MapAll - take care - private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) => - MapTypeToDisplayBase(source, - target, context); + // insert a new group, or update an existing group that has + // been deleted in the meantime and we need to re-create + // map/create + destGroup = context.Map(sourceGroup); + return destGroup; + } - // no MapAll - take care - private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) => - MapTypeToDisplayBase( - source, target, context); - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes - private static void Map(PropertyGroupBasic source, PropertyGroup target, - MapperContext context) + private static IPropertyType? MapSaveProperty( + PropertyTypeBasic sourceProperty, + IEnumerable destOrigProperties, + MapperContext context) + { + IPropertyType? destProperty; + if (sourceProperty.Id > 0) { - if (source.Id > 0) + // updating an existing property + // ensure it is still there, then map/update + destProperty = destOrigProperties.FirstOrDefault(x => x.Id == sourceProperty.Id); + if (destProperty != null) { - target.Id = source.Id; + context.Map(sourceProperty, destProperty); + return destProperty; } - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; + // force-clear the ID as it does not match anything + sourceProperty.Id = 0; } - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes - private static void Map(PropertyGroupBasic source, PropertyGroup target, - MapperContext context) - { - if (source.Id > 0) - { - target.Id = source.Id; - } + // insert a new property, or update an existing property that has + // been deleted in the meantime and we need to re-create + // map/create + destProperty = context.Map(sourceProperty); + return destProperty; + } - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; + private static void EnsureUniqueAliases(IEnumerable properties) + { + IPropertyType[] propertiesA = properties.ToArray(); + var distinctProperties = propertiesA + .Select(x => x.Alias?.ToUpperInvariant()) + .Distinct() + .Count(); + if (distinctProperties != propertiesA.Length) + { + throw new InvalidOperationException("Cannot map properties due to alias conflict."); + } + } + + private static void EnsureUniqueAliases(IEnumerable groups) + { + PropertyGroup[] groupsA = groups.ToArray(); + var distinctProperties = groupsA + .Select(x => x.Alias) + .Distinct() + .Count(); + if (distinctProperties != groupsA.Length) + { + throw new InvalidOperationException("Cannot map groups due to alias conflict."); + } + } + + private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, Func getContentType) + { + var current = target.CompositionAliases().ToArray(); + IEnumerable proposed = source.CompositeContentTypes; + + IEnumerable remove = current.Where(x => !proposed.Contains(x)); + IEnumerable add = proposed.Where(x => !current.Contains(x)); + + foreach (var alias in remove) + { + target.RemoveContentType(alias); } - // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames - private static void Map(PropertyGroupBasic source, - PropertyGroupDisplay target, MapperContext context) + foreach (var alias in add) { - target.Inherited = source.Inherited; - if (source.Id > 0) + // TODO: Remove N+1 lookup + IContentTypeComposition? contentType = getContentType(alias); + if (contentType != null) { - target.Id = source.Id; - } - - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; - target.Properties = context.MapEnumerable(source.Properties).WhereNotNull(); - } - - // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames - private static void Map(PropertyGroupBasic source, - PropertyGroupDisplay target, MapperContext context) - { - target.Inherited = source.Inherited; - if (source.Id > 0) - { - target.Id = source.Id; - } - - target.Key = source.Key; - target.Type = source.Type; - target.Name = source.Name; - target.Alias = source.Alias; - target.SortOrder = source.SortOrder; - target.Properties = - context.MapEnumerable(source.Properties).WhereNotNull(); - } - - // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName - private static void Map(PropertyTypeBasic source, PropertyTypeDisplay target, MapperContext context) - { - target.Alias = source.Alias; - target.AllowCultureVariant = source.AllowCultureVariant; - target.AllowSegmentVariant = source.AllowSegmentVariant; - target.DataTypeId = source.DataTypeId; - target.DataTypeKey = source.DataTypeKey; - target.Description = source.Description; - target.GroupId = source.GroupId; - target.Id = source.Id; - target.Inherited = source.Inherited; - target.Label = source.Label; - target.SortOrder = source.SortOrder; - target.Validation = source.Validation; - target.LabelOnTop = source.LabelOnTop; - } - - // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName - private static void Map(MemberPropertyTypeBasic source, MemberPropertyTypeDisplay target, MapperContext context) - { - target.Alias = source.Alias; - target.AllowCultureVariant = source.AllowCultureVariant; - target.AllowSegmentVariant = source.AllowSegmentVariant; - target.DataTypeId = source.DataTypeId; - target.DataTypeKey = source.DataTypeKey; - target.Description = source.Description; - target.GroupId = source.GroupId; - target.Id = source.Id; - target.Inherited = source.Inherited; - target.IsSensitiveData = source.IsSensitiveData; - target.Label = source.Label; - target.MemberCanEditProperty = source.MemberCanEditProperty; - target.MemberCanViewProperty = source.MemberCanViewProperty; - target.SortOrder = source.SortOrder; - target.Validation = source.Validation; - target.LabelOnTop = source.LabelOnTop; - } - - // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) - private static void MapSaveToTypeBase(TSource source, - IContentTypeComposition target, MapperContext context) - where TSource : ContentTypeSave - where TSourcePropertyType : PropertyTypeBasic - { - // TODO: not so clean really - var isPublishing = target is IContentType; - - var id = Convert.ToInt32(source.Id); - if (id > 0) - { - target.Id = id; - } - - target.Alias = source.Alias; - target.Description = source.Description; - target.Icon = source.Icon; - target.IsContainer = source.IsContainer; - target.IsElement = source.IsElement; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Thumbnail = source.Thumbnail; - - target.AllowedAsRoot = source.AllowAsRoot; - - bool allowedContentTypesUnchanged = target.AllowedContentTypes?.Select(x => x.Id.Value) - .SequenceEqual(source.AllowedContentTypes) ?? false; - - if (allowedContentTypesUnchanged is false) - { - target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); - } - - - if (!(target is IMemberType)) - { - target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); - target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); - } - - // handle property groups and property types - // note that ContentTypeSave has - // - all groups, inherited and local; only *one* occurrence per group *name* - // - potentially including the generic properties group - // - all properties, inherited and local - // - // also, see PropertyTypeGroupResolver.ResolveCore: - // - if a group is local *and* inherited, then Inherited is true - // and the identifier is the identifier of the *local* group - // - // IContentTypeComposition AddPropertyGroup, AddPropertyType methods do some - // unique-alias-checking, etc that is *not* compatible with re-mapping everything - // the way we do it here, so we should exclusively do it by - // - managing a property group's PropertyTypes collection - // - managing the content type's PropertyTypes collection (for generic properties) - - // handle actual groups (non-generic-properties) - PropertyGroup[] destOrigGroups = target.PropertyGroups.ToArray(); // local groups - IPropertyType[] destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not - var destGroups = new List(); - PropertyGroupBasic[] sourceGroups = - source.Groups.Where(x => x.IsGenericProperties == false).ToArray(); - var sourceGroupParentAliases = sourceGroups.Select(x => x.GetParentAlias()).Distinct().ToArray(); - foreach (PropertyGroupBasic sourceGroup in sourceGroups) - { - // get the dest group - PropertyGroup? destGroup = MapSaveGroup(sourceGroup, destOrigGroups, context); - - // handle local properties - IPropertyType[] destProperties = sourceGroup.Properties - .Where(x => x.Inherited == false) - .Select(x => MapSaveProperty(x, destOrigProperties, context)) - .WhereNotNull() - .ToArray(); - - // if the group has no local properties and is not used as parent, skip it, ie sort-of garbage-collect - // local groups which would not have local properties anymore - if (destProperties.Length == 0 && !sourceGroupParentAliases.Contains(sourceGroup.Alias)) - { - continue; - } - - // ensure no duplicate alias, then assign the group properties collection - EnsureUniqueAliases(destProperties); - - if (destGroup is not null) - { - if (destGroup.PropertyTypes?.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false) - { - destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); - } - - destGroups.Add(destGroup); - } - } - - // ensure no duplicate name, then assign the groups collection - EnsureUniqueAliases(destGroups); - - if (target.PropertyGroups.SequenceEqual(destGroups) is false) - { - target.PropertyGroups = new PropertyGroupCollection(destGroups); - } - - // because the property groups collection was rebuilt, there is no need to remove - // the old groups - they are just gone and will be cleared by the repository - - // handle non-grouped (ie generic) properties - PropertyGroupBasic? genericPropertiesGroup = - source.Groups.FirstOrDefault(x => x.IsGenericProperties); - if (genericPropertiesGroup != null) - { - // handle local properties - IPropertyType[] destProperties = genericPropertiesGroup.Properties - .Where(x => x.Inherited == false) - .Select(x => MapSaveProperty(x, destOrigProperties, context)) - .WhereNotNull() - .ToArray(); - - // ensure no duplicate alias, then assign the generic properties collection - EnsureUniqueAliases(destProperties); - target.NoGroupPropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); - } - - // because all property collections were rebuilt, there is no need to remove - // some old properties, they are just gone and will be cleared by the repository - } - - // Umbraco.Code.MapAll -Blueprints -Errors -ListViewEditorName -Trashed - private void MapTypeToDisplayBase(IContentTypeComposition source, ContentTypeCompositionDisplay target) - { - target.Alias = source.Alias; - target.AllowAsRoot = source.AllowedAsRoot; - target.CreateDate = source.CreateDate; - target.Description = source.Description; - target.Icon = source.Icon; - target.IconFilePath = target.IconIsClass - ? string.Empty - : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; - target.Id = source.Id; - target.IsContainer = source.IsContainer; - target.IsElement = source.IsElement; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Thumbnail = source.Thumbnail; - target.ThumbnailFilePath = target.ThumbnailIsClass - ? string.Empty - : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); - target.Udi = MapContentTypeUdi(source); - target.UpdateDate = source.UpdateDate; - - target.AllowedContentTypes = source.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); - target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); - target.LockedCompositeContentTypes = MapLockedCompositions(source); - } - - // no MapAll - relies on the non-generic method - private void MapTypeToDisplayBase(IContentTypeComposition source, TTarget target) - where TTarget : ContentTypeCompositionDisplay - where TTargetPropertyType : PropertyTypeDisplay, new() - { - MapTypeToDisplayBase(source, target); - - var groupsMapper = new PropertyTypeGroupMapper(_propertyEditors, _dataTypeService, - _shortStringHelper, _loggerFactory.CreateLogger>()); - target.Groups = groupsMapper.Map(source); - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -ListViewEditorName -Errors -LockedCompositeContentTypes - private void MapTypeToDisplayBase(ContentTypeSave source, ContentTypeCompositionDisplay target) - { - target.Alias = source.Alias; - target.AllowAsRoot = source.AllowAsRoot; - target.AllowedContentTypes = source.AllowedContentTypes; - target.Blueprints = source.Blueprints; - target.CompositeContentTypes = source.CompositeContentTypes; - target.Description = source.Description; - target.Icon = source.Icon; - target.IconFilePath = target.IconIsClass - ? string.Empty - : $"{_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith("/")}images/umbraco/{source.Icon}"; - target.Id = source.Id; - target.IsContainer = source.IsContainer; - target.IsElement = source.IsElement; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Thumbnail = source.Thumbnail; - target.ThumbnailFilePath = target.ThumbnailIsClass - ? string.Empty - : _hostingEnvironment.ToAbsolute("~/umbraco/images/thumbnails/" + source.Thumbnail); - target.Trashed = source.Trashed; - target.Udi = source.Udi; - } - - // no MapAll - relies on the non-generic method - private void MapTypeToDisplayBase(TSource source, - TTarget target, MapperContext context) - where TSource : ContentTypeSave - where TSourcePropertyType : PropertyTypeBasic - where TTarget : ContentTypeCompositionDisplay - where TTargetPropertyType : PropertyTypeDisplay - { - MapTypeToDisplayBase(source, target); - - target.Groups = - context - .MapEnumerable, PropertyGroupDisplay>( - source.Groups).WhereNotNull(); - } - - private IEnumerable MapLockedCompositions(IContentTypeComposition source) - { - // get ancestor ids from path of parent if not root - if (source.ParentId == Constants.System.Root) - { - return Enumerable.Empty(); - } - - IContentType? parent = _contentTypeService.Get(source.ParentId); - if (parent == null) - { - return Enumerable.Empty(); - } - - var aliases = new List(); - IEnumerable? ancestorIds = parent.Path?.Split(Constants.CharArrays.Comma) - .Select(s => int.Parse(s, CultureInfo.InvariantCulture)); - // loop through all content types and return ordered aliases of ancestors - IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray(); - if (ancestorIds is not null) - { - foreach (var ancestorId in ancestorIds) - { - IContentType? ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); - if (ancestor is not null && ancestor.Alias is not null) - { - aliases.Add(ancestor.Alias); - } - } - } - - - return aliases.OrderBy(x => x); - } - - public static Udi? MapContentTypeUdi(IContentTypeComposition source) - { - if (source == null) - { - return null; - } - - string udiType; - switch (source) - { - case IMemberType _: - udiType = Constants.UdiEntityType.MemberType; - break; - case IMediaType _: - udiType = Constants.UdiEntityType.MediaType; - break; - case IContentType _: - udiType = Constants.UdiEntityType.DocumentType; - break; - default: - throw new PanicException($"Source is of type {source.GetType()} which isn't supported here"); - } - - return Udi.Create(udiType, source.Key); - } - - private static PropertyGroup? MapSaveGroup(PropertyGroupBasic sourceGroup, - IEnumerable destOrigGroups, MapperContext context) - where TPropertyType : PropertyTypeBasic - { - PropertyGroup? destGroup; - if (sourceGroup.Id > 0) - { - // update an existing group - // ensure it is still there, then map/update - destGroup = destOrigGroups.FirstOrDefault(x => x.Id == sourceGroup.Id); - if (destGroup != null) - { - context.Map(sourceGroup, destGroup); - return destGroup; - } - - // force-clear the ID as it does not match anything - sourceGroup.Id = 0; - } - - // insert a new group, or update an existing group that has - // been deleted in the meantime and we need to re-create - // map/create - destGroup = context.Map(sourceGroup); - return destGroup; - } - - private static IPropertyType? MapSaveProperty(PropertyTypeBasic sourceProperty, - IEnumerable destOrigProperties, MapperContext context) - { - IPropertyType? destProperty; - if (sourceProperty.Id > 0) - { - // updating an existing property - // ensure it is still there, then map/update - destProperty = destOrigProperties.FirstOrDefault(x => x.Id == sourceProperty.Id); - if (destProperty != null) - { - context.Map(sourceProperty, destProperty); - return destProperty; - } - - // force-clear the ID as it does not match anything - sourceProperty.Id = 0; - } - - // insert a new property, or update an existing property that has - // been deleted in the meantime and we need to re-create - // map/create - destProperty = context.Map(sourceProperty); - return destProperty; - } - - private static void EnsureUniqueAliases(IEnumerable properties) - { - IPropertyType[] propertiesA = properties.ToArray(); - var distinctProperties = propertiesA - .Select(x => x.Alias?.ToUpperInvariant()) - .Distinct() - .Count(); - if (distinctProperties != propertiesA.Length) - { - throw new InvalidOperationException("Cannot map properties due to alias conflict."); - } - } - - private static void EnsureUniqueAliases(IEnumerable groups) - { - PropertyGroup[] groupsA = groups.ToArray(); - var distinctProperties = groupsA - .Select(x => x.Alias) - .Distinct() - .Count(); - if (distinctProperties != groupsA.Length) - { - throw new InvalidOperationException("Cannot map groups due to alias conflict."); - } - } - - private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, - Func getContentType) - { - var current = target.CompositionAliases().ToArray(); - IEnumerable proposed = source.CompositeContentTypes; - - IEnumerable remove = current.Where(x => !proposed.Contains(x)); - IEnumerable add = proposed.Where(x => !current.Contains(x)); - - foreach (var alias in remove) - { - target.RemoveContentType(alias); - } - - foreach (var alias in add) - { - // TODO: Remove N+1 lookup - IContentTypeComposition? contentType = getContentType(alias); - if (contentType != null) - { - target.AddContentType(contentType); - } + target.AddContentType(contentType); } } } diff --git a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs index 2f330b581f..7ad8b987d7 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs @@ -1,178 +1,174 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class ContentVariantMapper { - public class ContentVariantMapper + private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService _localizedTextService; + + public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) { - private readonly ILocalizationService _localizationService; - private readonly ILocalizedTextService _localizedTextService; + _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + } - public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) + public IEnumerable Map(IContent source, MapperContext context) + where TVariant : ContentVariantDisplay + { + var variesByCulture = source.ContentType.VariesByCulture(); + var variesBySegment = source.ContentType.VariesBySegment(); + + List variants = new (); + + if (!variesByCulture && !variesBySegment) { - _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + // this is invariant so just map the IContent instance to ContentVariationDisplay + TVariant? variantDisplay = context.Map(source); + if (variantDisplay is not null) + { + variants.Add(variantDisplay); + } } - - public IEnumerable Map(IContent source, MapperContext context) where TVariant : ContentVariantDisplay + else if (variesByCulture && !variesBySegment) { - var variesByCulture = source.ContentType.VariesByCulture(); - var variesBySegment = source.ContentType.VariesBySegment(); - - List variants = new (); - - if (!variesByCulture && !variesBySegment) - { - // this is invariant so just map the IContent instance to ContentVariationDisplay - var variantDisplay = context.Map(source); - if (variantDisplay is not null) - { - variants.Add(variantDisplay); - } - } - else if (variesByCulture && !variesBySegment) - { - var languages = GetLanguages(context); - variants = languages - .Select(language => CreateVariantDisplay(context, source, language, null)) - .WhereNotNull() - .ToList(); - } - else if (variesBySegment && !variesByCulture) - { - // Segment only - var segments = GetSegments(source); - variants = segments - .Select(segment => CreateVariantDisplay(context, source, null, segment)) - .WhereNotNull() - .ToList(); - } - else - { - // Culture and segment - var languages = GetLanguages(context).ToList(); - var segments = GetSegments(source).ToList(); - - if (languages.Count == 0 || segments.Count == 0) - { - // This should not happen - throw new InvalidOperationException("No languages or segments available"); - } - - variants = languages - .SelectMany(language => segments - .Select(segment => CreateVariantDisplay(context, source, language, segment))) - .WhereNotNull() - .ToList(); - } - - return SortVariants(variants); + IEnumerable languages = GetLanguages(context); + variants = languages + .Select(language => CreateVariantDisplay(context, source, language, null)) + .WhereNotNull() + .ToList(); } - - private IList SortVariants(IList variants) where TVariant : ContentVariantDisplay + else if (variesBySegment && !variesByCulture) { - if (variants.Count <= 1) + // Segment only + IEnumerable segments = GetSegments(source); + variants = segments + .Select(segment => CreateVariantDisplay(context, source, null, segment)) + .WhereNotNull() + .ToList(); + } + else + { + // Culture and segment + var languages = GetLanguages(context).ToList(); + var segments = GetSegments(source).ToList(); + + if (languages.Count == 0 || segments.Count == 0) { - return variants; + // This should not happen + throw new InvalidOperationException("No languages or segments available"); } - // Default variant first, then order by language, segment. - return variants - .OrderBy(v => IsDefaultLanguage(v) ? 0 : 1) - .ThenBy(v => IsDefaultSegment(v) ? 0 : 1) - .ThenBy(v => v?.Language?.Name) - .ThenBy(v => v.Segment) + variants = languages + .SelectMany(language => segments + .Select(segment => CreateVariantDisplay(context, source, language, segment))) + .WhereNotNull() .ToList(); } - private static bool IsDefaultSegment(ContentVariantDisplay variant) + return SortVariants(variants); + } + + private static bool IsDefaultSegment(ContentVariantDisplay variant) => variant.Segment == null; + + private IList SortVariants(IList variants) + where TVariant : ContentVariantDisplay + { + if ( variants.Count <= 1) { - return variant.Segment == null; + return variants; } - private static bool IsDefaultLanguage(ContentVariantDisplay variant) + // Default variant first, then order by language, segment. + return variants + .OrderBy(v => IsDefaultLanguage(v) ? 0 : 1) + .ThenBy(v => IsDefaultSegment(v) ? 0 : 1) + .ThenBy(v => v?.Language?.Name) + .ThenBy(v => v.Segment) + .ToList(); + } + + private static bool IsDefaultLanguage(ContentVariantDisplay variant) => + variant.Language == null || variant.Language.IsDefault; + + private IEnumerable GetLanguages(MapperContext context) + { + var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); + if (allLanguages.Count == 0) { - return variant.Language == null || variant.Language.IsDefault; + // This should never happen + return Enumerable.Empty(); } - private IEnumerable GetLanguages(MapperContext context) + return context.MapEnumerable(allLanguages).WhereNotNull().ToList(); + } + + /// + /// Returns all segments assigned to the content + /// + /// + /// + /// Returns all segments assigned to the content including the default `null` segment. + /// + private IEnumerable GetSegments(IContent content) + { + // The default segment (null) is always there, + // even when there is no property data at all yet + var segments = new List { null }; + + // Add actual segments based on the property values + segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment))); + + // Do not return a segment more than once + return segments.Distinct(); + } + + private TVariant? CreateVariantDisplay(MapperContext context, IContent content, ContentEditing.Language? language, string? segment) + where TVariant : ContentVariantDisplay + { + context.SetCulture(language?.IsoCode); + context.SetSegment(segment); + + TVariant? variantDisplay = context.Map(content); + + if (variantDisplay is null) { - var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (allLanguages.Count == 0) - { - // This should never happen - return Enumerable.Empty(); - } - else - { - return context.MapEnumerable(allLanguages).WhereNotNull().ToList(); - } + return null; } - /// - /// Returns all segments assigned to the content - /// - /// - /// - /// Returns all segments assigned to the content including the default `null` segment. - /// - private IEnumerable GetSegments(IContent content) + variantDisplay.Segment = segment; + variantDisplay.Language = language; + variantDisplay.Name = content.GetCultureName(language?.IsoCode); + variantDisplay.DisplayName = GetDisplayName(language, segment); + + return variantDisplay; + } + + private string GetDisplayName(ContentEditing.Language? language, string? segment) + { + var isCultureVariant = language is not null; + var isSegmentVariant = !segment.IsNullOrWhiteSpace(); + + if (!isCultureVariant && !isSegmentVariant) { - // The default segment (null) is always there, - // even when there is no property data at all yet - var segments = new List { null }; - - // Add actual segments based on the property values - segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment))); - - // Do not return a segment more than once - return segments.Distinct(); + return _localizedTextService.Localize("general", "default"); } - private TVariant? CreateVariantDisplay(MapperContext context, IContent content, ContentEditing.Language? language, string? segment) where TVariant : ContentVariantDisplay + var parts = new List(); + + if (isSegmentVariant) { - context.SetCulture(language?.IsoCode); - context.SetSegment(segment); - - var variantDisplay = context.Map(content); - - if (variantDisplay is null) - { - return null; - } - variantDisplay.Segment = segment; - variantDisplay.Language = language; - variantDisplay.Name = content.GetCultureName(language?.IsoCode); - variantDisplay.DisplayName = GetDisplayName(language, segment); - - return variantDisplay; + parts.Add(segment!); } - private string GetDisplayName(ContentEditing.Language? language, string? segment) + if (isCultureVariant) { - var isCultureVariant = language is not null; - var isSegmentVariant = !segment.IsNullOrWhiteSpace(); - - if(!isCultureVariant && !isSegmentVariant) - { - return _localizedTextService.Localize("general", "default"); - } - - var parts = new List(); - - if (isSegmentVariant) - parts.Add(segment!); - - if (isCultureVariant) - parts.Add(language?.Name!); - - return string.Join(" — ", parts); - + parts.Add(language?.Name!); } + + return string.Join(" — ", parts); } } diff --git a/src/Umbraco.Core/Models/Mapping/DataTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DataTypeMapDefinition.cs index f1fc81cd24..de2a773257 100644 --- a/src/Umbraco.Core/Models/Mapping/DataTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/DataTypeMapDefinition.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -10,205 +7,230 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class DataTypeMapDefinition : IMapDefinition { - public class DataTypeMapDefinition : IMapDefinition + private static readonly int[] SystemIds = { - private readonly PropertyEditorCollection _propertyEditors; - private readonly ILogger _logger; - private readonly ContentSettings _contentSettings; - private readonly IConfigurationEditorJsonSerializer _serializer; + Constants.DataTypes.DefaultContentListView, Constants.DataTypes.DefaultMediaListView, + Constants.DataTypes.DefaultMembersListView, + }; - public DataTypeMapDefinition(PropertyEditorCollection propertyEditors, ILogger logger, IOptions contentSettings, IConfigurationEditorJsonSerializer serializer) + private readonly ContentSettings _contentSettings; + private readonly ILogger _logger; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IConfigurationEditorJsonSerializer _serializer; + + public DataTypeMapDefinition(PropertyEditorCollection propertyEditors, ILogger logger, IOptions contentSettings, IConfigurationEditorJsonSerializer serializer) + { + _propertyEditors = propertyEditors; + _logger = logger; + _contentSettings = contentSettings.Value ?? throw new ArgumentNullException(nameof(contentSettings)); + _serializer = serializer; + } + + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new PropertyEditorBasic(), Map); + mapper.Define( + (source, context) => new DataTypeConfigurationFieldDisplay(), Map); + mapper.Define((source, context) => new DataTypeBasic(), Map); + mapper.Define((source, context) => new DataTypeBasic(), Map); + mapper.Define((source, context) => new DataTypeDisplay(), Map); + mapper.Define>(MapPreValues); + mapper.Define( + (source, context) => + new DataType(_propertyEditors[source.EditorAlias], _serializer) { CreateDate = DateTime.Now }, + Map); + mapper.Define>(MapPreValues); + } + + // Umbraco.Code.MapAll + private static void Map(IDataEditor source, PropertyEditorBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Name = source.Name; + } + + // Umbraco.Code.MapAll -Value + private static void Map(ConfigurationField source, DataTypeConfigurationFieldDisplay target, MapperContext context) + { + target.Config = source.Config; + target.Description = source.Description; + target.HideLabel = source.HideLabel; + target.Key = source.Key; + target.Name = source.Name; + target.View = source.View; + } + + // Umbraco.Code.MapAll -Udi -HasPrevalues -IsSystemDataType -Id -Trashed -Key + // Umbraco.Code.MapAll -ParentId -Path + private static void Map(IDataEditor source, DataTypeBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Group = source.Group; + target.Icon = source.Icon; + target.Name = source.Name; + } + + // Umbraco.Code.MapAll -HasPrevalues + private void Map(IDataType source, DataTypeBasic target, MapperContext context) + { + target.Id = source.Id; + target.IsSystemDataType = SystemIds.Contains(source.Id); + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Trashed = source.Trashed; + target.Udi = Udi.Create(Constants.UdiEntityType.DataType, source.Key); + + if (!_propertyEditors.TryGet(source.EditorAlias, out IDataEditor? editor)) { - _propertyEditors = propertyEditors; - _logger = logger; - _contentSettings = contentSettings.Value ?? throw new ArgumentNullException(nameof(contentSettings)); - _serializer = serializer; + return; } - private static readonly int[] SystemIds = - { - Constants.DataTypes.DefaultContentListView, - Constants.DataTypes.DefaultMediaListView, - Constants.DataTypes.DefaultMembersListView - }; + target.Alias = editor.Alias; + target.Group = editor.Group; + target.Icon = editor.Icon; + } - public void DefineMaps(IUmbracoMapper mapper) + // Umbraco.Code.MapAll -HasPrevalues + private void Map(IDataType source, DataTypeDisplay target, MapperContext context) + { + target.AvailableEditors = MapAvailableEditors(source, context); + target.Id = source.Id; + target.IsSystemDataType = SystemIds.Contains(source.Id); + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.PreValues = MapPreValues(source, context); + target.SelectedEditor = source.EditorAlias.IsNullOrWhiteSpace() ? null : source.EditorAlias; + target.Trashed = source.Trashed; + target.Udi = Udi.Create(Constants.UdiEntityType.DataType, source.Key); + + if (!_propertyEditors.TryGet(source.EditorAlias, out IDataEditor? editor)) { - mapper.Define((source, context) => new PropertyEditorBasic(), Map); - mapper.Define((source, context) => new DataTypeConfigurationFieldDisplay(), Map); - mapper.Define((source, context) => new DataTypeBasic(), Map); - mapper.Define((source, context) => new DataTypeBasic(), Map); - mapper.Define((source, context) => new DataTypeDisplay(), Map); - mapper.Define>(MapPreValues); - mapper.Define((source, context) => new DataType(_propertyEditors[source.EditorAlias], _serializer) { CreateDate = DateTime.Now }, Map); - mapper.Define>(MapPreValues); + return; } - // Umbraco.Code.MapAll - private static void Map(IDataEditor source, PropertyEditorBasic target, MapperContext context) + target.Alias = editor.Alias; + target.Group = editor.Group; + target.Icon = editor.Icon; + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate + // Umbraco.Code.MapAll -Key -Path -CreatorId -Level -SortOrder -Configuration + private void Map(DataTypeSave source, IDataType target, MapperContext context) + { + target.DatabaseType = MapDatabaseType(source); + target.Editor = _propertyEditors[source.EditorAlias]; + target.Id = Convert.ToInt32(source.Id); + target.Name = source.Name; + target.ParentId = source.ParentId; + } + + private IEnumerable MapAvailableEditors(IDataType source, MapperContext context) + { + IOrderedEnumerable properties = _propertyEditors + .Where(x => !x.IsDeprecated || _contentSettings.ShowDeprecatedPropertyEditors || + source.EditorAlias == x.Alias) + .OrderBy(x => x.Name); + return context.MapEnumerable(properties).WhereNotNull(); + } + + private IEnumerable MapPreValues(IDataType dataType, MapperContext context) + { + // in v7 it was apparently fine to have an empty .EditorAlias here, in which case we would map onto + // an empty fields list, which made no sense since there would be nothing to map to - and besides, + // a datatype without an editor alias is a serious issue - v8 wants an editor here + if (string.IsNullOrWhiteSpace(dataType.EditorAlias) || + !_propertyEditors.TryGet(dataType.EditorAlias, out IDataEditor? editor)) { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Name = source.Name; + throw new InvalidOperationException( + $"Could not find a property editor with alias \"{dataType.EditorAlias}\"."); } - // Umbraco.Code.MapAll -Value - private static void Map(ConfigurationField source, DataTypeConfigurationFieldDisplay target, MapperContext context) + IConfigurationEditor configurationEditor = editor.GetConfigurationEditor(); + var fields = context + .MapEnumerable(configurationEditor.Fields) + .WhereNotNull().ToList(); + IDictionary configurationDictionary = + configurationEditor.ToConfigurationEditor(dataType.Configuration); + + MapConfigurationFields(dataType, fields, configurationDictionary); + + return fields; + } + + private void MapConfigurationFields(IDataType? dataType, List fields, IDictionary? configuration) + { + if (fields == null) { - target.Config = source.Config; - target.Description = source.Description; - target.HideLabel = source.HideLabel; - target.Key = source.Key; - target.Name = source.Name; - target.View = source.View; + throw new ArgumentNullException(nameof(fields)); } - // Umbraco.Code.MapAll -Udi -HasPrevalues -IsSystemDataType -Id -Trashed -Key - // Umbraco.Code.MapAll -ParentId -Path - private static void Map(IDataEditor source, DataTypeBasic target, MapperContext context) + if (configuration == null) { - target.Alias = source.Alias; - target.Group = source.Group; - target.Icon = source.Icon; - target.Name = source.Name; + throw new ArgumentNullException(nameof(configuration)); } - // Umbraco.Code.MapAll -HasPrevalues - private void Map(IDataType source, DataTypeBasic target, MapperContext context) + // now we need to wire up the pre-values values with the actual fields defined + foreach (DataTypeConfigurationFieldDisplay field in fields.ToList()) { - target.Id = source.Id; - target.IsSystemDataType = SystemIds.Contains(source.Id); - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Trashed = source.Trashed; - target.Udi = Udi.Create(Constants.UdiEntityType.DataType, source.Key); - - if (!_propertyEditors.TryGet(source.EditorAlias, out var editor)) - return; - - target.Alias = editor!.Alias; - target.Group = editor.Group; - target.Icon = editor.Icon; - } - - // Umbraco.Code.MapAll -HasPrevalues - private void Map(IDataType source, DataTypeDisplay target, MapperContext context) - { - target.AvailableEditors = MapAvailableEditors(source, context); - target.Id = source.Id; - target.IsSystemDataType = SystemIds.Contains(source.Id); - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.PreValues = MapPreValues(source, context); - target.SelectedEditor = source.EditorAlias.IsNullOrWhiteSpace() ? null : source.EditorAlias; - target.Trashed = source.Trashed; - target.Udi = Udi.Create(Constants.UdiEntityType.DataType, source.Key); - - if (!_propertyEditors.TryGet(source.EditorAlias, out var editor)) - return; - - target.Alias = editor!.Alias; - target.Group = editor.Group; - target.Icon = editor.Icon; - } - - // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -Key -Path -CreatorId -Level -SortOrder -Configuration - private void Map(DataTypeSave source, IDataType target, MapperContext context) - { - target.DatabaseType = MapDatabaseType(source); - target.Editor = _propertyEditors[source.EditorAlias]; - target.Id = Convert.ToInt32(source.Id); - target.Name = source.Name; - target.ParentId = source.ParentId; - } - - private IEnumerable MapAvailableEditors(IDataType source, MapperContext context) - { - var properties = _propertyEditors - .Where(x => !x.IsDeprecated || _contentSettings.ShowDeprecatedPropertyEditors || source.EditorAlias == x.Alias) - .OrderBy(x => x.Name); - return context.MapEnumerable(properties).WhereNotNull(); - } - - private IEnumerable MapPreValues(IDataType dataType, MapperContext context) - { - // in v7 it was apparently fine to have an empty .EditorAlias here, in which case we would map onto - // an empty fields list, which made no sense since there would be nothing to map to - and besides, - // a datatype without an editor alias is a serious issue - v8 wants an editor here - - if (string.IsNullOrWhiteSpace(dataType.EditorAlias) || !_propertyEditors.TryGet(dataType.EditorAlias, out var editor)) - throw new InvalidOperationException($"Could not find a property editor with alias \"{dataType.EditorAlias}\"."); - - var configurationEditor = editor!.GetConfigurationEditor(); - var fields = context.MapEnumerable(configurationEditor.Fields).WhereNotNull().ToList(); - var configurationDictionary = configurationEditor.ToConfigurationEditor(dataType.Configuration); - - MapConfigurationFields(dataType, fields, configurationDictionary); - - return fields; - } - - private void MapConfigurationFields(IDataType? dataType, List fields, IDictionary? configuration) - { - if (fields == null) throw new ArgumentNullException(nameof(fields)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - // now we need to wire up the pre-values values with the actual fields defined - foreach (var field in fields.ToList()) + // filter out the not-supported pre-values for built-in data types + if (dataType != null && dataType.IsBuildInDataType() && + (field.Key?.InvariantEquals(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes) ?? false)) { - //filter out the not-supported pre-values for built-in data types - if (dataType != null && dataType.IsBuildInDataType() && (field.Key?.InvariantEquals(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes) ?? false)) - { - fields.Remove(field); - continue; - } + fields.Remove(field); + continue; + } - if (field.Key is not null && configuration.TryGetValue(field.Key, out var value)) - { - field.Value = value; - } - else - { - // weird - just leave the field without a value - but warn - _logger.LogWarning("Could not find a value for configuration field '{ConfigField}'", field.Key); - } + if (field.Key is not null && configuration.TryGetValue(field.Key, out var value)) + { + field.Value = value; + } + else + { + // weird - just leave the field without a value - but warn + _logger.LogWarning("Could not find a value for configuration field '{ConfigField}'", field.Key); } } + } - private ValueStorageType MapDatabaseType(DataTypeSave source) + private ValueStorageType MapDatabaseType(DataTypeSave source) + { + if (!_propertyEditors.TryGet(source.EditorAlias, out IDataEditor? editor)) { - if (!_propertyEditors.TryGet(source.EditorAlias, out var editor)) - throw new InvalidOperationException($"Could not find property editor \"{source.EditorAlias}\"."); - - // TODO: what about source.PropertyEditor? can we get the configuration here? 'cos it may change the storage type?! - var valueType = editor!.GetValueEditor().ValueType; - return ValueTypes.ToStorageType(valueType); + throw new InvalidOperationException($"Could not find property editor \"{source.EditorAlias}\"."); } - private IEnumerable MapPreValues(IDataEditor source, MapperContext context) + // TODO: what about source.PropertyEditor? can we get the configuration here? 'cos it may change the storage type?! + var valueType = editor.GetValueEditor().ValueType; + return ValueTypes.ToStorageType(valueType); + } + + private IEnumerable MapPreValues(IDataEditor source, MapperContext context) + { + // this is a new data type, initialize default configuration + // get the configuration editor, + // get the configuration fields and map to UI, + // get the configuration default values and map to UI + IConfigurationEditor configurationEditor = source.GetConfigurationEditor(); + + var fields = + context.MapEnumerable(configurationEditor.Fields) + .WhereNotNull().ToList(); + + IDictionary defaultConfiguration = configurationEditor.DefaultConfiguration; + if (defaultConfiguration != null) { - // this is a new data type, initialize default configuration - // get the configuration editor, - // get the configuration fields and map to UI, - // get the configuration default values and map to UI - - var configurationEditor = source.GetConfigurationEditor(); - - var fields = - context.MapEnumerable(configurationEditor.Fields).WhereNotNull().ToList(); - - var defaultConfiguration = configurationEditor.DefaultConfiguration; - if (defaultConfiguration != null) - MapConfigurationFields(null, fields, defaultConfiguration); - - return fields; + MapConfigurationFields(null, fields, defaultConfiguration); } + + return fields; } } diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs index a5db1d4b96..cab595e00f 100644 --- a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs @@ -1,127 +1,126 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// +/// The dictionary model mapper. +/// +public class DictionaryMapDefinition : IMapDefinition { - /// - /// - /// The dictionary model mapper. - /// - public class DictionaryMapDefinition : IMapDefinition + private readonly CommonMapper? _commonMapper; + private readonly ILocalizationService _localizationService; + + [Obsolete("Use the constructor with the CommonMapper")] + public DictionaryMapDefinition(ILocalizationService localizationService) => + _localizationService = localizationService; + + public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper) { - private readonly ILocalizationService _localizationService; - private readonly CommonMapper? _commonMapper; + _localizationService = localizationService; + _commonMapper = commonMapper; + } - [Obsolete("Use the constructor with the CommonMapper")] - public DictionaryMapDefinition(ILocalizationService localizationService) + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new DictionaryDisplay(), Map); + mapper.Define( + (source, context) => new DictionaryOverviewDisplay(), + Map); + } + + // Umbraco.Code.MapAll -ParentId -Path -Trashed -Udi -Icon + private static void Map(IDictionaryItem source, EntityBasic target, MapperContext context) + { + target.Alias = source.ItemKey; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.ItemKey; + } + + private static void GetParentId(Guid parentId, ILocalizationService localizationService, List ids) + { + IDictionaryItem? dictionary = localizationService.GetDictionaryItemById(parentId); + if (dictionary == null) { - _localizationService = localizationService; + return; } - public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper) + ids.Add(dictionary.Id); + + if (dictionary.ParentId.HasValue) { - _localizationService = localizationService; - _commonMapper = commonMapper; + GetParentId(dictionary.ParentId.Value, localizationService, ids); + } + } + + // Umbraco.Code.MapAll -Icon -Trashed -Alias + private void Map(IDictionaryItem source, DictionaryDisplay target, MapperContext context) + { + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.ItemKey; + target.ParentId = source.ParentId ?? Guid.Empty; + target.Udi = Udi.Create(Constants.UdiEntityType.DictionaryItem, source.Key); + if (_commonMapper != null) + { + target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source)); } - public void DefineMaps(IUmbracoMapper mapper) + // build up the path to make it possible to set active item in tree + // TODO: check if there is a better way + if (source.ParentId.HasValue) { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new DictionaryDisplay(), Map); - mapper.Define((source, context) => new DictionaryOverviewDisplay(), Map); + var ids = new List { -1 }; + var parentIds = new List(); + GetParentId(source.ParentId.Value, _localizationService, parentIds); + parentIds.Reverse(); + ids.AddRange(parentIds); + ids.Add(source.Id); + target.Path = string.Join(",", ids); + } + else + { + target.Path = "-1," + source.Id; } - // Umbraco.Code.MapAll -ParentId -Path -Trashed -Udi -Icon - private static void Map(IDictionaryItem source, EntityBasic target, MapperContext context) + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) { - target.Alias = source.ItemKey; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.ItemKey; + var langId = lang.Id; + IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); + + target.Translations.Add(new DictionaryTranslationDisplay + { + IsoCode = lang.IsoCode, + DisplayName = lang.CultureName, + Translation = translation?.Value ?? string.Empty, + LanguageId = lang.Id, + }); } + } - // Umbraco.Code.MapAll -Icon -Trashed -Alias - private void Map(IDictionaryItem source, DictionaryDisplay target, MapperContext context) + // Umbraco.Code.MapAll -Level -Translations + private void Map(IDictionaryItem source, DictionaryOverviewDisplay target, MapperContext context) + { + target.Id = source.Id; + target.Name = source.ItemKey; + + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) { - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.ItemKey; - target.ParentId = source.ParentId ?? Guid.Empty; - target.Udi = Udi.Create(Constants.UdiEntityType.DictionaryItem, source.Key); - if (_commonMapper != null) - { - target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source)); - } + var langId = lang.Id; + IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); - // build up the path to make it possible to set active item in tree - // TODO: check if there is a better way - if (source.ParentId.HasValue) - { - var ids = new List { -1 }; - var parentIds = new List(); - GetParentId(source.ParentId.Value, _localizationService, parentIds); - parentIds.Reverse(); - ids.AddRange(parentIds); - ids.Add(source.Id); - target.Path = string.Join(",", ids); - } - else - { - target.Path = "-1," + source.Id; - } - - // add all languages and the translations - foreach (var lang in _localizationService.GetAllLanguages()) - { - var langId = lang.Id; - var translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); - - target.Translations.Add(new DictionaryTranslationDisplay + target.Translations.Add( + new DictionaryOverviewTranslationDisplay { - IsoCode = lang.IsoCode, DisplayName = lang.CultureName, - Translation = translation?.Value ?? string.Empty, - LanguageId = lang.Id + HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false, }); - } - } - - // Umbraco.Code.MapAll -Level -Translations - private void Map(IDictionaryItem source, DictionaryOverviewDisplay target, MapperContext context) - { - target.Id = source.Id; - target.Name = source.ItemKey; - - // add all languages and the translations - foreach (var lang in _localizationService.GetAllLanguages()) - { - var langId = lang.Id; - var translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); - - target.Translations.Add( - new DictionaryOverviewTranslationDisplay - { - DisplayName = lang.CultureName, - HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false - }); - } - } - - private static void GetParentId(Guid parentId, ILocalizationService localizationService, List ids) - { - var dictionary = localizationService.GetDictionaryItemById(parentId); - if (dictionary == null) - return; - - ids.Add(dictionary.Id); - - if (dictionary.ParentId.HasValue) - GetParentId(dictionary.ParentId.Value, localizationService, ids); } } } diff --git a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs index 7450ec62b4..81096889c8 100644 --- a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs @@ -1,60 +1,61 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class LanguageMapDefinition : IMapDefinition { - public class LanguageMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) { - public void DefineMaps(IUmbracoMapper mapper) + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new ContentEditing.Language(), Map); + mapper.Define, IEnumerable>( + (source, context) => new List(), Map); + } + + // Umbraco.Code.MapAll -Udi -Path -Trashed -AdditionalData -Icon + private static void Map(ILanguage source, EntityBasic target, MapperContext context) + { + target.Name = source.CultureName; + target.Key = source.Key; + target.ParentId = -1; + target.Alias = source.IsoCode; + target.Id = source.Id; + } + + // Umbraco.Code.MapAll + private static void Map(ILanguage source, ContentEditing.Language target, MapperContext context) + { + target.Id = source.Id; + target.IsoCode = source.IsoCode; + target.Name = source.CultureName; + target.IsDefault = source.IsDefault; + target.IsMandatory = source.IsMandatory; + target.FallbackLanguageId = source.FallbackLanguageId; + } + + private static void Map(IEnumerable source, IEnumerable target, MapperContext context) + { + if (target == null) { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new ContentEditing.Language(), Map); - mapper.Define, IEnumerable>((source, context) => new List(), Map); + throw new ArgumentNullException(nameof(target)); } - // Umbraco.Code.MapAll -Udi -Path -Trashed -AdditionalData -Icon - private static void Map(ILanguage source, EntityBasic target, MapperContext context) + if (!(target is List list)) { - target.Name = source.CultureName; - target.Key = source.Key; - target.ParentId = -1; - target.Alias = source.IsoCode; - target.Id = source.Id; + throw new NotSupportedException($"{nameof(target)} must be a List."); } - // Umbraco.Code.MapAll - private static void Map(ILanguage source, ContentEditing.Language target, MapperContext context) + List temp = context.MapEnumerable(source); + + // Put the default language first in the list & then sort rest by a-z + ContentEditing.Language? defaultLang = temp.SingleOrDefault(x => x.IsDefault); + + // insert default lang first, then remaining language a-z + if (defaultLang is not null) { - target.Id = source.Id; - target.IsoCode = source.IsoCode; - target.Name = source.CultureName; - target.IsDefault = source.IsDefault; - target.IsMandatory = source.IsMandatory; - target.FallbackLanguageId = source.FallbackLanguageId; - } - - private static void Map(IEnumerable source, IEnumerable target, MapperContext context) - { - if (target == null) - throw new ArgumentNullException(nameof(target)); - if (!(target is List list)) - throw new NotSupportedException($"{nameof(target)} must be a List."); - - var temp = context.MapEnumerable(source); - - //Put the default language first in the list & then sort rest by a-z - var defaultLang = temp.SingleOrDefault(x => x!.IsDefault); - - // insert default lang first, then remaining language a-z - if (defaultLang is not null) - { - list.Add(defaultLang); - list.AddRange(temp.Where(x => x != defaultLang).OrderBy(x => x!.Name).WhereNotNull()); - } + list.Add(defaultLang); + list.AddRange(temp.Where(x => x != defaultLang).OrderBy(x => x.Name)); } } } diff --git a/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs index 13fe7f7c33..a042497013 100644 --- a/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs @@ -1,88 +1,89 @@ -using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class MacroMapDefinition : IMapDefinition { - public class MacroMapDefinition : IMapDefinition + private readonly ILogger _logger; + private readonly ParameterEditorCollection _parameterEditors; + + public MacroMapDefinition(ParameterEditorCollection parameterEditors, ILogger logger) { - private readonly ParameterEditorCollection _parameterEditors; - private readonly ILogger _logger; + _parameterEditors = parameterEditors; + _logger = logger; + } - public MacroMapDefinition(ParameterEditorCollection parameterEditors, ILogger logger) + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new MacroDisplay(), Map); + mapper.Define>((source, context) => + context.MapEnumerable(source.Properties.Values).WhereNotNull()); + mapper.Define((source, context) => new MacroParameter(), Map); + } + + // Umbraco.Code.MapAll -Trashed -AdditionalData + private static void Map(IMacro source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Macro; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); + } + + private void Map(IMacro source, MacroDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Macro; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); + target.CacheByPage = source.CacheByPage; + target.CacheByUser = source.CacheByMember; + target.CachePeriod = source.CacheDuration; + target.UseInEditor = source.UseInEditor; + target.RenderInEditor = !source.DontRender; + target.View = source.MacroSource; + } + + // Umbraco.Code.MapAll -Value + private void Map(IMacroProperty source, MacroParameter target, MapperContext context) + { + target.Alias = source.Alias; + target.Name = source.Name; + target.SortOrder = source.SortOrder; + + // map the view and the config + // we need to show the deprecated ones for backwards compatibility + IDataEditor? paramEditor = _parameterEditors[source.EditorAlias]; // TODO: include/filter deprecated?! + if (paramEditor == null) { - _parameterEditors = parameterEditors; - _logger = logger; + // we'll just map this to a text box + paramEditor = _parameterEditors[Constants.PropertyEditors.Aliases.TextBox]; + _logger.LogWarning( + "Could not resolve a parameter editor with alias {PropertyEditorAlias}, a textbox will be rendered in it's place", + source.EditorAlias); } - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new MacroDisplay(), Map); - mapper.Define>((source, context) => context.MapEnumerable(source.Properties.Values).WhereNotNull()); - mapper.Define((source, context) => new MacroParameter(), Map); - } + target.View = paramEditor?.GetValueEditor().View; - // Umbraco.Code.MapAll -Trashed -AdditionalData - private static void Map(IMacro source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = Constants.Icons.Macro; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); - } - - private void Map(IMacro source, MacroDisplay target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = Constants.Icons.Macro; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.Udi = Udi.Create(Constants.UdiEntityType.Macro, source.Key); - target.CacheByPage = source.CacheByPage; - target.CacheByUser = source.CacheByMember; - target.CachePeriod = source.CacheDuration; - target.UseInEditor = source.UseInEditor; - target.RenderInEditor = !source.DontRender; - target.View = source.MacroSource; - } - // Umbraco.Code.MapAll -Value - private void Map(IMacroProperty source, MacroParameter target, MapperContext context) - { - target.Alias = source.Alias; - target.Name = source.Name; - target.SortOrder = source.SortOrder; - - //map the view and the config - // we need to show the deprecated ones for backwards compatibility - var paramEditor = _parameterEditors[source.EditorAlias]; // TODO: include/filter deprecated?! - if (paramEditor == null) - { - //we'll just map this to a text box - paramEditor = _parameterEditors[Constants.PropertyEditors.Aliases.TextBox]; - _logger.LogWarning("Could not resolve a parameter editor with alias {PropertyEditorAlias}, a textbox will be rendered in it's place", source.EditorAlias); - } - - target.View = paramEditor?.GetValueEditor().View; - - // sets the parameter configuration to be the default configuration editor's configuration, - // ie configurationEditor.DefaultConfigurationObject, prepared for the value editor, ie - // after ToValueEditor - important to use DefaultConfigurationObject here, because depending - // on editors, ToValueEditor expects the actual strongly typed configuration - not the - // dictionary thing returned by DefaultConfiguration - - var configurationEditor = paramEditor?.GetConfigurationEditor(); - target.Configuration = configurationEditor?.ToValueEditor(configurationEditor.DefaultConfigurationObject); - } + // sets the parameter configuration to be the default configuration editor's configuration, + // ie configurationEditor.DefaultConfigurationObject, prepared for the value editor, ie + // after ToValueEditor - important to use DefaultConfigurationObject here, because depending + // on editors, ToValueEditor expects the actual strongly typed configuration - not the + // dictionary thing returned by DefaultConfiguration + IConfigurationEditor? configurationEditor = paramEditor?.GetConfigurationEditor(); + target.Configuration = configurationEditor?.ToValueEditor(configurationEditor.DefaultConfigurationObject); } } diff --git a/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs b/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs index 89cdc22106..70d4826ab6 100644 --- a/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs +++ b/src/Umbraco.Core/Models/Mapping/MapperContextExtensions.cs @@ -1,68 +1,61 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the class. +/// +public static class MapperContextExtensions { + private const string CultureKey = "Map.Culture"; + private const string SegmentKey = "Map.Segment"; + private const string IncludedPropertiesKey = "Map.IncludedProperties"; + /// - /// Provides extension methods for the class. + /// Gets the context culture. /// - public static class MapperContextExtensions + public static string? GetCulture(this MapperContext context) => + context.HasItems && context.Items.TryGetValue(CultureKey, out var obj) && obj is string s ? s : null; + + /// + /// Gets the context segment. + /// + public static string? GetSegment(this MapperContext context) => + context.HasItems && context.Items.TryGetValue(SegmentKey, out var obj) && obj is string s ? s : null; + + /// + /// Sets a context culture. + /// + public static void SetCulture(this MapperContext context, string? culture) { - private const string CultureKey = "Map.Culture"; - private const string SegmentKey = "Map.Segment"; - private const string IncludedPropertiesKey = "Map.IncludedProperties"; - - /// - /// Gets the context culture. - /// - public static string? GetCulture(this MapperContext context) + if (culture is not null) { - return context.HasItems && context.Items.TryGetValue(CultureKey, out var obj) && obj is string s ? s : null; - } - - /// - /// Gets the context segment. - /// - public static string? GetSegment(this MapperContext context) - { - return context.HasItems && context.Items.TryGetValue(SegmentKey, out var obj) && obj is string s ? s : null; - } - - /// - /// Sets a context culture. - /// - public static void SetCulture(this MapperContext context, string? culture) - { - if (culture is not null) - { - context.Items[CultureKey] = culture; - } - } - - /// - /// Sets a context segment. - /// - public static void SetSegment(this MapperContext context, string? segment) - { - if (segment is not null) - { - context.Items[SegmentKey] = segment; - } - } - - /// - /// Get included properties. - /// - public static string[]? GetIncludedProperties(this MapperContext context) - { - return context.HasItems && context.Items.TryGetValue(IncludedPropertiesKey, out var obj) && obj is string[] s ? s : null; - } - - /// - /// Sets included properties. - /// - public static void SetIncludedProperties(this MapperContext context, string[] properties) - { - context.Items[IncludedPropertiesKey] = properties; + context.Items[CultureKey] = culture; } } + + /// + /// Sets a context segment. + /// + public static void SetSegment(this MapperContext context, string? segment) + { + if (segment is not null) + { + context.Items[SegmentKey] = segment; + } + } + + /// + /// Get included properties. + /// + public static string[]? GetIncludedProperties(this MapperContext context) => context.HasItems && + context.Items.TryGetValue(IncludedPropertiesKey, out var obj) && obj is string[] s + ? s + : null; + + /// + /// Sets included properties. + /// + public static void SetIncludedProperties(this MapperContext context, string[] properties) => + context.Items[IncludedPropertiesKey] = properties; } diff --git a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs index c2c3e14f5d..8444d5bd0a 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberMapDefinition.cs @@ -1,32 +1,31 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +public class MemberMapDefinition : IMapDefinition { /// - public class MemberMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) => mapper.Define(Map); + + private static void Map(MemberSave source, IMember target, MapperContext context) { - /// - public void DefineMaps(IUmbracoMapper mapper) => mapper.Define(Map); + target.IsApproved = source.IsApproved; + target.Name = source.Name; + target.Email = source.Email; + target.Key = source.Key; + target.Username = source.Username; + target.Comments = source.Comments; + target.CreateDate = source.CreateDate; + target.UpdateDate = source.UpdateDate; + target.Email = source.Email; - private static void Map(MemberSave source, IMember target, MapperContext context) - { - target.IsApproved = source.IsApproved; - target.Name = source.Name; - target.Email = source.Email; - target.Key = source.Key; - target.Username = source.Username; - target.Comments = source.Comments; - target.CreateDate = source.CreateDate; - target.UpdateDate = source.UpdateDate; - target.Email = source.Email; + // TODO: ensure all properties are mapped as required + // target.Id = source.Id; + // target.ParentId = -1; + // target.Path = "-1," + source.Id; - // TODO: ensure all properties are mapped as required - //target.Id = source.Id; - //target.ParentId = -1; - //target.Path = "-1," + source.Id; - - //TODO: add groups as required - } + // TODO: add groups as required } } diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 9a39051590..ae9876628f 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Schema; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Dictionary; @@ -12,286 +8,285 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +/// +/// A custom tab/property resolver for members which will ensure that the built-in membership properties are or aren't displayed +/// depending on if the member type has these properties +/// +/// +/// This also ensures that the IsLocked out property is readonly when the member is not locked out - this is because +/// an admin cannot actually set isLockedOut = true, they can only unlock. +/// +public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper { - /// - /// A custom tab/property resolver for members which will ensure that the built-in membership properties are or aren't displayed - /// depending on if the member type has these properties - /// - /// - /// This also ensures that the IsLocked out property is readonly when the member is not locked out - this is because - /// an admin cannot actually set isLockedOut = true, they can only unlock. - /// - public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly ILocalizedTextService _localizedTextService; + private readonly IMemberTypeService _memberTypeService; + private readonly IMemberService _memberService; + private readonly IMemberGroupService _memberGroupService; + private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; + private readonly PropertyEditorCollection _propertyEditorCollection; + + public MemberTabsAndPropertiesMapper( + ICultureDictionary cultureDictionary, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + ILocalizedTextService localizedTextService, + IMemberTypeService memberTypeService, + IMemberService memberService, + IMemberGroupService memberGroupService, + IOptions memberPasswordConfiguration, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + PropertyEditorCollection propertyEditorCollection) + : base(cultureDictionary, localizedTextService, contentTypeBaseServiceProvider) { - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly ILocalizedTextService _localizedTextService; - private readonly IMemberTypeService _memberTypeService; - private readonly IMemberService _memberService; - private readonly IMemberGroupService _memberGroupService; - private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration; - private readonly PropertyEditorCollection _propertyEditorCollection; + _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); + _memberPasswordConfiguration = memberPasswordConfiguration.Value; + _propertyEditorCollection = propertyEditorCollection; + } - public MemberTabsAndPropertiesMapper(ICultureDictionary cultureDictionary, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - ILocalizedTextService localizedTextService, - IMemberTypeService memberTypeService, - IMemberService memberService, - IMemberGroupService memberGroupService, - IOptions memberPasswordConfiguration, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - PropertyEditorCollection propertyEditorCollection) - : base(cultureDictionary, localizedTextService, contentTypeBaseServiceProvider) + /// + /// Overridden to deal with custom member properties and permissions. + public override IEnumerable> Map(IMember source, MapperContext context) + { + + IMemberType? memberType = _memberTypeService.Get(source.ContentTypeId); + + if (memberType is not null) { - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); - _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); - _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); - _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); - _memberPasswordConfiguration = memberPasswordConfiguration.Value; - _propertyEditorCollection = propertyEditorCollection; + + IgnoreProperties = memberType.CompositionPropertyTypes + .Where(x => x.HasIdentity == false) + .Select(x => x.Alias) + .ToArray(); } - /// - /// Overridden to deal with custom member properties and permissions. - public override IEnumerable> Map(IMember source, MapperContext context) + IEnumerable> resolved = base.Map(source, context); + + return resolved; + } + + [Obsolete("Use MapMembershipProperties. Will be removed in Umbraco 10.")] + protected override IEnumerable GetCustomGenericProperties(IContentBase content) + { + var member = (IMember)content; + return MapMembershipProperties(member, null); + } + + private Dictionary GetPasswordConfig(IMember member) + { + var result = new Dictionary(_memberPasswordConfiguration.GetConfiguration(true)) { + // the password change toggle will only be displayed if there is already a password assigned. + {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} + }; - var memberType = _memberTypeService.Get(source.ContentTypeId); + // This will always be true for members since we always want to allow admins to change a password - so long as that + // user has access to edit members (but that security is taken care of separately) + result["allowManuallyChangingPassword"] = true; - if (memberType is not null) + return result; + } + + /// + /// Overridden to assign the IsSensitive property values + /// + /// + /// + /// + /// + protected override List MapProperties(IContentBase content, List properties, MapperContext context) + { + List result = base.MapProperties(content, properties, context); + var member = (IMember)content; + IMemberType? memberType = _memberTypeService.Get(member.ContentTypeId); + + // now update the IsSensitive value + foreach (ContentPropertyDisplay prop in result) + { + // check if this property is flagged as sensitive + var isSensitiveProperty = memberType?.IsSensitiveProperty(prop.Alias) ?? false; + // check permissions for viewing sensitive data + if (isSensitiveProperty && _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() == false) { - - IgnoreProperties = memberType.CompositionPropertyTypes - .Where(x => x.HasIdentity == false) - .Select(x => x.Alias) - .ToArray(); + // mark this property as sensitive + prop.IsSensitive = true; + // mark this property as readonly so that it does not post any data + prop.Readonly = true; + // replace this editor with a sensitive value + prop.View = "sensitivevalue"; + // clear the value + prop.Value = null; } - - var resolved = base.Map(source, context); - - return resolved; } + return result; + } - [Obsolete("Use MapMembershipProperties. Will be removed in Umbraco 10.")] - protected override IEnumerable GetCustomGenericProperties(IContentBase content) + /// + /// Returns the login property display field + /// + /// + /// + /// + /// + /// If the membership provider installed is the umbraco membership provider, then we will allow changing the username, however if + /// the membership provider is a custom one, we cannot allow changing the username because MembershipProvider's do not actually natively + /// allow that. + /// + internal static ContentPropertyDisplay GetLoginProperty(IMember member, ILocalizedTextService localizedText) + { + var prop = new ContentPropertyDisplay { - var member = (IMember)content; - return MapMembershipProperties(member, null); - } + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login", + Label = localizedText.Localize(null,"login"), + Value = member.Username + }; - private Dictionary GetPasswordConfig(IMember member) + prop.View = "textbox"; + prop.Validation.Mandatory = true; + return prop; + } + + internal IDictionary GetMemberGroupValue(string username) + { + IEnumerable userRoles = _memberService.GetAllRoles(username); + + // create a dictionary of all roles (except internal roles) + "false" + var result = _memberGroupService.GetAll() + .Select(x => x.Name!) + // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access + .Where(x => x?.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToDictionary(x => x, x => false); + + // if user has no roles, just return the dictionary + if (userRoles == null) { - var result = new Dictionary(_memberPasswordConfiguration.GetConfiguration(true)) - { - // the password change toggle will only be displayed if there is already a password assigned. - {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} - }; - - // This will always be true for members since we always want to allow admins to change a password - so long as that - // user has access to edit members (but that security is taken care of separately) - result["allowManuallyChangingPassword"] = true; - return result; } - /// - /// Overridden to assign the IsSensitive property values - /// - /// - /// - /// - /// - protected override List MapProperties(IContentBase content, List properties, MapperContext context) + // else update the dictionary to "true" for the user roles (except internal roles) + foreach (var userRole in userRoles.Where(x => x?.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false)) { - var result = base.MapProperties(content, properties, context); - var member = (IMember)content; - var memberType = _memberTypeService.Get(member.ContentTypeId); + result[userRole] = true; + } - // now update the IsSensitive value - foreach (var prop in result) + return result; + } + + public IEnumerable MapMembershipProperties(IMember member, MapperContext? context) + { + var properties = new List + { + GetLoginProperty(member, _localizedTextService), + new() { - // check if this property is flagged as sensitive - var isSensitiveProperty = memberType?.IsSensitiveProperty(prop.Alias) ?? false; - // check permissions for viewing sensitive data - if (isSensitiveProperty && (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() == false)) + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", + Label = _localizedTextService.Localize("general","email"), + Value = member.Email, + View = "email", + Validation = { Mandatory = true } + }, + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", + Label = _localizedTextService.Localize(null,"password"), + Value = new Dictionary { - // mark this property as sensitive - prop.IsSensitive = true; - // mark this property as readonly so that it does not post any data - prop.Readonly = true; - // replace this editor with a sensitive value - prop.View = "sensitivevalue"; - // clear the value - prop.Value = null; + // TODO: why ignoreCase, what are we doing here?! + { "newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null) } + }, + View = "changepassword", + Config = GetPasswordConfig(member) // Initialize the dictionary with the configuration from the default membership provider + }, + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", + Label = _localizedTextService.Localize("content","membergroup"), + Value = GetMemberGroupValue(member.Username), + View = "membergroups", + Config = new Dictionary + { + { "IsRequired", true } + }, + }, + + // These properties used to live on the member as property data, defaulting to sensitive, so we set them to sensitive here too + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}failedPasswordAttempts", + Label = _localizedTextService.Localize("user", "failedPasswordAttempts"), + Value = member.FailedPasswordAttempts, + View = "readonlyvalue", + IsSensitive = true, + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}approved", + Label = _localizedTextService.Localize("user", "stateApproved"), + Value = member.IsApproved, + View = "boolean", + IsSensitive = true, + Readonly = false, + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lockedOut", + Label = _localizedTextService.Localize("user", "stateLockedOut"), + Value = member.IsLockedOut, + View = "boolean", + IsSensitive = true, + Readonly = !member.IsLockedOut, // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock) + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLockoutDate", + Label = _localizedTextService.Localize("user", "lastLockoutDate"), + Value = member.LastLockoutDate?.ToString(), + View = "readonlyvalue", + IsSensitive = true, + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLoginDate", + Label = _localizedTextService.Localize("user", "lastLogin"), + Value = member.LastLoginDate?.ToString(), + View = "readonlyvalue", + IsSensitive = true, + }, + + new() + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastPasswordChangeDate", + Label = _localizedTextService.Localize("user", "lastPasswordChangeDate"), + Value = member.LastPasswordChangeDate?.ToString(), + View = "readonlyvalue", + IsSensitive = true, + }, + }; + + if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() is false) + { + // Current user doesn't have access to sensitive data so explicitly set the views and remove the value from sensitive data + foreach (ContentPropertyDisplay property in properties) + { + if (property.IsSensitive) + { + property.Value = null; + property.View = "sensitivevalue"; + property.Readonly = true; } } - return result; } - /// - /// Returns the login property display field - /// - /// - /// - /// - /// - /// - /// If the membership provider installed is the umbraco membership provider, then we will allow changing the username, however if - /// the membership provider is a custom one, we cannot allow changing the username because MembershipProvider's do not actually natively - /// allow that. - /// - internal static ContentPropertyDisplay GetLoginProperty(IMember member, ILocalizedTextService localizedText) - { - var prop = new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login", - Label = localizedText.Localize(null,"login"), - Value = member.Username - }; - - prop.View = "textbox"; - prop.Validation.Mandatory = true; - return prop; - } - - internal IDictionary GetMemberGroupValue(string username) - { - IEnumerable userRoles = _memberService.GetAllRoles(username); - - // create a dictionary of all roles (except internal roles) + "false" - var result = _memberGroupService.GetAll() - .Select(x => x.Name!) - // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access - .Where(x => x?.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToDictionary(x => x, x => false); - - // if user has no roles, just return the dictionary - if (userRoles == null) - { - return result; - } - - // else update the dictionary to "true" for the user roles (except internal roles) - foreach (var userRole in userRoles.Where(x => x?.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false)) - { - result[userRole] = true; - } - - return result; - } - - public IEnumerable MapMembershipProperties(IMember member, MapperContext? context) - { - var properties = new List - { - GetLoginProperty(member, _localizedTextService), - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", - Label = _localizedTextService.Localize("general","email"), - Value = member.Email, - View = "email", - Validation = { Mandatory = true } - }, - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", - Label = _localizedTextService.Localize(null,"password"), - Value = new Dictionary - { - // TODO: why ignoreCase, what are we doing here?! - { "newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null) } - }, - View = "changepassword", - Config = GetPasswordConfig(member) // Initialize the dictionary with the configuration from the default membership provider - }, - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", - Label = _localizedTextService.Localize("content","membergroup"), - Value = GetMemberGroupValue(member.Username), - View = "membergroups", - Config = new Dictionary - { - { "IsRequired", true } - }, - }, - - // These properties used to live on the member as property data, defaulting to sensitive, so we set them to sensitive here too - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}failedPasswordAttempts", - Label = _localizedTextService.Localize("user", "failedPasswordAttempts"), - Value = member.FailedPasswordAttempts, - View = "readonlyvalue", - IsSensitive = true, - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}approved", - Label = _localizedTextService.Localize("user", "stateApproved"), - Value = member.IsApproved, - View = "boolean", - IsSensitive = true, - Readonly = false, - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lockedOut", - Label = _localizedTextService.Localize("user", "stateLockedOut"), - Value = member.IsLockedOut, - View = "boolean", - IsSensitive = true, - Readonly = !member.IsLockedOut, // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock) - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLockoutDate", - Label = _localizedTextService.Localize("user", "lastLockoutDate"), - Value = member.LastLockoutDate?.ToString(), - View = "readonlyvalue", - IsSensitive = true, - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLoginDate", - Label = _localizedTextService.Localize("user", "lastLogin"), - Value = member.LastLoginDate?.ToString(), - View = "readonlyvalue", - IsSensitive = true, - }, - - new() - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastPasswordChangeDate", - Label = _localizedTextService.Localize("user", "lastPasswordChangeDate"), - Value = member.LastPasswordChangeDate?.ToString(), - View = "readonlyvalue", - IsSensitive = true, - }, - }; - - if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() is false) - { - // Current user doesn't have access to sensitive data so explicitly set the views and remove the value from sensitive data - foreach (var property in properties) - { - if (property.IsSensitive) - { - property.Value = null; - property.View = "sensitivevalue"; - property.Readonly = true; - } - } - } - - return properties; - } + return properties; } } diff --git a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs index 5d4d9ba485..cb77d790cd 100644 --- a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; @@ -8,258 +5,291 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class PropertyTypeGroupMapper + where TPropertyType : PropertyTypeDisplay, new() { - public class PropertyTypeGroupMapper - where TPropertyType : PropertyTypeDisplay, new() + private readonly IDataTypeService _dataTypeService; + private readonly ILogger> _logger; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IShortStringHelper _shortStringHelper; + + public PropertyTypeGroupMapper(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ILogger> logger) { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly IShortStringHelper _shortStringHelper; - private readonly ILogger> _logger; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _shortStringHelper = shortStringHelper; + _logger = logger; + } - public PropertyTypeGroupMapper(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ILogger> logger) + public IEnumerable> Map(IContentTypeComposition source) + { + // deal with groups + var groups = new List>(); + + // add groups local to this content type + foreach (PropertyGroup propertyGroup in source.PropertyGroups) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _shortStringHelper = shortStringHelper; - _logger = logger; + var group = new PropertyGroupDisplay + { + Id = propertyGroup.Id, + Key = propertyGroup.Key, + Type = propertyGroup.Type, + Name = propertyGroup.Name, + Alias = propertyGroup.Alias, + SortOrder = propertyGroup.SortOrder, + Properties = MapProperties(propertyGroup.PropertyTypes, source, propertyGroup.Id, false), + ContentTypeId = source.Id, + }; + + groups.Add(group); } - /// - /// Gets the content type that defines a property group, within a composition. - /// - /// The composition. - /// The identifier of the property group. - /// The composition content type that defines the specified property group. - private static IContentTypeComposition? GetContentTypeForPropertyGroup(IContentTypeComposition contentType, int propertyGroupId) + // add groups inherited through composition + var localGroupIds = groups.Select(x => x.Id).ToArray(); + foreach (PropertyGroup propertyGroup in source.CompositionPropertyGroups) { - // test local groups - if (contentType.PropertyGroups.Any(x => x.Id == propertyGroupId)) - return contentType; + // skip those that are local to this content type + if (localGroupIds.Contains(propertyGroup.Id)) + { + continue; + } - // test composition types groups - // .ContentTypeComposition is just the local ones, not recursive, - // so we have to recurse here - return contentType.ContentTypeComposition - .Select(x => GetContentTypeForPropertyGroup(x, propertyGroupId)) - .FirstOrDefault(x => x != null); + // get the content type that defines this group + IContentTypeComposition? definingContentType = GetContentTypeForPropertyGroup(source, propertyGroup.Id); + if (definingContentType == null) + { + throw new Exception("PropertyGroup with id=" + propertyGroup.Id + + " was not found on any of the content type's compositions."); + } + + var group = new PropertyGroupDisplay + { + Inherited = true, + Id = propertyGroup.Id, + Key = propertyGroup.Key, + Type = propertyGroup.Type, + Name = propertyGroup.Name, + Alias = propertyGroup.Alias, + SortOrder = propertyGroup.SortOrder, + Properties = + MapProperties(propertyGroup.PropertyTypes, definingContentType, propertyGroup.Id, true), + ContentTypeId = definingContentType.Id, + ParentTabContentTypes = new[] { definingContentType.Id }, + ParentTabContentTypeNames = new[] { definingContentType.Name }, + }; + + groups.Add(group); } - /// - /// Gets the content type that defines a property group, within a composition. - /// - /// The composition. - /// The identifier of the property type. - /// The composition content type that defines the specified property group. - private static IContentTypeComposition? GetContentTypeForPropertyType(IContentTypeComposition contentType, int propertyTypeId) - { - // test local property types - if (contentType.PropertyTypes.Any(x => x.Id == propertyTypeId)) - return contentType; + // deal with generic properties + var genericProperties = new List(); - // test composition property types - // .ContentTypeComposition is just the local ones, not recursive, - // so we have to recurse here - return contentType.ContentTypeComposition - .Select(x => GetContentTypeForPropertyType(x, propertyTypeId)) - .FirstOrDefault(x => x != null); + // add generic properties local to this content type + IEnumerable entityGenericProperties = source.PropertyTypes.Where(x => x.PropertyGroupId == null); + genericProperties.AddRange(MapProperties(entityGenericProperties, source, PropertyGroupBasic.GenericPropertiesGroupId, false)); + + // add generic properties inherited through compositions + var localGenericPropertyIds = genericProperties.Select(x => x.Id).ToArray(); + IEnumerable compositionGenericProperties = source.CompositionPropertyTypes + .Where(x => x.PropertyGroupId == null // generic + && localGenericPropertyIds.Contains(x.Id) == false); // skip those that are local + foreach (IPropertyType compositionGenericProperty in compositionGenericProperties) + { + IContentTypeComposition? definingContentType = + GetContentTypeForPropertyType(source, compositionGenericProperty.Id); + if (definingContentType == null) + { + throw new Exception("PropertyType with id=" + compositionGenericProperty.Id + + " was not found on any of the content type's compositions."); + } + + genericProperties.AddRange(MapProperties(new[] { compositionGenericProperty }, definingContentType, PropertyGroupBasic.GenericPropertiesGroupId, true)); } - public IEnumerable> Map(IContentTypeComposition source) + // if there are any generic properties, add the corresponding tab + if (genericProperties.Any()) { - // deal with groups - var groups = new List>(); - - // add groups local to this content type - foreach (var propertyGroup in source.PropertyGroups) + var genericGroup = new PropertyGroupDisplay { - var group = new PropertyGroupDisplay - { - Id = propertyGroup.Id, - Key = propertyGroup.Key, - Type = propertyGroup.Type, - Name = propertyGroup.Name, - Alias = propertyGroup.Alias, - SortOrder = propertyGroup.SortOrder, - Properties = MapProperties(propertyGroup.PropertyTypes, source, propertyGroup.Id, false), - ContentTypeId = source.Id - }; + Id = PropertyGroupBasic.GenericPropertiesGroupId, + Name = "Generic properties", + Alias = "genericProperties", + SortOrder = 999, + Properties = genericProperties, + ContentTypeId = source.Id, + }; - groups.Add(group); - } - - // add groups inherited through composition - var localGroupIds = groups.Select(x => x.Id).ToArray(); - foreach (var propertyGroup in source.CompositionPropertyGroups) - { - // skip those that are local to this content type - if (localGroupIds.Contains(propertyGroup.Id)) continue; - - // get the content type that defines this group - var definingContentType = GetContentTypeForPropertyGroup(source, propertyGroup.Id); - if (definingContentType == null) - throw new Exception("PropertyGroup with id=" + propertyGroup.Id + " was not found on any of the content type's compositions."); - - var group = new PropertyGroupDisplay - { - Inherited = true, - Id = propertyGroup.Id, - Key = propertyGroup.Key, - Type = propertyGroup.Type, - Name = propertyGroup.Name, - Alias = propertyGroup.Alias, - SortOrder = propertyGroup.SortOrder, - Properties = MapProperties(propertyGroup.PropertyTypes, definingContentType, propertyGroup.Id, true), - ContentTypeId = definingContentType.Id, - ParentTabContentTypes = new[] { definingContentType.Id }, - ParentTabContentTypeNames = new[] { definingContentType.Name } - }; - - groups.Add(group); - } - - // deal with generic properties - var genericProperties = new List(); - - // add generic properties local to this content type - var entityGenericProperties = source.PropertyTypes.Where(x => x.PropertyGroupId == null); - genericProperties.AddRange(MapProperties(entityGenericProperties, source, PropertyGroupBasic.GenericPropertiesGroupId, false)); - - // add generic properties inherited through compositions - var localGenericPropertyIds = genericProperties.Select(x => x.Id).ToArray(); - var compositionGenericProperties = source.CompositionPropertyTypes - .Where(x => x.PropertyGroupId == null // generic - && localGenericPropertyIds.Contains(x.Id) == false); // skip those that are local - foreach (var compositionGenericProperty in compositionGenericProperties) - { - var definingContentType = GetContentTypeForPropertyType(source, compositionGenericProperty.Id); - if (definingContentType == null) - throw new Exception("PropertyType with id=" + compositionGenericProperty.Id + " was not found on any of the content type's compositions."); - genericProperties.AddRange(MapProperties(new[] { compositionGenericProperty }, definingContentType, PropertyGroupBasic.GenericPropertiesGroupId, true)); - } - - // if there are any generic properties, add the corresponding tab - if (genericProperties.Any()) - { - var genericGroup = new PropertyGroupDisplay - { - Id = PropertyGroupBasic.GenericPropertiesGroupId, - Name = "Generic properties", - Alias = "genericProperties", - SortOrder = 999, - Properties = genericProperties, - ContentTypeId = source.Id - }; - - groups.Add(genericGroup); - } - - // handle locked properties - var lockedPropertyAliases = new List(); - // add built-in member property aliases to list of aliases to be locked - foreach (var propertyAlias in ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Keys) - { - lockedPropertyAliases.Add(propertyAlias); - } - // lock properties by aliases - foreach (var property in groups.SelectMany(x => x.Properties)) - { - if (property.Alias is not null) - { - property.Locked = lockedPropertyAliases.Contains(property.Alias); - } - } - - // now merge tabs based on alias - // as for one name, we might have one local tab, plus some inherited tabs - var groupsGroupsByAlias = groups.GroupBy(x => x.Alias).ToArray(); - groups = new List>(); // start with a fresh list - foreach (var groupsByAlias in groupsGroupsByAlias) - { - // single group, just use it - if (groupsByAlias.Count() == 1) - { - groups.Add(groupsByAlias.First()); - continue; - } - - // multiple groups, merge - var group = groupsByAlias.FirstOrDefault(x => x.Inherited == false) // try local - ?? groupsByAlias.First(); // else pick one randomly - groups.Add(group); - - // in case we use the local one, flag as inherited - group.Inherited = true; // TODO Remove to allow changing sort order of the local one (and use the inherited group order below) - - // merge (and sort) properties - var properties = groupsByAlias.SelectMany(x => x.Properties).OrderBy(x => x.SortOrder).ToArray(); - group.Properties = properties; - - // collect parent group info - var parentGroups = groupsByAlias.Where(x => x.ContentTypeId != source.Id).ToArray(); - group.ParentTabContentTypes = parentGroups.SelectMany(x => x.ParentTabContentTypes).ToArray(); - group.ParentTabContentTypeNames = parentGroups.SelectMany(x => x.ParentTabContentTypeNames).ToArray(); - } - - return groups.OrderBy(x => x.SortOrder); + groups.Add(genericGroup); } - private IEnumerable MapProperties(IEnumerable? properties, IContentTypeBase contentType, int groupId, bool inherited) + // handle locked properties + var lockedPropertyAliases = new List(); + + // add built-in member property aliases to list of aliases to be locked + foreach (var propertyAlias in ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Keys) { - var mappedProperties = new List(); + lockedPropertyAliases.Add(propertyAlias); + } - foreach (var p in properties?.Where(x => x.DataTypeId != 0).OrderBy(x => x.SortOrder) ?? Enumerable.Empty()) + // lock properties by aliases + foreach (TPropertyType property in groups.SelectMany(x => x.Properties)) + { + if (property.Alias is not null) { - var propertyEditorAlias = p.PropertyEditorAlias; - var propertyEditor = _propertyEditors[propertyEditorAlias]; - var dataType = _dataTypeService.GetDataType(p.DataTypeId); + property.Locked = lockedPropertyAliases.Contains(property.Alias); + } + } - //fixme: Don't explode if we can't find this, log an error and change this to a label - if (propertyEditor == null) - { - _logger.LogError("No property editor could be resolved with the alias: {PropertyEditorAlias}, defaulting to label", p.PropertyEditorAlias); - propertyEditorAlias = Constants.PropertyEditors.Aliases.Label; - propertyEditor = _propertyEditors[propertyEditorAlias]; - } - - var config = propertyEditor is null || dataType is null - ? new Dictionary() - : dataType.Editor?.GetConfigurationEditor().ToConfigurationEditor(dataType.Configuration); - - mappedProperties.Add(new TPropertyType - { - Id = p.Id, - Alias = p.Alias, - Description = p.Description, - LabelOnTop = p.LabelOnTop, - Editor = p.PropertyEditorAlias, - Validation = new PropertyTypeValidation - { - Mandatory = p.Mandatory, - MandatoryMessage = p.MandatoryMessage, - Pattern = p.ValidationRegExp, - PatternMessage = p.ValidationRegExpMessage, - }, - Label = p.Name, - View = propertyEditor?.GetValueEditor().View, - Config = config, - //Value = "", - GroupId = groupId, - Inherited = inherited, - DataTypeId = p.DataTypeId, - DataTypeKey = p.DataTypeKey, - DataTypeName = dataType?.Name, - DataTypeIcon = propertyEditor?.Icon, - SortOrder = p.SortOrder, - ContentTypeId = contentType.Id, - ContentTypeName = contentType.Name, - AllowCultureVariant = p.VariesByCulture(), - AllowSegmentVariant = p.VariesBySegment() - }); + // now merge tabs based on alias + // as for one name, we might have one local tab, plus some inherited tabs + IGrouping>[] groupsGroupsByAlias = + groups.GroupBy(x => x.Alias).ToArray(); + groups = new List>(); // start with a fresh list + foreach (IGrouping> groupsByAlias in groupsGroupsByAlias) + { + // single group, just use it + if (groupsByAlias.Count() == 1) + { + groups.Add(groupsByAlias.First()); + continue; } - return mappedProperties; + // multiple groups, merge + PropertyGroupDisplay group = + groupsByAlias.FirstOrDefault(x => x.Inherited == false) // try local + ?? groupsByAlias.First(); // else pick one randomly + groups.Add(group); + + // in case we use the local one, flag as inherited + group.Inherited = + true; // TODO Remove to allow changing sort order of the local one (and use the inherited group order below) + + // merge (and sort) properties + TPropertyType[] properties = + groupsByAlias.SelectMany(x => x.Properties).OrderBy(x => x.SortOrder).ToArray(); + group.Properties = properties; + + // collect parent group info + PropertyGroupDisplay[] parentGroups = + groupsByAlias.Where(x => x.ContentTypeId != source.Id).ToArray(); + group.ParentTabContentTypes = parentGroups.SelectMany(x => x.ParentTabContentTypes).ToArray(); + group.ParentTabContentTypeNames = parentGroups.SelectMany(x => x.ParentTabContentTypeNames).ToArray(); } + + return groups.OrderBy(x => x.SortOrder); + } + + /// + /// Gets the content type that defines a property group, within a composition. + /// + /// The composition. + /// The identifier of the property group. + /// The composition content type that defines the specified property group. + private static IContentTypeComposition? GetContentTypeForPropertyGroup( + IContentTypeComposition contentType, + int propertyGroupId) + { + // test local groups + if (contentType.PropertyGroups.Any(x => x.Id == propertyGroupId)) + { + return contentType; + } + + // test composition types groups + // .ContentTypeComposition is just the local ones, not recursive, + // so we have to recurse here + return contentType.ContentTypeComposition + .Select(x => GetContentTypeForPropertyGroup(x, propertyGroupId)) + .FirstOrDefault(x => x != null); + } + + /// + /// Gets the content type that defines a property group, within a composition. + /// + /// The composition. + /// The identifier of the property type. + /// The composition content type that defines the specified property group. + private static IContentTypeComposition? GetContentTypeForPropertyType( + IContentTypeComposition contentType, + int propertyTypeId) + { + // test local property types + if (contentType.PropertyTypes.Any(x => x.Id == propertyTypeId)) + { + return contentType; + } + + // test composition property types + // .ContentTypeComposition is just the local ones, not recursive, + // so we have to recurse here + return contentType.ContentTypeComposition + .Select(x => GetContentTypeForPropertyType(x, propertyTypeId)) + .FirstOrDefault(x => x != null); + } + + private IEnumerable MapProperties( + IEnumerable? properties, + IContentTypeBase contentType, + int groupId, + bool inherited) + { + var mappedProperties = new List(); + + foreach (IPropertyType p in properties?.Where(x => x.DataTypeId != 0).OrderBy(x => x.SortOrder) ?? + Enumerable.Empty()) + { + var propertyEditorAlias = p.PropertyEditorAlias; + IDataEditor? propertyEditor = _propertyEditors[propertyEditorAlias]; + IDataType? dataType = _dataTypeService.GetDataType(p.DataTypeId); + + // fixme: Don't explode if we can't find this, log an error and change this to a label + if (propertyEditor == null) + { + _logger.LogError( + "No property editor could be resolved with the alias: {PropertyEditorAlias}, defaulting to label", + p.PropertyEditorAlias); + propertyEditorAlias = Constants.PropertyEditors.Aliases.Label; + propertyEditor = _propertyEditors[propertyEditorAlias]; + } + + IDictionary? config = propertyEditor is null || dataType is null ? new Dictionary() + : dataType.Editor?.GetConfigurationEditor().ToConfigurationEditor(dataType.Configuration); + + mappedProperties.Add(new TPropertyType + { + Id = p.Id, + Alias = p.Alias, + Description = p.Description, + LabelOnTop = p.LabelOnTop, + Editor = p.PropertyEditorAlias, + Validation = new PropertyTypeValidation + { + Mandatory = p.Mandatory, + MandatoryMessage = p.MandatoryMessage, + Pattern = p.ValidationRegExp, + PatternMessage = p.ValidationRegExpMessage, + }, + Label = p.Name, + View = propertyEditor?.GetValueEditor().View, + Config = config, + + // Value = "", + GroupId = groupId, + Inherited = inherited, + DataTypeId = p.DataTypeId, + DataTypeKey = p.DataTypeKey, + DataTypeName = dataType?.Name, + DataTypeIcon = propertyEditor?.Icon, + SortOrder = p.SortOrder, + ContentTypeId = contentType.Id, + ContentTypeName = contentType.Name, + AllowCultureVariant = p.VariesByCulture(), + AllowSegmentVariant = p.VariesBySegment(), + }); + } + + return mappedProperties; } } diff --git a/src/Umbraco.Core/Models/Mapping/RedirectUrlMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RedirectUrlMapDefinition.cs index f4715b3a6b..148470c706 100644 --- a/src/Umbraco.Core/Models/Mapping/RedirectUrlMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/RedirectUrlMapDefinition.cs @@ -1,32 +1,29 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class RedirectUrlMapDefinition : IMapDefinition { - public class RedirectUrlMapDefinition : IMapDefinition + private readonly IPublishedUrlProvider _publishedUrlProvider; + + public RedirectUrlMapDefinition(IPublishedUrlProvider publishedUrlProvider) => + _publishedUrlProvider = publishedUrlProvider; + + public void DefineMaps(IUmbracoMapper mapper) => + mapper.Define((source, context) => new ContentRedirectUrl(), Map); + + // Umbraco.Code.MapAll + private void Map(IRedirectUrl source, ContentRedirectUrl target, MapperContext context) { - private readonly IPublishedUrlProvider _publishedUrlProvider; - - public RedirectUrlMapDefinition(IPublishedUrlProvider publishedUrlProvider) - { - _publishedUrlProvider = publishedUrlProvider; - } - - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new ContentRedirectUrl(), Map); - } - - // Umbraco.Code.MapAll - private void Map(IRedirectUrl source, ContentRedirectUrl target, MapperContext context) - { - target.ContentId = source.ContentId; - target.CreateDateUtc = source.CreateDateUtc; - target.Culture = source.Culture; - target.DestinationUrl = source.ContentId > 0 ? _publishedUrlProvider?.GetUrl(source.ContentId, culture: source.Culture) : "#"; - target.OriginalUrl = _publishedUrlProvider?.GetUrlFromRoute(source.ContentId, source.Url, source.Culture); - target.RedirectId = source.Key; - } + target.ContentId = source.ContentId; + target.CreateDateUtc = source.CreateDateUtc; + target.Culture = source.Culture; + target.DestinationUrl = source.ContentId > 0 + ? _publishedUrlProvider?.GetUrl(source.ContentId, culture: source.Culture) + : "#"; + target.OriginalUrl = _publishedUrlProvider?.GetUrlFromRoute(source.ContentId, source.Url, source.Culture); + target.RedirectId = source.Key; } } diff --git a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs index b0aaab9537..d565836847 100644 --- a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs @@ -1,95 +1,96 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class RelationMapDefinition : IMapDefinition { - public class RelationMapDefinition : IMapDefinition + private readonly IEntityService _entityService; + private readonly IRelationService _relationService; + + public RelationMapDefinition(IEntityService entityService, IRelationService relationService) { - private readonly IEntityService _entityService; - private readonly IRelationService _relationService; + _entityService = entityService; + _relationService = relationService; + } - public RelationMapDefinition(IEntityService entityService, IRelationService relationService) + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new RelationTypeDisplay(), Map); + mapper.Define((source, context) => new RelationDisplay(), Map); + mapper.Define(Map); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + private static void Map(RelationTypeSave source, IRelationType target, MapperContext context) + { + target.Alias = source.Alias; + target.ChildObjectType = source.ChildObjectType; + target.Id = source.Id.TryConvertTo().Result; + target.IsBidirectional = source.IsBidirectional; + if (target is IRelationTypeWithIsDependency targetWithIsDependency) { - _entityService = entityService; - _relationService = relationService; + targetWithIsDependency.IsDependency = source.IsDependency; } - public void DefineMaps(IUmbracoMapper mapper) + target.Key = source.Key; + target.Name = source.Name; + target.ParentObjectType = source.ParentObjectType; + } + + // Umbraco.Code.MapAll -Icon -Trashed -AdditionalData + // Umbraco.Code.MapAll -ParentId -Notifications + private void Map(IRelationType source, RelationTypeDisplay target, MapperContext context) + { + target.ChildObjectType = source.ChildObjectType; + target.Id = source.Id; + target.IsBidirectional = source.IsBidirectional; + + if (source is IRelationTypeWithIsDependency sourceWithIsDependency) { - mapper.Define((source, context) => new RelationTypeDisplay(), Map); - mapper.Define((source, context) => new RelationDisplay(), Map); - mapper.Define(Map); + target.IsDependency = sourceWithIsDependency.IsDependency; } - // Umbraco.Code.MapAll -Icon -Trashed -AdditionalData - // Umbraco.Code.MapAll -ParentId -Notifications - private void Map(IRelationType source, RelationTypeDisplay target, MapperContext context) + target.Key = source.Key; + target.Name = source.Name; + target.Alias = source.Alias; + target.ParentObjectType = source.ParentObjectType; + target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); + target.Path = "-1," + source.Id; + + target.IsSystemRelationType = source.IsSystemRelationType(); + + // Set the "friendly" and entity names for the parent and child object types + if (source.ParentObjectType.HasValue) { - target.ChildObjectType = source.ChildObjectType; - target.Id = source.Id; - target.IsBidirectional = source.IsBidirectional; - - if (source is IRelationTypeWithIsDependency sourceWithIsDependency) - { - target.IsDependency = sourceWithIsDependency.IsDependency; - } - target.Key = source.Key; - target.Name = source.Name; - target.Alias = source.Alias; - target.ParentObjectType = source.ParentObjectType; - target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); - target.Path = "-1," + source.Id; - - target.IsSystemRelationType = source.IsSystemRelationType(); - - // Set the "friendly" and entity names for the parent and child object types - if (source.ParentObjectType.HasValue) - { - var objType = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType.Value); - target.ParentObjectTypeName = objType.GetFriendlyName(); - } - - if (source.ChildObjectType.HasValue) - { - var objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); - target.ChildObjectTypeName = objType.GetFriendlyName(); - } + UmbracoObjectTypes objType = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType.Value); + target.ParentObjectTypeName = objType.GetFriendlyName(); } - // Umbraco.Code.MapAll -ParentName -ChildName - private void Map(IRelation source, RelationDisplay target, MapperContext context) + if (source.ChildObjectType.HasValue) { - target.ChildId = source.ChildId; - target.Comment = source.Comment; - target.CreateDate = source.CreateDate; - target.ParentId = source.ParentId; - - var entities = _relationService.GetEntitiesFromRelation(source); - - if (entities is not null) - { - target.ParentName = entities.Item1.Name; - target.ChildName = entities.Item2.Name; - } + UmbracoObjectTypes objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); + target.ChildObjectTypeName = objType.GetFriendlyName(); } + } - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - private static void Map(RelationTypeSave source, IRelationType target, MapperContext context) + // Umbraco.Code.MapAll -ParentName -ChildName + private void Map(IRelation source, RelationDisplay target, MapperContext context) + { + target.ChildId = source.ChildId; + target.Comment = source.Comment; + target.CreateDate = source.CreateDate; + target.ParentId = source.ParentId; + + Tuple? entities = _relationService.GetEntitiesFromRelation(source); + + if (entities is not null) { - target.Alias = source.Alias; - target.ChildObjectType = source.ChildObjectType; - target.Id = source.Id.TryConvertTo().Result; - target.IsBidirectional = source.IsBidirectional; - if (target is IRelationTypeWithIsDependency targetWithIsDependency) - { - targetWithIsDependency.IsDependency = source.IsDependency; - } - - target.Key = source.Key; - target.Name = source.Name; - target.ParentObjectType = source.ParentObjectType; + target.ParentName = entities.Item1.Name; + target.ChildName = entities.Item2.Name; } } } diff --git a/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs index b7bdbccd26..c64af5ac0a 100644 --- a/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs @@ -1,48 +1,45 @@ -using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Sections; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class SectionMapDefinition : IMapDefinition { - public class SectionMapDefinition : IMapDefinition + private readonly ILocalizedTextService _textService; + + public SectionMapDefinition(ILocalizedTextService textService) => _textService = textService; + + public void DefineMaps(IUmbracoMapper mapper) { - private readonly ILocalizedTextService _textService; - public SectionMapDefinition(ILocalizedTextService textService) - { - _textService = textService; - } + mapper.Define((source, context) => new Section(), Map); - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new Section(), Map); + // this is for AutoMapper ReverseMap - but really? + mapper.Define(); + mapper.Define(); + mapper.Define(Map); + mapper.Define(); + mapper.Define(); + mapper.Define(); + mapper.Define(); + mapper.Define(); + mapper.Define(); + } - // this is for AutoMapper ReverseMap - but really? - mapper.Define(); - mapper.Define(); - mapper.Define(Map); - mapper.Define(); - mapper.Define(); - mapper.Define(); - mapper.Define(); - mapper.Define(); - mapper.Define(); - } + // Umbraco.Code.MapAll + private static void Map(Section source, ManifestSection target, MapperContext context) + { + target.Alias = source.Alias; + target.Name = source.Name; + } - // Umbraco.Code.MapAll -RoutePath - private void Map(ISection source, Section target, MapperContext context) - { - target.Alias = source.Alias; - target.Name = _textService.Localize("sections", source.Alias); - } - - // Umbraco.Code.MapAll - private static void Map(Section source, ManifestSection target, MapperContext context) - { - target.Alias = source.Alias; - target.Name = source.Name; - } + // Umbraco.Code.MapAll -RoutePath + private void Map(ISection source, Section target, MapperContext context) + { + target.Alias = source.Alias; + target.Name = _textService.Localize("sections", source.Alias); } } diff --git a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs index be4b6bae61..42ea05e8f9 100644 --- a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs @@ -1,159 +1,156 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public abstract class TabsAndPropertiesMapper { - public abstract class TabsAndPropertiesMapper + protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService) + : this(cultureDictionary, localizedTextService, new List()) { - protected ICultureDictionary CultureDictionary { get; } - protected ILocalizedTextService LocalizedTextService { get; } - protected IEnumerable IgnoreProperties { get; set; } + } - protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService) - : this(cultureDictionary, localizedTextService, new List()) - { } + protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IEnumerable ignoreProperties) + { + CultureDictionary = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary)); + LocalizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + IgnoreProperties = ignoreProperties ?? throw new ArgumentNullException(nameof(ignoreProperties)); + } - protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IEnumerable ignoreProperties) + protected ICultureDictionary CultureDictionary { get; } + + protected ILocalizedTextService LocalizedTextService { get; } + + protected IEnumerable IgnoreProperties { get; set; } + + /// + /// Returns a collection of custom generic properties that exist on the generic properties tab + /// + /// + protected virtual IEnumerable GetCustomGenericProperties(IContentBase content) => + Enumerable.Empty(); + + /// + /// Maps properties on to the generic properties tab + /// + /// + /// + /// + /// + /// The generic properties tab is responsible for + /// setting up the properties such as Created date, updated date, template selected, etc... + /// + protected virtual void MapGenericProperties(IContentBase content, List> tabs, MapperContext context) + { + // add the generic properties tab, for properties that don't belong to a tab + // get the properties, map and translate them, then add the tab + var noGroupProperties = content.GetNonGroupedProperties() + .Where(x => IgnoreProperties.Contains(x.Alias) == false) // skip ignored + .ToList(); + List genericProperties = MapProperties(content, noGroupProperties, context); + + IEnumerable customProperties = GetCustomGenericProperties(content); + if (customProperties != null) { - CultureDictionary = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary)); - LocalizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); - IgnoreProperties = ignoreProperties ?? throw new ArgumentNullException(nameof(ignoreProperties)); + genericProperties.AddRange(customProperties); } - /// - /// Returns a collection of custom generic properties that exist on the generic properties tab - /// - /// - protected virtual IEnumerable GetCustomGenericProperties(IContentBase content) + if (genericProperties.Count > 0) { - return Enumerable.Empty(); - } - - /// - /// Maps properties on to the generic properties tab - /// - /// - /// - /// - /// - /// The generic properties tab is responsible for - /// setting up the properties such as Created date, updated date, template selected, etc... - /// - protected virtual void MapGenericProperties(IContentBase content, List> tabs, MapperContext context) - { - // add the generic properties tab, for properties that don't belong to a tab - // get the properties, map and translate them, then add the tab - var noGroupProperties = content.GetNonGroupedProperties() - .Where(x => IgnoreProperties.Contains(x.Alias) == false) // skip ignored - .ToList(); - var genericProperties = MapProperties(content, noGroupProperties, context); - - - - var customProperties = GetCustomGenericProperties(content); - if (customProperties != null) + tabs.Add(new Tab { - genericProperties.AddRange(customProperties); - } - - if (genericProperties.Count > 0) - { - tabs.Add(new Tab - { - Id = 0, - Label = LocalizedTextService.Localize("general", "properties"), - Alias = "Generic properties", - Properties = genericProperties - }); - } - } - - /// - /// Maps a list of to a list of - /// - /// - /// - /// - /// - protected virtual List MapProperties(IContentBase content, List properties, MapperContext context) - { - return context.MapEnumerable(properties.OrderBy(x => x.PropertyType?.SortOrder)).WhereNotNull().ToList(); + Id = 0, + Label = LocalizedTextService.Localize("general", "properties"), + Alias = "Generic properties", + Properties = genericProperties, + }); } } /// - /// Creates the tabs collection with properties assigned for display models + /// Maps a list of to a list of /// - public class TabsAndPropertiesMapper : TabsAndPropertiesMapper - where TSource : IContentBase + /// + /// + /// + /// + protected virtual List MapProperties(IContentBase content, List properties, MapperContext context) => + context.MapEnumerable(properties.OrderBy(x => x.PropertyType?.SortOrder)) + .WhereNotNull().ToList(); +} + +/// +/// Creates the tabs collection with properties assigned for display models +/// +public class TabsAndPropertiesMapper : TabsAndPropertiesMapper + where TSource : IContentBase +{ + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + + public TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + : base(cultureDictionary, localizedTextService) => + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? + throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider)); + + public virtual IEnumerable> Map(TSource source, MapperContext context) { - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + var tabs = new List>(); - public TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) - : base(cultureDictionary, localizedTextService) + // Property groups only exist on the content type (as it's only used for display purposes) + IContentTypeComposition? contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); + + // Merge the groups, as compositions can introduce duplicate aliases + PropertyGroup[]? groups = contentType?.CompositionPropertyGroups.OrderBy(x => x.SortOrder).ToArray(); + var parentAliases = groups?.Select(x => x.GetParentAlias()).Distinct().ToArray(); + if (groups is not null) { - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider)); - } - - public virtual IEnumerable> Map(TSource source, MapperContext context) - { - var tabs = new List>(); - - // Property groups only exist on the content type (as it's only used for display purposes) - var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source); - - // Merge the groups, as compositions can introduce duplicate aliases - var groups = contentType?.CompositionPropertyGroups.OrderBy(x => x.SortOrder).ToArray(); - var parentAliases = groups?.Select(x => x.GetParentAlias()).Distinct().ToArray(); - if (groups is not null) + foreach (IGrouping groupsByAlias in groups.GroupBy(x => x.Alias)) { - foreach (var groupsByAlias in groups.GroupBy(x => x.Alias)) + var properties = new List(); + + // Merge properties for groups with the same alias + foreach (PropertyGroup group in groupsByAlias) { - var properties = new List(); + IEnumerable groupProperties = source.GetPropertiesForGroup(group) + .Where(x => IgnoreProperties.Contains(x.Alias) == false); // Skip ignored properties - // Merge properties for groups with the same alias - foreach (var group in groupsByAlias) - { - var groupProperties = source.GetPropertiesForGroup(group) - .Where(x => IgnoreProperties.Contains(x.Alias) == false); // Skip ignored properties - - properties.AddRange(groupProperties); - } - - if (properties.Count == 0 && (!parentAliases?.Contains(groupsByAlias.Key) ?? false)) - continue; - - // Map the properties - var mappedProperties = MapProperties(source, properties, context); - - // Add the tab (the first is closest to the content type, e.g. local, then direct composition) - var g = groupsByAlias.First(); - - tabs.Add(new Tab - { - Id = g.Id, - Key = g.Key, - Type = g.Type.ToString(), - Alias = g.Alias, - Label = LocalizedTextService.UmbracoDictionaryTranslate(CultureDictionary, g.Name), - Properties = mappedProperties - }); + properties.AddRange(groupProperties); } + + if (properties.Count == 0 && (!parentAliases?.Contains(groupsByAlias.Key) ?? false)) + { + continue; + } + + // Map the properties + List mappedProperties = MapProperties(source, properties, context); + + // Add the tab (the first is closest to the content type, e.g. local, then direct composition) + PropertyGroup g = groupsByAlias.First(); + + tabs.Add(new Tab + { + Id = g.Id, + Key = g.Key, + Type = g.Type.ToString(), + Alias = g.Alias, + Label = LocalizedTextService.UmbracoDictionaryTranslate(CultureDictionary, g.Name), + Properties = mappedProperties, + }); } - - MapGenericProperties(source, tabs, context); - - // Activate the first tab, if any - if (tabs.Count > 0) - tabs[0].IsActive = true; - - return tabs; } + + MapGenericProperties(source, tabs, context); + + // Activate the first tab, if any + if (tabs.Count > 0) + { + tabs[0].IsActive = true; + } + + return tabs; } } diff --git a/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs index 7bd436fa54..f9c1690c6a 100644 --- a/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/TagMapDefinition.cs @@ -1,21 +1,18 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class TagMapDefinition : IMapDefinition { - public class TagMapDefinition : IMapDefinition - { - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new TagModel(), Map); - } + public void DefineMaps(IUmbracoMapper mapper) => + mapper.Define((source, context) => new TagModel(), Map); - // Umbraco.Code.MapAll - private static void Map(ITag source, TagModel target, MapperContext context) - { - target.Id = source.Id; - target.Text = source.Text; - target.Group = source.Group; - target.NodeCount = source.NodeCount; - } + // Umbraco.Code.MapAll + private static void Map(ITag source, TagModel target, MapperContext context) + { + target.Id = source.Id; + target.Text = source.Text; + target.Group = source.Group; + target.NodeCount = source.NodeCount; } } diff --git a/src/Umbraco.Core/Models/Mapping/TemplateMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/TemplateMapDefinition.cs index 624868f3f4..5afc8bc8d7 100644 --- a/src/Umbraco.Core/Models/Mapping/TemplateMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/TemplateMapDefinition.cs @@ -1,50 +1,47 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class TemplateMapDefinition : IMapDefinition { - public class TemplateMapDefinition : IMapDefinition + private readonly IShortStringHelper _shortStringHelper; + + public TemplateMapDefinition(IShortStringHelper shortStringHelper) => _shortStringHelper = shortStringHelper; + + public void DefineMaps(IUmbracoMapper mapper) { - private readonly IShortStringHelper _shortStringHelper; + mapper.Define((source, context) => new TemplateDisplay(), Map); + mapper.Define( + (source, context) => new Template(_shortStringHelper, source.Name, source.Alias), Map); + } - public TemplateMapDefinition(IShortStringHelper shortStringHelper) - { - _shortStringHelper = shortStringHelper; - } + // Umbraco.Code.MapAll + private static void Map(ITemplate source, TemplateDisplay target, MapperContext context) + { + target.Id = source.Id; + target.Name = source.Name; + target.Alias = source.Alias; + target.Key = source.Key; + target.Content = source.Content; + target.Path = source.Path; + target.VirtualPath = source.VirtualPath; + target.MasterTemplateAlias = source.MasterTemplateAlias; + target.IsMasterTemplate = source.IsMasterTemplate; + } - public void DefineMaps(IUmbracoMapper mapper) - { - mapper.Define((source, context) => new TemplateDisplay(), Map); - mapper.Define((source, context) => new Template(_shortStringHelper, source.Name, source.Alias), Map); - } - - // Umbraco.Code.MapAll - private static void Map(ITemplate source, TemplateDisplay target, MapperContext context) - { - target.Id = source.Id; - target.Name = source.Name; - target.Alias = source.Alias; - target.Key = source.Key; - target.Content = source.Content; - target.Path = source.Path; - target.VirtualPath = source.VirtualPath; - target.MasterTemplateAlias = source.MasterTemplateAlias; - target.IsMasterTemplate = source.IsMasterTemplate; - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - // Umbraco.Code.MapAll -Path -VirtualPath -MasterTemplateId -IsMasterTemplate - // Umbraco.Code.MapAll -GetFileContent - private static void Map(TemplateDisplay source, ITemplate target, MapperContext context) - { - // don't need to worry about mapping MasterTemplateAlias here; - // the template controller handles any changes made to the master template - target.Name = source.Name; - target.Alias = source.Alias; - target.Content = source.Content; - target.Id = source.Id; - target.Key = source.Key; - } + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -Path -VirtualPath -MasterTemplateId -IsMasterTemplate + // Umbraco.Code.MapAll -GetFileContent + private static void Map(TemplateDisplay source, ITemplate target, MapperContext context) + { + // don't need to worry about mapping MasterTemplateAlias here; + // the template controller handles any changes made to the master template + target.Name = source.Name; + target.Alias = source.Alias; + target.Content = source.Content; + target.Id = source.Id; + target.Key = source.Key; } } diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs index a2c3fa7f28..47ed9ec4ab 100644 --- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Cache; @@ -16,449 +13,502 @@ using Umbraco.Cms.Core.Sections; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; +using UserProfile = Umbraco.Cms.Core.Models.ContentEditing.UserProfile; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class UserMapDefinition : IMapDefinition { - public class UserMapDefinition : IMapDefinition + private readonly ActionCollection _actions; + private readonly AppCaches _appCaches; + private readonly IEntityService _entityService; + private readonly GlobalSettings _globalSettings; + private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly MediaFileManager _mediaFileManager; + private readonly ISectionService _sectionService; + private readonly IShortStringHelper _shortStringHelper; + private readonly ILocalizedTextService _textService; + private readonly IUserService _userService; + + public UserMapDefinition( + ILocalizedTextService textService, + IUserService userService, + IEntityService entityService, + ISectionService sectionService, + AppCaches appCaches, + ActionCollection actions, + IOptions globalSettings, + MediaFileManager mediaFileManager, + IShortStringHelper shortStringHelper, + IImageUrlGenerator imageUrlGenerator) { - private readonly ISectionService _sectionService; - private readonly IEntityService _entityService; - private readonly IUserService _userService; - private readonly ILocalizedTextService _textService; - private readonly ActionCollection _actions; - private readonly AppCaches _appCaches; - private readonly GlobalSettings _globalSettings; - private readonly MediaFileManager _mediaFileManager; - private readonly IShortStringHelper _shortStringHelper; - private readonly IImageUrlGenerator _imageUrlGenerator; + _sectionService = sectionService; + _entityService = entityService; + _userService = userService; + _textService = textService; + _actions = actions; + _appCaches = appCaches; + _globalSettings = globalSettings.Value; + _mediaFileManager = mediaFileManager; + _shortStringHelper = shortStringHelper; + _imageUrlGenerator = imageUrlGenerator; + } - public UserMapDefinition(ILocalizedTextService textService, IUserService userService, IEntityService entityService, ISectionService sectionService, - AppCaches appCaches, ActionCollection actions, IOptions globalSettings, MediaFileManager mediaFileManager, IShortStringHelper shortStringHelper, - IImageUrlGenerator imageUrlGenerator) + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define( + (source, context) => new UserGroup(_shortStringHelper) { CreateDate = DateTime.UtcNow }, Map); + mapper.Define(Map); + mapper.Define((source, context) => new UserProfile(), Map); + mapper.Define((source, context) => new UserGroupBasic(), Map); + mapper.Define((source, context) => new UserGroupBasic(), Map); + mapper.Define( + (source, context) => new AssignedUserGroupPermissions(), + Map); + mapper.Define( + (source, context) => new AssignedContentPermissions(), + Map); + mapper.Define((source, context) => new UserGroupDisplay(), Map); + mapper.Define((source, context) => new UserBasic(), Map); + mapper.Define((source, context) => new UserDetail(), Map); + + // used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! + mapper.Define(Map); + + // important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. + mapper.Define((source, context) => new UserDisplay(), Map); + } + + // mappers + private static void Map(UserGroupSave source, IUserGroup target, MapperContext context) + { + if (!(target is UserGroup ttarget)) { - _sectionService = sectionService; - _entityService = entityService; - _userService = userService; - _textService = textService; - _actions = actions; - _appCaches = appCaches; - _globalSettings = globalSettings.Value; - _mediaFileManager = mediaFileManager; - _shortStringHelper = shortStringHelper; - _imageUrlGenerator = imageUrlGenerator; + throw new NotSupportedException($"{nameof(target)} must be a UserGroup."); } - public void DefineMaps(IUmbracoMapper mapper) + Map(source, ttarget); + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + private static void Map(UserGroupSave source, UserGroup target) + { + target.StartMediaId = source.StartMediaId; + target.StartContentId = source.StartContentId; + target.Icon = source.Icon; + target.Alias = source.Alias; + target.Name = source.Name; + target.Permissions = source.DefaultPermissions; + target.Key = source.Key; + + var id = GetIntId(source.Id); + if (id > 0) { - mapper.Define((source, context) => new UserGroup(_shortStringHelper) { CreateDate = DateTime.UtcNow }, Map); - mapper.Define(Map); - mapper.Define((source, context) => new ContentEditing.UserProfile(), Map); - mapper.Define((source, context) => new UserGroupBasic(), Map); - mapper.Define((source, context) => new UserGroupBasic(), Map); - mapper.Define((source, context) => new AssignedUserGroupPermissions(), Map); - mapper.Define((source, context) => new AssignedContentPermissions(), Map); - mapper.Define((source, context) => new UserGroupDisplay(), Map); - mapper.Define((source, context) => new UserBasic(), Map); - mapper.Define((source, context) => new UserDetail(), Map); - - // used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! - mapper.Define(Map); - - // important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - mapper.Define((source, context) => new UserDisplay(), Map); + target.Id = id; } - // mappers - - private static void Map(UserGroupSave source, IUserGroup target, MapperContext context) + target.ClearAllowedSections(); + if (source.Sections is not null) { - if (!(target is UserGroup ttarget)) - throw new NotSupportedException($"{nameof(target)} must be a UserGroup."); - Map(source, ttarget); - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - private static void Map(UserGroupSave source, UserGroup target) - { - target.StartMediaId = source.StartMediaId; - target.StartContentId = source.StartContentId; - target.Icon = source.Icon; - target.Alias = source.Alias; - target.Name = source.Name; - target.Permissions = source.DefaultPermissions; - target.Key = source.Key; - - var id = GetIntId(source.Id); - if (id > 0) - target.Id = id; - - target.ClearAllowedSections(); - if (source.Sections is not null) + foreach (var section in source.Sections) { - foreach (var section in source.Sections) - { - target.AddAllowedSection(section); - } + target.AddAllowedSection(section); } - - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - // Umbraco.Code.MapAll -Id -TourData -StartContentIds -StartMediaIds -Language -Username - // Umbraco.Code.MapAll -PasswordQuestion -SessionTimeout -EmailConfirmedDate -InvitedDate - // Umbraco.Code.MapAll -SecurityStamp -Avatar -ProviderUserKey -RawPasswordValue - // Umbraco.Code.MapAll -RawPasswordAnswerValue -Comments -IsApproved -IsLockedOut -LastLoginDate - // Umbraco.Code.MapAll -LastPasswordChangeDate -LastLockoutDate -FailedPasswordAttempts - // Umbraco.Code.MapAll -PasswordConfiguration - private void Map(UserInvite source, IUser target, MapperContext context) - { - target.Email = source.Email; - target.Key = source.Key; - target.Name = source.Name; - target.IsApproved = false; - - target.ClearGroups(); - var groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); - foreach (var group in groups) - target.AddGroup(group.ToReadOnlyGroup()); - } - - // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate - // Umbraco.Code.MapAll -TourData -SessionTimeout -EmailConfirmedDate -InvitedDate -SecurityStamp -Avatar - // Umbraco.Code.MapAll -ProviderUserKey -RawPasswordValue -RawPasswordAnswerValue -PasswordQuestion -Comments - // Umbraco.Code.MapAll -IsApproved -IsLockedOut -LastLoginDate -LastPasswordChangeDate -LastLockoutDate - // Umbraco.Code.MapAll -FailedPasswordAttempts - // Umbraco.Code.MapAll -PasswordConfiguration - private void Map(UserSave source, IUser target, MapperContext context) - { - target.Name = source.Name; - target.StartContentIds = source.StartContentIds ?? Array.Empty(); - target.StartMediaIds = source.StartMediaIds ?? Array.Empty(); - target.Language = source.Culture; - target.Email = source.Email; - target.Key = source.Key; - target.Username = source.Username; - target.Id = source.Id; - - target.ClearGroups(); - var groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); - foreach (var group in groups) - target.AddGroup(group.ToReadOnlyGroup()); - } - - // Umbraco.Code.MapAll - private static void Map(IProfile source, ContentEditing.UserProfile target, MapperContext context) - { - target.Name = source.Name; - target.UserId = source.Id; - } - - // Umbraco.Code.MapAll -ContentStartNode -UserCount -MediaStartNode -Key -Sections - // Umbraco.Code.MapAll -Notifications -Udi -Trashed -AdditionalData -IsSystemUserGroup - private void Map(IReadOnlyUserGroup source, UserGroupBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Id = source.Id; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.IsSystemUserGroup = source.IsSystemUserGroup(); - - MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); - } - - // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications - // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -IsSystemUserGroup - private void Map(IUserGroup source, UserGroupBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.UserCount = source.UserCount; - target.IsSystemUserGroup = source.IsSystemUserGroup(); - - MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); - } - - // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -AssignedPermissions - private void Map(IUserGroup source, AssignedUserGroupPermissions target, MapperContext context) - { - target.Id = source.Id; - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - - target.DefaultPermissions = MapUserGroupDefaultPermissions(source); - - if (target.Icon.IsNullOrWhiteSpace()) - target.Icon = Constants.Icons.UserGroup; - } - - // Umbraco.Code.MapAll -Trashed -Alias -AssignedPermissions - private static void Map(EntitySlim source, AssignedContentPermissions target, MapperContext context) - { - target.Icon = MapContentTypeIcon(source); - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); - - if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) - target.Icon = Constants.Icons.Member; - } - - // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications -Udi - // Umbraco.Code.MapAll -Trashed -AdditionalData -Users -AssignedPermissions - private void Map(IUserGroup source, UserGroupDisplay target, MapperContext context) - { - target.Alias = source.Alias; - target.DefaultPermissions = MapUserGroupDefaultPermissions(source); - target.Icon = source.Icon; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.UserCount = source.UserCount; - target.IsSystemUserGroup = source.IsSystemUserGroup(); - - MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); - - //Important! Currently we are never mapping to multiple UserGroupDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - var users = _userService.GetAllInGroup(source.Id); - target.Users = context.MapEnumerable(users).WhereNotNull(); - - //Deal with assigned permissions: - - var allContentPermissions = _userService.GetPermissions(source, true) - .ToDictionary(x => x.EntityId, x => x); - - IEntitySlim[] contentEntities; - if (allContentPermissions.Keys.Count == 0) - { - contentEntities = Array.Empty(); - } - else - { - // a group can end up with way more than 2000 assigned permissions, - // so we need to break them into groups in order to avoid breaking - // the entity service due to too many Sql parameters. - - var list = new List(); - foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(Constants.Sql.MaxParameterCount)) - list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); - contentEntities = list.ToArray(); - } - - var allAssignedPermissions = new List(); - foreach (var entity in contentEntities) - { - var contentPermissions = allContentPermissions[entity.Id]; - - var assignedContentPermissions = context.Map(entity); - if (assignedContentPermissions is null) - { - continue; - } - assignedContentPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(target.DefaultPermissions); - - //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions - //and we'll re-check it if it's one of the explicitly assigned ones - foreach (var permission in assignedContentPermissions.AssignedPermissions.SelectMany(x => x.Value)) - { - permission.Checked = false; - permission.Checked = contentPermissions.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); - } - - allAssignedPermissions.Add(assignedContentPermissions); - } - - target.AssignedPermissions = allAssignedPermissions; - } - - // Umbraco.Code.MapAll -Notifications -Udi -Icon -IsCurrentUser -Trashed -ResetPasswordValue - // Umbraco.Code.MapAll -Alias -AdditionalData - private void Map(IUser source, UserDisplay target, MapperContext context) - { - target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); - target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); - target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService,_appCaches), UmbracoObjectTypes.Document, "content","contentRoot", context); - target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media","mediaRoot", context); - target.CreateDate = source.CreateDate; - target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); - target.Email = source.Email; - target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); - target.FailedPasswordAttempts = source.FailedPasswordAttempts; - target.Id = source.Id; - target.Key = source.Key; - target.LastLockoutDate = source.LastLockoutDate; - target.LastLoginDate = source.LastLoginDate == default(DateTime) ? null : (DateTime?)source.LastLoginDate; - target.LastPasswordChangeDate = source.LastPasswordChangeDate; - target.Name = source.Name; - target.Navigation = CreateUserEditorNavigation(); - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.StartContentIds = GetStartNodes(source.StartContentIds?.ToArray(), UmbracoObjectTypes.Document, "content","contentRoot", context); - target.StartMediaIds = GetStartNodes(source.StartMediaIds?.ToArray(), UmbracoObjectTypes.Media, "media","mediaRoot", context); - target.UpdateDate = source.UpdateDate; - target.UserGroups = context.MapEnumerable(source.Groups).WhereNotNull(); - target.Username = source.Username; - target.UserState = source.UserState; - } - - // Umbraco.Code.MapAll -Notifications -IsCurrentUser -Udi -Icon -Trashed -Alias -AdditionalData - private void Map(IUser source, UserBasic target, MapperContext context) - { - //Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost - //Alternatively, if this is annoying the back office UI would need to be updated to request the avatars for the list of users separately so it doesn't look - //like the load time is waiting. - target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); - target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); - target.Email = source.Email; - target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); - target.Id = source.Id; - target.Key = source.Key; - target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?)source.LastLoginDate; - target.Name = source.Name; - target.ParentId = -1; - target.Path = "-1," + source.Id; - target.UserGroups = context.MapEnumerable(source.Groups).WhereNotNull(); - target.Username = source.Username; - target.UserState = source.UserState; - } - - // Umbraco.Code.MapAll -SecondsUntilTimeout - private void Map(IUser source, UserDetail target, MapperContext context) - { - target.AllowedSections = source.AllowedSections; - target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); - target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); - target.Email = source.Email; - target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); - target.Name = source.Name; - target.StartContentIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); - target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); - target.UserId = source.Id; - - //we need to map the legacy UserType - //the best we can do here is to return the user's first user group as a IUserType object - //but we should attempt to return any group that is the built in ones first - target.UserGroups = source.Groups.Select(x => x.Alias).ToArray(); - } - - // helpers - - private void MapUserGroupBasic(UserGroupBasic target, IEnumerable sourceAllowedSections, int? sourceStartContentId, int? sourceStartMediaId, MapperContext context) - { - var allSections = _sectionService.GetSections(); - target.Sections = context.MapEnumerable(allSections.Where(x => sourceAllowedSections.Contains(x.Alias))).WhereNotNull(); - - if (sourceStartMediaId > 0) - target.MediaStartNode = context.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media)); - else if (sourceStartMediaId == -1) - target.MediaStartNode = CreateRootNode(_textService.Localize("media", "mediaRoot")); - - if (sourceStartContentId > 0) - target.ContentStartNode = context.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document)); - else if (sourceStartContentId == -1) - target.ContentStartNode = CreateRootNode(_textService.Localize("content", "contentRoot")); - - if (target.Icon.IsNullOrWhiteSpace()) - target.Icon = Constants.Icons.UserGroup; - } - - private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) - { - Permission GetPermission(IAction action) - => new Permission - { - Category = action.Category.IsNullOrWhiteSpace() - ? _textService.Localize("actionCategories",Constants.Conventions.PermissionCategories.OtherCategory) - : _textService.Localize("actionCategories", action.Category), - Name = _textService.Localize("actions", action.Alias), - Description = _textService.Localize("actionDescriptions", action.Alias), - Icon = action.Icon, - Checked = source.Permissions != null && source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)), - PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture) - }; - - return _actions - .Where(x => x.CanBePermissionAssigned) - .Select(GetPermission) - .GroupBy(x => x.Category) - .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); - } - - private static string? MapContentTypeIcon(IEntitySlim entity) - => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; - - private IEnumerable GetStartNodes(int[]? startNodeIds, UmbracoObjectTypes objectType, string localizedArea,string localizedAlias, MapperContext context) - { - if (startNodeIds is null || startNodeIds.Length <= 0) - return Enumerable.Empty(); - - var startNodes = new List(); - if (startNodeIds.Contains(-1)) - startNodes.Add(CreateRootNode(_textService.Localize(localizedArea, localizedAlias))); - - var mediaItems = _entityService.GetAll(objectType, startNodeIds); - startNodes.AddRange(context.MapEnumerable(mediaItems).WhereNotNull()); - return startNodes; - } - - private IEnumerable CreateUserEditorNavigation() - { - return new[] - { - new EditorNavigation - { - Active = true, - Alias = "details", - Icon = "icon-umb-users", - Name = _textService.Localize("general","user"), - View = "views/users/views/user/details.html" - } - }; - } - - private static int GetIntId(object? id) - { - if (id is string strId && int.TryParse(strId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt)) - { - return asInt; - } - var result = id.TryConvertTo(); - if (result.Success == false) - { - throw new InvalidOperationException( - "Cannot convert the profile to a " + typeof(UserDetail).Name + " object since the id is not an integer"); - } - return result.Result; - } - - private EntityBasic CreateRootNode(string name) - { - return new EntityBasic - { - Name = name, - Path = "-1", - Icon = "icon-folder", - Id = -1, - Trashed = false, - ParentId = -1 - }; } } + + // Umbraco.Code.MapAll + private static void Map(IProfile source, UserProfile target, MapperContext context) + { + target.Name = source.Name; + target.UserId = source.Id; + } + + // Umbraco.Code.MapAll -Trashed -Alias -AssignedPermissions + private static void Map(EntitySlim source, AssignedContentPermissions target, MapperContext context) + { + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) + { + target.Icon = Constants.Icons.Member; + } + } + + private static string? MapContentTypeIcon(IEntitySlim entity) + => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + + private static int GetIntId(object? id) + { + if (id is string strId && + int.TryParse(strId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt)) + { + return asInt; + } + + Attempt result = id.TryConvertTo(); + if (result.Success == false) + { + throw new InvalidOperationException( + "Cannot convert the profile to a " + typeof(UserDetail).Name + + " object since the id is not an integer"); + } + + return result.Result; + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -Id -TourData -StartContentIds -StartMediaIds -Language -Username + // Umbraco.Code.MapAll -PasswordQuestion -SessionTimeout -EmailConfirmedDate -InvitedDate + // Umbraco.Code.MapAll -SecurityStamp -Avatar -ProviderUserKey -RawPasswordValue + // Umbraco.Code.MapAll -RawPasswordAnswerValue -Comments -IsApproved -IsLockedOut -LastLoginDate + // Umbraco.Code.MapAll -LastPasswordChangeDate -LastLockoutDate -FailedPasswordAttempts + // Umbraco.Code.MapAll -PasswordConfiguration + private void Map(UserInvite source, IUser target, MapperContext context) + { + target.Email = source.Email; + target.Key = source.Key; + target.Name = source.Name; + target.IsApproved = false; + + target.ClearGroups(); + IEnumerable groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); + foreach (IUserGroup group in groups) + { + target.AddGroup(group.ToReadOnlyGroup()); + } + } + + // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate + // Umbraco.Code.MapAll -TourData -SessionTimeout -EmailConfirmedDate -InvitedDate -SecurityStamp -Avatar + // Umbraco.Code.MapAll -ProviderUserKey -RawPasswordValue -RawPasswordAnswerValue -PasswordQuestion -Comments + // Umbraco.Code.MapAll -IsApproved -IsLockedOut -LastLoginDate -LastPasswordChangeDate -LastLockoutDate + // Umbraco.Code.MapAll -FailedPasswordAttempts + // Umbraco.Code.MapAll -PasswordConfiguration + private void Map(UserSave source, IUser target, MapperContext context) + { + target.Name = source.Name; + target.StartContentIds = source.StartContentIds ?? Array.Empty(); + target.StartMediaIds = source.StartMediaIds ?? Array.Empty(); + target.Language = source.Culture; + target.Email = source.Email; + target.Key = source.Key; + target.Username = source.Username; + target.Id = source.Id; + + target.ClearGroups(); + IEnumerable groups = _userService.GetUserGroupsByAlias(source.UserGroups.ToArray()); + foreach (IUserGroup group in groups) + { + target.AddGroup(group.ToReadOnlyGroup()); + } + } + + // Umbraco.Code.MapAll -ContentStartNode -UserCount -MediaStartNode -Key -Sections + // Umbraco.Code.MapAll -Notifications -Udi -Trashed -AdditionalData -IsSystemUserGroup + private void Map(IReadOnlyUserGroup source, UserGroupBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.IsSystemUserGroup = source.IsSystemUserGroup(); + + MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); + } + + // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications + // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -IsSystemUserGroup + private void Map(IUserGroup source, UserGroupBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserCount = source.UserCount; + target.IsSystemUserGroup = source.IsSystemUserGroup(); + + MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); + } + + // Umbraco.Code.MapAll -Udi -Trashed -AdditionalData -AssignedPermissions + private void Map(IUserGroup source, AssignedUserGroupPermissions target, MapperContext context) + { + target.Id = source.Id; + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + + target.DefaultPermissions = MapUserGroupDefaultPermissions(source); + + if (target.Icon.IsNullOrWhiteSpace()) + { + target.Icon = Constants.Icons.UserGroup; + } + } + + // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications -Udi + // Umbraco.Code.MapAll -Trashed -AdditionalData -Users -AssignedPermissions + private void Map(IUserGroup source, UserGroupDisplay target, MapperContext context) + { + target.Alias = source.Alias; + target.DefaultPermissions = MapUserGroupDefaultPermissions(source); + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserCount = source.UserCount; + target.IsSystemUserGroup = source.IsSystemUserGroup(); + + MapUserGroupBasic(target, source.AllowedSections, source.StartContentId, source.StartMediaId, context); + + // Important! Currently we are never mapping to multiple UserGroupDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. + IEnumerable users = _userService.GetAllInGroup(source.Id); + target.Users = context.MapEnumerable(users).WhereNotNull(); + + // Deal with assigned permissions: + var allContentPermissions = _userService.GetPermissions(source, true) + .ToDictionary(x => x.EntityId, x => x); + + IEntitySlim[] contentEntities; + if (allContentPermissions.Keys.Count == 0) + { + contentEntities = Array.Empty(); + } + else + { + // a group can end up with way more than 2000 assigned permissions, + // so we need to break them into groups in order to avoid breaking + // the entity service due to too many Sql parameters. + var list = new List(); + foreach (IEnumerable idGroup in allContentPermissions.Keys.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); + } + + contentEntities = list.ToArray(); + } + + var allAssignedPermissions = new List(); + foreach (IEntitySlim entity in contentEntities) + { + EntityPermission contentPermissions = allContentPermissions[entity.Id]; + + AssignedContentPermissions? assignedContentPermissions = context.Map(entity); + if (assignedContentPermissions is null) + { + continue; + } + + assignedContentPermissions.AssignedPermissions = + AssignedUserGroupPermissions.ClonePermissions(target.DefaultPermissions); + + // since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions + // and we'll re-check it if it's one of the explicitly assigned ones + foreach (Permission permission in assignedContentPermissions.AssignedPermissions.SelectMany(x => x.Value)) + { + permission.Checked = false; + permission.Checked = + contentPermissions.AssignedPermissions.Contains( + permission.PermissionCode, + StringComparer.InvariantCulture); + } + + allAssignedPermissions.Add(assignedContentPermissions); + } + + target.AssignedPermissions = allAssignedPermissions; + } + + // Umbraco.Code.MapAll -Notifications -Udi -Icon -IsCurrentUser -Trashed -ResetPasswordValue + // Umbraco.Code.MapAll -Alias -AdditionalData + private void Map(IUser source, UserDisplay target, MapperContext context) + { + target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); + target.CalculatedStartContentIds = + GetStartNodes(source.CalculateContentStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Document, "content", "contentRoot", context); + target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media", "mediaRoot", context); + target.CreateDate = source.CreateDate; + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); + target.FailedPasswordAttempts = source.FailedPasswordAttempts; + target.Id = source.Id; + target.Key = source.Key; + target.LastLockoutDate = source.LastLockoutDate; + target.LastLoginDate = source.LastLoginDate == default(DateTime) ? null : source.LastLoginDate; + target.LastPasswordChangeDate = source.LastPasswordChangeDate; + target.Name = source.Name; + target.Navigation = CreateUserEditorNavigation(); + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.StartContentIds = GetStartNodes(source.StartContentIds?.ToArray(), UmbracoObjectTypes.Document, "content", "contentRoot", context); + target.StartMediaIds = GetStartNodes(source.StartMediaIds?.ToArray(), UmbracoObjectTypes.Media, "media", "mediaRoot", context); + target.UpdateDate = source.UpdateDate; + target.UserGroups = context.MapEnumerable(source.Groups).WhereNotNull(); + target.Username = source.Username; + target.UserState = source.UserState; + } + + // Umbraco.Code.MapAll -Notifications -IsCurrentUser -Udi -Icon -Trashed -Alias -AdditionalData + private void Map(IUser source, UserBasic target, MapperContext context) + { + // Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost + // Alternatively, if this is annoying the back office UI would need to be updated to request the avatars for the list of users separately so it doesn't look + // like the load time is waiting. + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); + target.Id = source.Id; + target.Key = source.Key; + target.LastLoginDate = source.LastLoginDate == default ? null : source.LastLoginDate; + target.Name = source.Name; + target.ParentId = -1; + target.Path = "-1," + source.Id; + target.UserGroups = context.MapEnumerable(source.Groups).WhereNotNull(); + target.Username = source.Username; + target.UserState = source.UserState; + } + + // Umbraco.Code.MapAll -SecondsUntilTimeout + private void Map(IUser source, UserDetail target, MapperContext context) + { + target.AllowedSections = source.AllowedSections; + target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); + target.Email = source.Email; + target.EmailHash = source.Email?.ToLowerInvariant().Trim().GenerateHash(); + target.Name = source.Name; + target.StartContentIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); + target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); + target.UserId = source.Id; + + // we need to map the legacy UserType + // the best we can do here is to return the user's first user group as a IUserType object + // but we should attempt to return any group that is the built in ones first + target.UserGroups = source.Groups.Select(x => x.Alias).ToArray(); + } + + // helpers + private void MapUserGroupBasic(UserGroupBasic target, IEnumerable sourceAllowedSections, int? sourceStartContentId, int? sourceStartMediaId, MapperContext context) + { + IEnumerable allSections = _sectionService.GetSections(); + target.Sections = context + .MapEnumerable(allSections.Where(x => sourceAllowedSections.Contains(x.Alias))) + .WhereNotNull(); + + if (sourceStartMediaId > 0) + { + target.MediaStartNode = + context.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media)); + } + else if (sourceStartMediaId == -1) + { + target.MediaStartNode = CreateRootNode(_textService.Localize("media", "mediaRoot")); + } + + if (sourceStartContentId > 0) + { + target.ContentStartNode = + context.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document)); + } + else if (sourceStartContentId == -1) + { + target.ContentStartNode = CreateRootNode(_textService.Localize("content", "contentRoot")); + } + + if (target.Icon.IsNullOrWhiteSpace()) + { + target.Icon = Constants.Icons.UserGroup; + } + } + + private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) + { + Permission GetPermission(IAction action) + { + return new() + { + Category = action.Category.IsNullOrWhiteSpace() + ? _textService.Localize( + "actionCategories", + Constants.Conventions.PermissionCategories.OtherCategory) + : _textService.Localize("actionCategories", action.Category), + Name = _textService.Localize("actions", action.Alias), + Description = _textService.Localize("actionDescriptions", action.Alias), + Icon = action.Icon, + Checked = source.Permissions != null && + source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)), + PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture), + }; + } + + return _actions + .Where(x => x.CanBePermissionAssigned) + .Select(GetPermission) + .GroupBy(x => x.Category) + .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); + } + + private IEnumerable GetStartNodes(int[]? startNodeIds, UmbracoObjectTypes objectType, string localizedArea, string localizedAlias, MapperContext context) + { + if (startNodeIds is null || startNodeIds.Length <= 0) + { + return Enumerable.Empty(); + } + + var startNodes = new List(); + if (startNodeIds.Contains(-1)) + { + startNodes.Add(CreateRootNode(_textService.Localize(localizedArea, localizedAlias))); + } + + IEnumerable mediaItems = _entityService.GetAll(objectType, startNodeIds); + startNodes.AddRange(context.MapEnumerable(mediaItems).WhereNotNull()); + return startNodes; + } + + private IEnumerable CreateUserEditorNavigation() => + new[] + { + new EditorNavigation + { + Active = true, + Alias = "details", + Icon = "icon-umb-users", + Name = _textService.Localize("general", "user"), + View = "views/users/views/user/details.html", + }, + }; + + private EntityBasic CreateRootNode(string name) => + new EntityBasic + { + Name = name, + Path = "-1", + Icon = "icon-folder", + Id = -1, + Trashed = false, + ParentId = -1, + }; } diff --git a/src/Umbraco.Core/Models/Media.cs b/src/Umbraco.Core/Models/Media.cs index 926fe2ef09..d0cf05b8b9 100644 --- a/src/Umbraco.Core/Models/Media.cs +++ b/src/Umbraco.Core/Models/Media.cs @@ -1,84 +1,87 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Media object +/// +[Serializable] +[DataContract(IsReference = true)] +public class Media : ContentBase, IMedia { /// - /// Represents a Media object + /// Constructor for creating a Media object /// - [Serializable] - [DataContract(IsReference = true)] - public class Media : ContentBase, IMedia + /// name of the Media object + /// Parent object + /// MediaType for the current Media object + public Media(string? name, IMedia? parent, IMediaType mediaType) + : this(name, parent, mediaType, new PropertyCollection()) { - /// - /// Constructor for creating a Media object - /// - /// name of the Media object - /// Parent object - /// MediaType for the current Media object - public Media(string? name, IMedia? parent, IMediaType mediaType) - : this(name, parent, mediaType, new PropertyCollection()) - { } + } - /// - /// Constructor for creating a Media object - /// - /// name of the Media object - /// Parent object - /// MediaType for the current Media object - /// Collection of properties - public Media(string? name, IMedia? parent, IMediaType mediaType, IPropertyCollection properties) - : base(name, parent, mediaType, properties) - { } + /// + /// Constructor for creating a Media object + /// + /// name of the Media object + /// Parent object + /// MediaType for the current Media object + /// Collection of properties + public Media(string? name, IMedia? parent, IMediaType mediaType, IPropertyCollection properties) + : base(name, parent, mediaType, properties) + { + } - /// - /// Constructor for creating a Media object - /// - /// name of the Media object - /// Id of the Parent IMedia - /// MediaType for the current Media object - public Media(string? name, int parentId, IMediaType? mediaType) - : this(name, parentId, mediaType, new PropertyCollection()) - { } + /// + /// Constructor for creating a Media object + /// + /// name of the Media object + /// Id of the Parent IMedia + /// MediaType for the current Media object + public Media(string? name, int parentId, IMediaType? mediaType) + : this(name, parentId, mediaType, new PropertyCollection()) + { + } - /// - /// Constructor for creating a Media object - /// - /// Name of the Media object - /// Id of the Parent IMedia - /// MediaType for the current Media object - /// Collection of properties - public Media(string? name, int parentId, IMediaType? mediaType, IPropertyCollection properties) - : base(name, parentId, mediaType, properties) - { } + /// + /// Constructor for creating a Media object + /// + /// Name of the Media object + /// Id of the Parent IMedia + /// MediaType for the current Media object + /// Collection of properties + public Media(string? name, int parentId, IMediaType? mediaType, IPropertyCollection properties) + : base(name, parentId, mediaType, properties) + { + } - /// - /// Changes the for the current Media object - /// - /// New MediaType for this Media - /// Leaves PropertyTypes intact after change - internal void ChangeContentType(IMediaType mediaType) + /// + /// Changes the for the current Media object + /// + /// New MediaType for this Media + /// Leaves PropertyTypes intact after change + internal void ChangeContentType(IMediaType mediaType) => ChangeContentType(mediaType, false); + + /// + /// Changes the for the current Media object and removes PropertyTypes, + /// which are not part of the new MediaType. + /// + /// New MediaType for this Media + /// Boolean indicating whether to clear PropertyTypes upon change + internal void ChangeContentType(IMediaType mediaType, bool clearProperties) + { + ChangeContentType(new SimpleContentType(mediaType)); + + if (clearProperties) { - ChangeContentType(mediaType, false); + Properties.EnsureCleanPropertyTypes(mediaType.CompositionPropertyTypes); + } + else + { + Properties.EnsurePropertyTypes(mediaType.CompositionPropertyTypes); } - /// - /// Changes the for the current Media object and removes PropertyTypes, - /// which are not part of the new MediaType. - /// - /// New MediaType for this Media - /// Boolean indicating whether to clear PropertyTypes upon change - internal void ChangeContentType(IMediaType mediaType, bool clearProperties) - { - ChangeContentType(new SimpleContentType(mediaType)); - - if (clearProperties) - Properties.EnsureCleanPropertyTypes(mediaType.CompositionPropertyTypes); - else - Properties.EnsurePropertyTypes(mediaType.CompositionPropertyTypes); - - Properties.ClearCollectionChangedEvents(); // be sure not to double add - Properties.CollectionChanged += PropertiesChanged; - } + Properties.ClearCollectionChangedEvents(); // be sure not to double add + Properties.CollectionChanged += PropertiesChanged; } } diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs index 236ec9deb7..ee69c25de4 100644 --- a/src/Umbraco.Core/Models/MediaExtensions.cs +++ b/src/Umbraco.Core/Models/MediaExtensions.cs @@ -1,32 +1,30 @@ -using System.Linq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Extensions -{ - public static class MediaExtensions - { - /// - /// Gets the URL of a media item. - /// - public static string? GetUrl(this IMedia media, string propertyAlias, MediaUrlGeneratorCollection mediaUrlGenerators) - { - if (media.TryGetMediaPath(propertyAlias, mediaUrlGenerators, out var mediaPath)) - { - return mediaPath; - } +namespace Umbraco.Extensions; - return string.Empty; +public static class MediaExtensions +{ + /// + /// Gets the URL of a media item. + /// + public static string? GetUrl(this IMedia media, string propertyAlias, MediaUrlGeneratorCollection mediaUrlGenerators) + { + if (media.TryGetMediaPath(propertyAlias, mediaUrlGenerators, out var mediaPath)) + { + return mediaPath; } - /// - /// Gets the URLs of a media item. - /// - public static string?[] GetUrls(this IMedia media, ContentSettings contentSettings, MediaUrlGeneratorCollection mediaUrlGenerators) - => contentSettings.Imaging.AutoFillImageProperties - .Select(field => media.GetUrl(field.Alias, mediaUrlGenerators)) - .Where(link => string.IsNullOrWhiteSpace(link) == false) - .ToArray(); + return string.Empty; } + + /// + /// Gets the URLs of a media item. + /// + public static string?[] GetUrls(this IMedia media, ContentSettings contentSettings, MediaUrlGeneratorCollection mediaUrlGenerators) + => contentSettings.Imaging.AutoFillImageProperties + .Select(field => media.GetUrl(field.Alias, mediaUrlGenerators)) + .Where(link => string.IsNullOrWhiteSpace(link) == false) + .ToArray(); } diff --git a/src/Umbraco.Core/Models/MediaType.cs b/src/Umbraco.Core/Models/MediaType.cs index a529dc3189..64683ae462 100644 --- a/src/Umbraco.Core/Models/MediaType.cs +++ b/src/Umbraco.Core/Models/MediaType.cs @@ -1,54 +1,51 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the content type that a object is based on +/// +[Serializable] +[DataContract(IsReference = true)] +public class MediaType : ContentTypeCompositionBase, IMediaType { + public const bool SupportsPublishingConst = false; + /// - /// Represents the content type that a object is based on + /// Constuctor for creating a MediaType with the parent's id. /// - [Serializable] - [DataContract(IsReference = true)] - public class MediaType : ContentTypeCompositionBase, IMediaType + /// Only use this for creating MediaTypes at the root (with ParentId -1). + public MediaType(IShortStringHelper shortStringHelper, int parentId) + : base(shortStringHelper, parentId) { - public const bool SupportsPublishingConst = false; - - /// - /// Constuctor for creating a MediaType with the parent's id. - /// - /// Only use this for creating MediaTypes at the root (with ParentId -1). - /// - public MediaType(IShortStringHelper shortStringHelper, int parentId) : base(shortStringHelper, parentId) - { - } - - /// - /// Constuctor for creating a MediaType with the parent as an inherited type. - /// - /// Use this to ensure inheritance from parent. - /// - public MediaType(IShortStringHelper shortStringHelper,IMediaType parent) : this(shortStringHelper, parent, string.Empty) - { - } - - /// - /// Constuctor for creating a MediaType with the parent as an inherited type. - /// - /// Use this to ensure inheritance from parent. - /// - /// - public MediaType(IShortStringHelper shortStringHelper, IMediaType parent, string alias) - : base(shortStringHelper, parent, alias) - { - } - - /// - public override ISimpleContentType ToSimple() => new SimpleContentType(this); - - /// - public override bool SupportsPublishing => SupportsPublishingConst; - - /// - IMediaType IMediaType.DeepCloneWithResetIdentities(string newAlias) => (IMediaType)DeepCloneWithResetIdentities(newAlias); } + + /// + /// Constuctor for creating a MediaType with the parent as an inherited type. + /// + /// Use this to ensure inheritance from parent. + public MediaType(IShortStringHelper shortStringHelper, IMediaType parent) + : this(shortStringHelper, parent, string.Empty) + { + } + + /// + /// Constuctor for creating a MediaType with the parent as an inherited type. + /// + /// Use this to ensure inheritance from parent. + public MediaType(IShortStringHelper shortStringHelper, IMediaType parent, string alias) + : base(shortStringHelper, parent, alias) + { + } + + /// + public override bool SupportsPublishing => SupportsPublishingConst; + + /// + public override ISimpleContentType ToSimple() => new SimpleContentType(this); + + /// + IMediaType IMediaType.DeepCloneWithResetIdentities(string newAlias) => + (IMediaType)DeepCloneWithResetIdentities(newAlias); } diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 4244e1ba44..cddf04b4fe 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -1,500 +1,576 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; using Microsoft.Extensions.Logging; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Member object +/// +[Serializable] +[DataContract(IsReference = true)] +public class Member : ContentBase, IMember { + private IDictionary? _additionalData; + private string _email; + private DateTime? _emailConfirmedDate; + private int _failedPasswordAttempts; + private bool _isApproved; + private bool _isLockedOut; + private DateTime? _lastLockoutDate; + private DateTime? _lastLoginDate; + private DateTime? _lastPasswordChangeDate; + private string? _passwordConfig; + private string? _rawPasswordValue; + private string? _securityStamp; + private string _username; + /// - /// Represents a Member object + /// Initializes a new instance of the class. + /// Constructor for creating an empty Member object /// - [Serializable] - [DataContract(IsReference = true)] - public class Member : ContentBase, IMember + /// ContentType for the current Content object + public Member(IMemberType contentType) + : base(string.Empty, -1, contentType, new PropertyCollection()) { - private IDictionary? _additionalData; - private string _username; - private string _email; - private string? _rawPasswordValue; - private string? _passwordConfig; - private DateTime? _emailConfirmedDate; - private string? _securityStamp; - private int _failedPasswordAttempts; - private bool _isApproved; - private bool _isLockedOut; - private DateTime? _lastLockoutDate; - private DateTime? _lastLoginDate; - private DateTime? _lastPasswordChangeDate; + IsApproved = true; - /// - /// Initializes a new instance of the class. - /// Constructor for creating an empty Member object - /// - /// ContentType for the current Content object - public Member(IMemberType contentType) - : base("", -1, contentType, new PropertyCollection()) + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + _email = string.Empty; + _username = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// Constructor for creating a Member object + /// + /// Name of the content + /// ContentType for the current Content object + public Member(string name, IMemberType contentType) + : base(name, -1, contentType, new PropertyCollection()) + { + if (name == null) { - IsApproved = true; - - // this cannot be null but can be empty - _rawPasswordValue = ""; - _email = ""; - _username = ""; + throw new ArgumentNullException(nameof(name)); } - /// - /// Initializes a new instance of the class. - /// Constructor for creating a Member object - /// - /// Name of the content - /// ContentType for the current Content object - public Member(string name, IMemberType contentType) - : base(name, -1, contentType, new PropertyCollection()) + if (string.IsNullOrWhiteSpace(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)); - - IsApproved = true; - - // this cannot be null but can be empty - _rawPasswordValue = ""; - _email = ""; - _username = ""; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Initializes a new instance of the class. - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true) - : base(name, -1, contentType, new PropertyCollection()) + IsApproved = true; + + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + _email = string.Empty; + _username = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// + public Member(string name, string email, string username, IMemberType contentType, bool isApproved = true) + : base(name, -1, contentType, new PropertyCollection()) + { + if (name == null) { - 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 (email == null) - throw new ArgumentNullException(nameof(email)); - if (string.IsNullOrWhiteSpace(email)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email)); - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (string.IsNullOrWhiteSpace(username)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); - - _email = email; - _username = username; - IsApproved = isApproved; - - // this cannot be null but can be empty - _rawPasswordValue = ""; + throw new ArgumentNullException(nameof(name)); } - /// - /// Initializes a new instance of the class. - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - /// - /// - public Member(string name, string email, string username, IMemberType contentType, int userId, bool isApproved = true) - : base(name, -1, contentType, new PropertyCollection()) + if (string.IsNullOrWhiteSpace(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)); - if (email == null) throw new ArgumentNullException(nameof(email)); - if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(email)); - if (username == null) throw new ArgumentNullException(nameof(username)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); - - _email = email; - _username = username; - CreatorId = userId; - IsApproved = isApproved; - - //this cannot be null but can be empty - _rawPasswordValue = ""; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password - /// - /// - public Member(string? name, string email, string username, string? rawPasswordValue, IMemberType? contentType) - : base(name, -1, contentType, new PropertyCollection()) + if (email == null) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = true; + throw new ArgumentNullException(nameof(email)); } - /// - /// Initializes a new instance of the class. - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password - /// - /// - /// - public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved) - : base(name, -1, contentType, new PropertyCollection()) + if (string.IsNullOrWhiteSpace(email)) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = isApproved; + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(email)); } - /// - /// Constructor for creating a Member object - /// - /// - /// - /// - /// - /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password - /// - /// - /// - /// - public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved, int userId) - : base(name, -1, contentType, new PropertyCollection()) + if (username == null) { - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - IsApproved = isApproved; - CreatorId = userId; + throw new ArgumentNullException(nameof(username)); } - /// - /// Gets or sets the Username - /// - [DataMember] - public string Username + if (string.IsNullOrWhiteSpace(username)) { - get => _username; - set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(username)); } - /// - /// Gets or sets the Email - /// - [DataMember] - public string Email + _email = email; + _username = username; + IsApproved = isApproved; + + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// + /// + public Member(string name, string email, string username, IMemberType contentType, int userId, bool isApproved = true) + : base(name, -1, contentType, new PropertyCollection()) + { + if (name == null) { - get => _email; - set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + throw new ArgumentNullException(nameof(name)); } - [DataMember] - public DateTime? EmailConfirmedDate + if (string.IsNullOrWhiteSpace(name)) { - get => _emailConfirmedDate; - set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Gets or sets the raw password value - /// - [IgnoreDataMember] - public string? RawPasswordValue + if (email == null) { - get => _rawPasswordValue; - set + throw new ArgumentNullException(nameof(email)); + } + + if (string.IsNullOrWhiteSpace(email)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(email)); + } + + if (username == null) + { + throw new ArgumentNullException(nameof(username)); + } + + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(username)); + } + + _email = email; + _username = username; + CreatorId = userId; + IsApproved = isApproved; + + // this cannot be null but can be empty + _rawPasswordValue = string.Empty; + } + + /// + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's + /// password + /// + /// + public Member(string? name, string email, string username, string? rawPasswordValue, IMemberType? contentType) + : base(name, -1, contentType, new PropertyCollection()) + { + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + IsApproved = true; + } + + /// + /// Initializes a new instance of the class. + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's + /// password + /// + /// + /// + public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved) + : base(name, -1, contentType, new PropertyCollection()) + { + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + IsApproved = isApproved; + } + + /// + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's + /// password + /// + /// + /// + /// + public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved, int userId) + : base(name, -1, contentType, new PropertyCollection()) + { + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + IsApproved = isApproved; + CreatorId = userId; + } + + /// + /// Gets or sets the Groups that Member is part of + /// + [DataMember] + public IEnumerable? Groups { get; set; } + + /// + /// Gets or sets the Username + /// + [DataMember] + public string Username + { + get => _username; + set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + } + + /// + /// Gets or sets the Email + /// + [DataMember] + public string Email + { + get => _email; + set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + } + + [DataMember] + public DateTime? EmailConfirmedDate + { + get => _emailConfirmedDate; + set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + } + + /// + /// Gets or sets the raw password value + /// + [IgnoreDataMember] + public string? RawPasswordValue + { + get => _rawPasswordValue; + set + { + if (value == null) { - if (value == null) - { - //special case, this is used to ensure that the password is not updated when persisting, in this case - //we don't want to track changes either - _rawPasswordValue = null; - } - else - { - SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); - } + // special case, this is used to ensure that the password is not updated when persisting, in this case + // we don't want to track changes either + _rawPasswordValue = null; + } + else + { + SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); } } + } - [IgnoreDataMember] - public string? PasswordConfiguration + [IgnoreDataMember] + public string? PasswordConfiguration + { + get => _passwordConfig; + set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + } + + // TODO: When get/setting all of these properties we MUST: + // * Check if we are using the umbraco membership provider, if so then we need to use the configured fields - not the explicit fields below + // * If any of the fields don't exist, what should we do? Currently it will throw an exception! + + /// + /// Gets or set the comments for the member + /// + /// + /// Alias: umbracoMemberComments + /// Part of the standard properties collection. + /// + [DataMember] + public string? Comments + { + get { - get => _passwordConfig; - set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + Attempt a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string)); + if (a.Success == false) + { + return a.Result; + } + + return Properties[Constants.Conventions.Member.Comments]?.GetValue() == null + ? string.Empty + : Properties[Constants.Conventions.Member.Comments]?.GetValue()?.ToString(); } - /// - /// Gets or sets the Groups that Member is part of - /// - [DataMember] - public IEnumerable? Groups { get; set; } - - // TODO: When get/setting all of these properties we MUST: - // * Check if we are using the umbraco membership provider, if so then we need to use the configured fields - not the explicit fields below - // * If any of the fields don't exist, what should we do? Currently it will throw an exception! - - /// - /// Gets or set the comments for the member - /// - /// - /// Alias: umbracoMemberComments - /// Part of the standard properties collection. - /// - [DataMember] - public string? Comments + set { - get - { - var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.Comments, nameof(Comments), default(string)); - if (a.Success == false) - return a.Result; - - return Properties[Constants.Conventions.Member.Comments]?.GetValue() == null - ? string.Empty - : Properties[Constants.Conventions.Member.Comments]?.GetValue()?.ToString(); - } - set - { - if (WarnIfPropertyTypeNotFoundOnSet( + if (WarnIfPropertyTypeNotFoundOnSet( Constants.Conventions.Member.Comments, nameof(Comments)) == false) - return; - - Properties[Constants.Conventions.Member.Comments]?.SetValue(value); - } - } - - /// - /// Gets or sets a value indicating whether the Member is approved - /// - [DataMember] - public bool IsApproved - { - get => _isApproved; - set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); - } - - /// - /// Gets or sets a boolean indicating whether the Member is locked out - /// - /// - /// Alias: umbracoMemberLockedOut - /// Part of the standard properties collection. - /// - [DataMember] - public bool IsLockedOut - { - get => _isLockedOut; - set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); - } - - /// - /// Gets or sets the date for last login - /// - /// - /// Alias: umbracoMemberLastLogin - /// Part of the standard properties collection. - /// - [DataMember] - public DateTime? LastLoginDate - { - get => _lastLoginDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); - } - - /// - /// Gest or sets the date for last password change - /// - /// - /// Alias: umbracoMemberLastPasswordChangeDate - /// Part of the standard properties collection. - /// - [DataMember] - public DateTime? LastPasswordChangeDate - { - get => _lastPasswordChangeDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDate, nameof(LastPasswordChangeDate)); - } - - /// - /// Gets or sets the date for when Member was locked out - /// - /// - /// Alias: umbracoMemberLastLockoutDate - /// Part of the standard properties collection. - /// - [DataMember] - public DateTime? LastLockoutDate - { - get => _lastLockoutDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); - } - - /// - /// Gets or sets the number of failed password attempts. - /// This is the number of times the password was entered incorrectly upon login. - /// - /// - /// Alias: umbracoMemberFailedPasswordAttempts - /// Part of the standard properties collection. - /// - [DataMember] - public int FailedPasswordAttempts - { - get => _failedPasswordAttempts; - set => SetPropertyValueAndDetectChanges(value, ref _failedPasswordAttempts, nameof(FailedPasswordAttempts)); - } - - /// - /// String alias of the default ContentType - /// - [DataMember] - public virtual string ContentTypeAlias => ContentType.Alias; - - /// - /// The security stamp used by ASP.Net identity - /// - [IgnoreDataMember] - public string? SecurityStamp - { - get => _securityStamp; - set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); - } - - - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public string? LongStringPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public string? ShortStringPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public int IntegerPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public bool BoolPropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public DateTime DateTimePropertyValue { get; set; } - /// - /// Internal/Experimental - only used for mapping queries. - /// - /// - /// Adding these to have first level properties instead of the Properties collection. - /// - [IgnoreDataMember] - [EditorBrowsable(EditorBrowsableState.Never)] - public string? PropertyTypeAlias { get; set; } - - private Attempt WarnIfPropertyTypeNotFoundOnGet(string propertyAlias, string propertyName, T defaultVal) - { - void DoLog(string logPropertyAlias, string logPropertyName) { - StaticApplicationLogging.Logger.LogWarning("Trying to access the '{PropertyName}' property on '{MemberType}' " + - "but the {PropertyAlias} property does not exist on the member type so a default value is returned. " + - "Ensure that you have a property type with alias: {PropertyAlias} configured on your member type in order to use the '{PropertyName}' property on the model correctly.", - logPropertyName, - typeof(Member), - logPropertyAlias); + return; } - // if the property doesn't exist, - if (Properties.Contains(propertyAlias) == false) - { - // put a warn in the log if this entity has been persisted - // then return a failure - if (HasIdentity) - DoLog(propertyAlias, propertyName); - return Attempt.Fail(defaultVal); - } - - return Attempt.Succeed(); + Properties[Constants.Conventions.Member.Comments]?.SetValue(value); } + } - private bool WarnIfPropertyTypeNotFoundOnSet(string propertyAlias, string propertyName) + /// + /// Gets or sets a value indicating whether the Member is approved + /// + [DataMember] + public bool IsApproved + { + get => _isApproved; + set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); + } + + /// + /// Gets or sets a boolean indicating whether the Member is locked out + /// + /// + /// Alias: umbracoMemberLockedOut + /// Part of the standard properties collection. + /// + [DataMember] + public bool IsLockedOut + { + get => _isLockedOut; + set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); + } + + /// + /// Gets or sets the date for last login + /// + /// + /// Alias: umbracoMemberLastLogin + /// Part of the standard properties collection. + /// + [DataMember] + public DateTime? LastLoginDate + { + get => _lastLoginDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); + } + + /// + /// Gest or sets the date for last password change + /// + /// + /// Alias: umbracoMemberLastPasswordChangeDate + /// Part of the standard properties collection. + /// + [DataMember] + public DateTime? LastPasswordChangeDate + { + get => _lastPasswordChangeDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDate, nameof(LastPasswordChangeDate)); + } + + /// + /// Gets or sets the date for when Member was locked out + /// + /// + /// Alias: umbracoMemberLastLockoutDate + /// Part of the standard properties collection. + /// + [DataMember] + public DateTime? LastLockoutDate + { + get => _lastLockoutDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); + } + + /// + /// Gets or sets the number of failed password attempts. + /// This is the number of times the password was entered incorrectly upon login. + /// + /// + /// Alias: umbracoMemberFailedPasswordAttempts + /// Part of the standard properties collection. + /// + [DataMember] + public int FailedPasswordAttempts + { + get => _failedPasswordAttempts; + set => SetPropertyValueAndDetectChanges(value, ref _failedPasswordAttempts, nameof(FailedPasswordAttempts)); + } + + /// + /// String alias of the default ContentType + /// + [DataMember] + public virtual string ContentTypeAlias => ContentType.Alias; + + /// + /// The security stamp used by ASP.Net identity + /// + [IgnoreDataMember] + public string? SecurityStamp + { + get => _securityStamp; + set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); + } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public string? LongStringPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public string? ShortStringPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public int IntegerPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public bool BoolPropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public DateTime DateTimePropertyValue { get; set; } + + /// + /// Internal/Experimental - only used for mapping queries. + /// + /// + /// Adding these to have first level properties instead of the Properties collection. + /// + [IgnoreDataMember] + [EditorBrowsable(EditorBrowsableState.Never)] + public string? PropertyTypeAlias { get; set; } + + /// + [DataMember] + [DoNotClone] + public IDictionary AdditionalData => _additionalData ??= new Dictionary(); + + /// + [IgnoreDataMember] + public bool HasAdditionalData => _additionalData != null; + + private Attempt WarnIfPropertyTypeNotFoundOnGet(string propertyAlias, string propertyName, T defaultVal) + { + static void DoLog(string logPropertyAlias, string logPropertyName) { - void DoLog(string logPropertyAlias, string logPropertyName) - { - StaticApplicationLogging.Logger.LogWarning("An attempt was made to set a value on the property '{PropertyName}' on type '{MemberType}' but the " + - "property type {PropertyAlias} does not exist on the member type, ensure that this property type exists so that setting this property works correctly.", - logPropertyName, - typeof(Member), - logPropertyAlias); - } - - // if the property doesn't exist, - if (Properties.Contains(propertyAlias) == false) - { - // put a warn in the log if this entity has been persisted - // then return a failure - if (HasIdentity) - DoLog(propertyAlias, propertyName); - return false; - } - - return true; + StaticApplicationLogging.Logger.LogWarning( + "Trying to access the '{PropertyName}' property on '{MemberType}' " + + "but the {PropertyAlias} property does not exist on the member type so a default value is returned. " + + "Ensure that you have a property type with alias: {PropertyAlias} configured on your member type in order to use the '{PropertyName}' property on the model correctly.", + logPropertyName, + typeof(Member), + logPropertyAlias); } - /// - [DataMember] - [DoNotClone] - public IDictionary? AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); + // if the property doesn't exist, + if (Properties.Contains(propertyAlias) == false) + { + // put a warn in the log if this entity has been persisted + // then return a failure + if (HasIdentity) + { + DoLog(propertyAlias, propertyName); + } - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; + return Attempt.Fail(defaultVal); + } + + return Attempt.Succeed(); + } + + private bool WarnIfPropertyTypeNotFoundOnSet(string propertyAlias, string propertyName) + { + static void DoLog(string logPropertyAlias, string logPropertyName) + { + StaticApplicationLogging.Logger.LogWarning( + "An attempt was made to set a value on the property '{PropertyName}' on type '{MemberType}' but the " + + "property type {PropertyAlias} does not exist on the member type, ensure that this property type exists so that setting this property works correctly.", + logPropertyName, + typeof(Member), + logPropertyAlias); + } + + // if the property doesn't exist, + if (Properties.Contains(propertyAlias) == false) + { + // put a warn in the log if this entity has been persisted + // then return a failure + if (HasIdentity) + { + DoLog(propertyAlias, propertyName); + } + + return false; + } + + return true; } } diff --git a/src/Umbraco.Core/Models/MemberGroup.cs b/src/Umbraco.Core/Models/MemberGroup.cs index 7a35b78875..5ae7a7edd2 100644 --- a/src/Umbraco.Core/Models/MemberGroup.cs +++ b/src/Umbraco.Core/Models/MemberGroup.cs @@ -1,53 +1,51 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a member type +/// +[Serializable] +[DataContract(IsReference = true)] +public class MemberGroup : EntityBase, IMemberGroup { - /// - /// Represents a member type - /// - [Serializable] - [DataContract(IsReference = true)] - public class MemberGroup : EntityBase, IMemberGroup + private IDictionary? _additionalData; + private int _creatorId; + private string? _name; + + /// + [DataMember] + [DoNotClone] + public IDictionary AdditionalData => +_additionalData ??= new Dictionary(); + + /// + [IgnoreDataMember] + public bool HasAdditionalData => _additionalData != null; + + [DataMember] + public string? Name { - private IDictionary? _additionalData; - private string? _name; - private int _creatorId; - - /// - [DataMember] - [DoNotClone] - public IDictionary AdditionalData => _additionalData ?? (_additionalData = new Dictionary()); - - /// - [IgnoreDataMember] - public bool HasAdditionalData => _additionalData != null; - - [DataMember] - public string? Name + get => _name; + set { - get => _name; - set + if (_name != value) { - if (_name != value) - { - //if the name has changed, add the value to the additional data, - //this is required purely for event handlers to know the previous name of the group - //so we can keep the public access up to date. - AdditionalData["previousName"] = _name; - } - - SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + // if the name has changed, add the value to the additional data, + // this is required purely for event handlers to know the previous name of the group + // so we can keep the public access up to date. + AdditionalData["previousName"] = _name; } - } - [DataMember] - public int CreatorId - { - get => _creatorId; - set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); + SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); } } + + [DataMember] + public int CreatorId + { + get => _creatorId; + set => SetPropertyValueAndDetectChanges(value, ref _creatorId, nameof(CreatorId)); + } } diff --git a/src/Umbraco.Core/Models/MemberPropertyModel.cs b/src/Umbraco.Core/Models/MemberPropertyModel.cs index f6d06956e5..96466af397 100644 --- a/src/Umbraco.Core/Models/MemberPropertyModel.cs +++ b/src/Umbraco.Core/Models/MemberPropertyModel.cs @@ -1,37 +1,34 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A simple representation of an Umbraco member property +/// +public class MemberPropertyModel { - /// - /// A simple representation of an Umbraco member property - /// - public class MemberPropertyModel - { - [Required] - public string Alias { get; set; } = null!; + [Required] + public string Alias { get; set; } = null!; - //NOTE: This has to be a string currently, if it is an object it will bind as an array which we don't want. - // If we want to have this as an 'object' with a true type on it, we have to create a custom model binder - // for an UmbracoProperty and then bind with the correct type based on the property type for this alias. This - // would be a bit long winded and perhaps unnecessary. The reason is because it is always posted as a string anyways - // and when we set this value on the property object that gets sent to the database we do a TryConvertTo to the - // real type anyways. + // NOTE: This has to be a string currently, if it is an object it will bind as an array which we don't want. + // If we want to have this as an 'object' with a true type on it, we have to create a custom model binder + // for an UmbracoProperty and then bind with the correct type based on the property type for this alias. This + // would be a bit long winded and perhaps unnecessary. The reason is because it is always posted as a string anyways + // and when we set this value on the property object that gets sent to the database we do a TryConvertTo to the + // real type anyways. + [DataType(System.ComponentModel.DataAnnotations.DataType.Text)] + public string? Value { get; set; } - [DataType(System.ComponentModel.DataAnnotations.DataType.Text)] - public string? Value { get; set; } + [ReadOnly(true)] + public string? Name { get; set; } - [ReadOnly(true)] - public string? Name { get; set; } + // TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers can just render their own inside the view - // TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers can just render their own inside the view - - ///// - ///// This can dynamically be set to a custom template name to change - ///// the editor type for this property - ///// - //[ReadOnly(true)] - //public string EditorTemplate { get; set; } - - } + ///// + ///// This can dynamically be set to a custom template name to change + ///// the editor type for this property + ///// + // [ReadOnly(true)] + // public string EditorTemplate { get; set; } } diff --git a/src/Umbraco.Core/Models/MemberType.cs b/src/Umbraco.Core/Models/MemberType.cs index 4db8388b94..502a61df9f 100644 --- a/src/Umbraco.Core/Models/MemberType.cs +++ b/src/Umbraco.Core/Models/MemberType.cs @@ -1,169 +1,172 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the content type that a object is based on +/// +[Serializable] +[DataContract(IsReference = true)] +public class MemberType : ContentTypeCompositionBase, IMemberType { + public const bool SupportsPublishingConst = false; + private readonly IShortStringHelper _shortStringHelper; + /// - /// Represents the content type that a object is based on + /// Gets or Sets a Dictionary of Tuples (MemberCanEdit, VisibleOnProfile, IsSensitive) by the PropertyTypes' alias. /// - [Serializable] - [DataContract(IsReference = true)] - public class MemberType : ContentTypeCompositionBase, IMemberType + private readonly IDictionary _memberTypePropertyTypes; + + // Dictionary is divided into string: PropertyTypeAlias, Tuple: MemberCanEdit, VisibleOnProfile, PropertyTypeId + private string _alias = string.Empty; + + public MemberType(IShortStringHelper shortStringHelper, int parentId) + : base(shortStringHelper, parentId) { - private readonly IShortStringHelper _shortStringHelper; - public const bool SupportsPublishingConst = false; + _shortStringHelper = shortStringHelper; + _memberTypePropertyTypes = new Dictionary(); + } - //Dictionary is divided into string: PropertyTypeAlias, Tuple: MemberCanEdit, VisibleOnProfile, PropertyTypeId - private string _alias = string.Empty; + public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent) + : this( + shortStringHelper, + parent, + string.Empty) + { + } - public MemberType(IShortStringHelper shortStringHelper, int parentId) : base(shortStringHelper, parentId) + public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) + : base(shortStringHelper, parent, alias) + { + _shortStringHelper = shortStringHelper; + _memberTypePropertyTypes = new Dictionary(); + } + + /// + public override bool SupportsPublishing => SupportsPublishingConst; + + public override ContentVariation Variations + { + // note: although technically possible, variations on members don't make much sense + // and therefore are disabled - they are fully supported at service level, though, + // but not at published snapshot level. + get => base.Variations; + set => throw new NotSupportedException("Variations are not supported on members."); + } + + /// + public override ISimpleContentType ToSimple() => new SimpleContentType(this); + + /// + /// The Alias of the ContentType + /// + [DataMember] + public override string Alias + { + get => _alias; + set { - _shortStringHelper = shortStringHelper; - _memberTypePropertyTypes = new Dictionary(); + // NOTE: WE are overriding this because we don't want to do a ToSafeAlias when the alias is the special case of + // "_umbracoSystemDefaultProtectType" which is used internally, currently there is an issue with the safe alias as it strips + // leading underscores which we don't want in this case. + // see : http://issues.umbraco.org/issue/U4-3968 + + // TODO: BUT, I'm pretty sure we could do this with regards to underscores now: + // .ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase) + // Need to ask Stephen + var newVal = value == "_umbracoSystemDefaultProtectType" + ? value + : value == null + ? string.Empty + : value.ToSafeAlias(_shortStringHelper); + + SetPropertyValueAndDetectChanges(newVal, ref _alias!, nameof(Alias)); } + } - public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent) : this(shortStringHelper, parent, string.Empty) + /// + /// Gets a boolean indicating whether a Property is editable by the Member. + /// + /// PropertyType Alias of the Property to check + /// + public bool MemberCanEditProperty(string? propertyTypeAlias) => propertyTypeAlias is not null && + _memberTypePropertyTypes.TryGetValue( + propertyTypeAlias, + out MemberTypePropertyProfileAccess? propertyProfile) && + propertyProfile.IsEditable; + + /// + /// Gets a boolean indicating whether a Property is visible on the Members profile. + /// + /// PropertyType Alias of the Property to check + /// + public bool MemberCanViewProperty(string propertyTypeAlias) => + _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile) && + propertyProfile.IsVisible; + + /// + /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile. + /// + /// PropertyType Alias of the Property to check + /// + public bool IsSensitiveProperty(string propertyTypeAlias) => + _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile) && + propertyProfile.IsSensitive; + + /// + /// Sets a boolean indicating whether a Property is editable by the Member. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + public void SetMemberCanEditProperty(string propertyTypeAlias, bool value) + { + if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile)) { + propertyProfile.IsEditable = value; } - - public MemberType(IShortStringHelper shortStringHelper, IContentTypeComposition parent, string alias) - : base(shortStringHelper, parent, alias) + else { - _shortStringHelper = shortStringHelper; - _memberTypePropertyTypes = new Dictionary(); + var tuple = new MemberTypePropertyProfileAccess(false, value, false); + _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); } + } - /// - public override ISimpleContentType ToSimple() => new SimpleContentType(this); - - /// - public override bool SupportsPublishing => SupportsPublishingConst; - - public override ContentVariation Variations + /// + /// Sets a boolean indicating whether a Property is visible on the Members profile. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + public void SetMemberCanViewProperty(string propertyTypeAlias, bool value) + { + if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile)) { - // note: although technically possible, variations on members don't make much sense - // and therefore are disabled - they are fully supported at service level, though, - // but not at published snapshot level. - - get => base.Variations; - set => throw new NotSupportedException("Variations are not supported on members."); + propertyProfile.IsVisible = value; } - - /// - /// The Alias of the ContentType - /// - [DataMember] - public override string Alias + else { - get => _alias; - set - { - //NOTE: WE are overriding this because we don't want to do a ToSafeAlias when the alias is the special case of - // "_umbracoSystemDefaultProtectType" which is used internally, currently there is an issue with the safe alias as it strips - // leading underscores which we don't want in this case. - // see : http://issues.umbraco.org/issue/U4-3968 - - // TODO: BUT, I'm pretty sure we could do this with regards to underscores now: - // .ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase) - // Need to ask Stephen - - var newVal = value == "_umbracoSystemDefaultProtectType" - ? value - : (value == null ? string.Empty : value.ToSafeAlias(_shortStringHelper)); - - SetPropertyValueAndDetectChanges(newVal, ref _alias!, nameof(Alias)); - } + var tuple = new MemberTypePropertyProfileAccess(value, false, false); + _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); } + } - /// - /// Gets or Sets a Dictionary of Tuples (MemberCanEdit, VisibleOnProfile, IsSensitive) by the PropertyTypes' alias. - /// - private IDictionary _memberTypePropertyTypes; - - /// - /// Gets a boolean indicating whether a Property is editable by the Member. - /// - /// PropertyType Alias of the Property to check - /// - public bool MemberCanEditProperty(string? propertyTypeAlias) + /// + /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile. + /// + /// PropertyType Alias of the Property to set + /// Boolean value, true or false + public void SetIsSensitiveProperty(string propertyTypeAlias, bool value) + { + if (_memberTypePropertyTypes.TryGetValue( + propertyTypeAlias, out MemberTypePropertyProfileAccess? propertyProfile)) { - return propertyTypeAlias is not null && _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsEditable; + propertyProfile.IsSensitive = value; } - - /// - /// Gets a boolean indicating whether a Property is visible on the Members profile. - /// - /// PropertyType Alias of the Property to check - /// - public bool MemberCanViewProperty(string propertyTypeAlias) + else { - return _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsVisible; - } - /// - /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile. - /// - /// PropertyType Alias of the Property to check - /// - public bool IsSensitiveProperty(string propertyTypeAlias) - { - return _memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsSensitive; - } - - /// - /// Sets a boolean indicating whether a Property is editable by the Member. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - public void SetMemberCanEditProperty(string propertyTypeAlias, bool value) - { - if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile)) - { - propertyProfile.IsEditable = value; - } - else - { - var tuple = new MemberTypePropertyProfileAccess(false, value, false); - _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); - } - } - - /// - /// Sets a boolean indicating whether a Property is visible on the Members profile. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - public void SetMemberCanViewProperty(string propertyTypeAlias, bool value) - { - if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile)) - { - propertyProfile.IsVisible = value; - } - else - { - var tuple = new MemberTypePropertyProfileAccess(value, false, false); - _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); - } - } - - /// - /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile. - /// - /// PropertyType Alias of the Property to set - /// Boolean value, true or false - public void SetIsSensitiveProperty(string propertyTypeAlias, bool value) - { - if (_memberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile)) - { - propertyProfile.IsSensitive = value; - } - else - { - var tuple = new MemberTypePropertyProfileAccess(false, false, value); - _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); - } + var tuple = new MemberTypePropertyProfileAccess(false, false, value); + _memberTypePropertyTypes.Add(propertyTypeAlias, tuple); } } } diff --git a/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs b/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs index 89bf2f283d..e6e619354b 100644 --- a/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs +++ b/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs @@ -1,19 +1,20 @@ -namespace Umbraco.Cms.Core.Models -{ - /// - /// Used to track the property types that are visible/editable on member profiles - /// - public class MemberTypePropertyProfileAccess - { - public MemberTypePropertyProfileAccess(bool isVisible, bool isEditable, bool isSenstive) - { - IsVisible = isVisible; - IsEditable = isEditable; - IsSensitive = isSenstive; - } +namespace Umbraco.Cms.Core.Models; - public bool IsVisible { get; set; } - public bool IsEditable { get; set; } - public bool IsSensitive { get; set; } +/// +/// Used to track the property types that are visible/editable on member profiles +/// +public class MemberTypePropertyProfileAccess +{ + public MemberTypePropertyProfileAccess(bool isVisible, bool isEditable, bool isSenstive) + { + IsVisible = isVisible; + IsEditable = isEditable; + IsSensitive = isSenstive; } + + public bool IsVisible { get; set; } + + public bool IsEditable { get; set; } + + public bool IsSensitive { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs index 9c585589fa..613a873d7a 100644 --- a/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs @@ -1,55 +1,42 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Represents an -> user group & permission key value pair collection +/// +/// +/// This implements purely so it can be used with the repository layer which is why it's +/// explicitly implemented. +/// +public class ContentPermissionSet : EntityPermissionSet, IEntity { - /// - /// Represents an -> user group & permission key value pair collection - /// - /// - /// This implements purely so it can be used with the repository layer which is why it's explicitly implemented. - /// - public class ContentPermissionSet : EntityPermissionSet, IEntity + private readonly IContent _content; + + public ContentPermissionSet(IContent content, EntityPermissionCollection permissionsSet) + : base(content.Id, permissionsSet) => + _content = content; + + public override int EntityId => _content.Id; + + int IEntity.Id { - private readonly IContent _content; - - public ContentPermissionSet(IContent content, EntityPermissionCollection permissionsSet) - : base(content.Id, permissionsSet) - { - _content = content; - } - - public override int EntityId - { - get { return _content.Id; } - } - - #region Explicit implementation of IAggregateRoot - int IEntity.Id - { - get { return EntityId; } - set { throw new NotImplementedException(); } - } - - bool IEntity.HasIdentity - { - get { return EntityId > 0; } - } - - void IEntity.ResetIdentity() => throw new InvalidOperationException($"Resetting identity on {nameof(ContentPermissionSet)} is invalid"); - - Guid IEntity.Key { get; set; } - - DateTime IEntity.CreateDate { get; set; } - - DateTime IEntity.UpdateDate { get; set; } - - DateTime? IEntity.DeleteDate { get; set; } - - object IDeepCloneable.DeepClone() - { - throw new NotImplementedException(); - } - #endregion + get => EntityId; + set => throw new NotImplementedException(); } + + bool IEntity.HasIdentity => EntityId > 0; + + Guid IEntity.Key { get; set; } + + void IEntity.ResetIdentity() => + throw new InvalidOperationException($"Resetting identity on {nameof(ContentPermissionSet)} is invalid"); + + DateTime IEntity.CreateDate { get; set; } + + DateTime IEntity.UpdateDate { get; set; } + + DateTime? IEntity.DeleteDate { get; set; } + + object IDeepCloneable.DeepClone() => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs index a86c844622..58e84f27f9 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs @@ -1,66 +1,84 @@ -using System; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +/// +/// Represents an entity permission (defined on the user group and derived to retrieve permissions for a given user) +/// +public class EntityPermission : IEquatable { - /// - /// Represents an entity permission (defined on the user group and derived to retrieve permissions for a given user) - /// - public class EntityPermission : IEquatable + public EntityPermission(int groupId, int entityId, string[] assignedPermissions) { - public EntityPermission(int groupId, int entityId, string[] assignedPermissions) - { - UserGroupId = groupId; - EntityId = entityId; - AssignedPermissions = assignedPermissions; - IsDefaultPermissions = false; - } - - public EntityPermission(int groupId, int entityId, string[] assignedPermissions, bool isDefaultPermissions) - { - UserGroupId = groupId; - EntityId = entityId; - AssignedPermissions = assignedPermissions; - IsDefaultPermissions = isDefaultPermissions; - } - - public int EntityId { get; private set; } - public int UserGroupId { get; private set; } - - /// - /// The assigned permissions for the user/entity combo - /// - public string[] AssignedPermissions { get; private set; } - - /// - /// True if the permissions assigned to this object are the group's default permissions and not explicitly defined permissions - /// - /// - /// This will be the case when looking up entity permissions and falling back to the default permissions - /// - public bool IsDefaultPermissions { get; private set; } - - public bool Equals(EntityPermission? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return EntityId == other.EntityId && UserGroupId == other.UserGroupId; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((EntityPermission) obj); - } - - public override int GetHashCode() - { - unchecked - { - return (EntityId * 397) ^ UserGroupId; - } - } + UserGroupId = groupId; + EntityId = entityId; + AssignedPermissions = assignedPermissions; + IsDefaultPermissions = false; } + public EntityPermission(int groupId, int entityId, string[] assignedPermissions, bool isDefaultPermissions) + { + UserGroupId = groupId; + EntityId = entityId; + AssignedPermissions = assignedPermissions; + IsDefaultPermissions = isDefaultPermissions; + } + + public int EntityId { get; } + + public int UserGroupId { get; } + + /// + /// The assigned permissions for the user/entity combo + /// + public string[] AssignedPermissions { get; } + + /// + /// True if the permissions assigned to this object are the group's default permissions and not explicitly defined + /// permissions + /// + /// + /// This will be the case when looking up entity permissions and falling back to the default permissions + /// + public bool IsDefaultPermissions { get; } + + public bool Equals(EntityPermission? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return EntityId == other.EntityId && UserGroupId == other.UserGroupId; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((EntityPermission)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (EntityId * 397) ^ UserGroupId; + } + } } diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs index ac03ef75d8..727f7964f7 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs @@ -1,57 +1,55 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +/// +/// A of +/// +public class EntityPermissionCollection : HashSet { - /// - /// A of - /// - public class EntityPermissionCollection : HashSet + private Dictionary? _aggregateNodePermissions; + + private string[]? _aggregatePermissions; + + public EntityPermissionCollection() { - public EntityPermissionCollection() - { - } - - public EntityPermissionCollection(IEnumerable collection) : base(collection) - { - } - - /// - /// Returns the aggregate permissions in the permission set for a single node - /// - /// - /// - /// This value is only calculated once per node - /// - public IEnumerable GetAllPermissions(int entityId) - { - if (_aggregateNodePermissions == null) - _aggregateNodePermissions = new Dictionary(); - - string[]? entityPermissions; - if (_aggregateNodePermissions.TryGetValue(entityId, out entityPermissions) == false) - { - entityPermissions = this.Where(x => x.EntityId == entityId).SelectMany(x => x.AssignedPermissions).Distinct().ToArray(); - _aggregateNodePermissions[entityId] = entityPermissions; - } - return entityPermissions; - } - - private Dictionary? _aggregateNodePermissions; - - /// - /// Returns the aggregate permissions in the permission set for all nodes - /// - /// - /// - /// This value is only calculated once - /// - public IEnumerable GetAllPermissions() - { - return _aggregatePermissions ?? (_aggregatePermissions = - this.SelectMany(x => x.AssignedPermissions).Distinct().ToArray()); - } - - private string[]? _aggregatePermissions; } + + public EntityPermissionCollection(IEnumerable collection) + : base(collection) + { + } + + /// + /// Returns the aggregate permissions in the permission set for a single node + /// + /// + /// + /// This value is only calculated once per node + /// + public IEnumerable GetAllPermissions(int entityId) + { + if (_aggregateNodePermissions == null) + { + _aggregateNodePermissions = new Dictionary(); + } + + if (_aggregateNodePermissions.TryGetValue(entityId, out string[]? entityPermissions) == false) + { + entityPermissions = this.Where(x => x.EntityId == entityId).SelectMany(x => x.AssignedPermissions) + .Distinct().ToArray(); + _aggregateNodePermissions[entityId] = entityPermissions; + } + + return entityPermissions; + } + + /// + /// Returns the aggregate permissions in the permission set for all nodes + /// + /// + /// + /// This value is only calculated once + /// + public IEnumerable GetAllPermissions() => +_aggregatePermissions ??= + this.SelectMany(x => x.AssignedPermissions).Distinct().ToArray(); } diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs index 68e97a5d9f..0ae0dbf335 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs @@ -1,54 +1,41 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +/// +/// Represents an entity -> user group & permission key value pair collection +/// +public class EntityPermissionSet { - /// - /// Represents an entity -> user group & permission key value pair collection - /// - public class EntityPermissionSet + private static readonly Lazy EmptyInstance = + new(() => new EntityPermissionSet(-1, new EntityPermissionCollection())); + + public EntityPermissionSet(int entityId, EntityPermissionCollection permissionsSet) { - private static readonly Lazy EmptyInstance = new Lazy(() => new EntityPermissionSet(-1, new EntityPermissionCollection())); - /// - /// Returns an empty permission set - /// - /// - public static EntityPermissionSet Empty() - { - return EmptyInstance.Value; - } - - public EntityPermissionSet(int entityId, EntityPermissionCollection permissionsSet) - { - EntityId = entityId; - PermissionsSet = permissionsSet; - } - - /// - /// The entity id with permissions assigned - /// - public virtual int EntityId { get; private set; } - - /// - /// The key/value pairs of user group id & single permission - /// - public EntityPermissionCollection PermissionsSet { get; private set; } - - - /// - /// Returns the aggregate permissions in the permission set - /// - /// - /// - /// This value is only calculated once - /// - public IEnumerable GetAllPermissions() - { - return PermissionsSet.GetAllPermissions(); - } - - - - + EntityId = entityId; + PermissionsSet = permissionsSet; } + + /// + /// The entity id with permissions assigned + /// + public virtual int EntityId { get; } + + /// + /// The key/value pairs of user group id & single permission + /// + public EntityPermissionCollection PermissionsSet { get; } + + /// + /// Returns an empty permission set + /// + /// + public static EntityPermissionSet Empty() => EmptyInstance.Value; + + /// + /// Returns the aggregate permissions in the permission set + /// + /// + /// + /// This value is only calculated once + /// + public IEnumerable GetAllPermissions() => PermissionsSet.GetAllPermissions(); } diff --git a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs index f8efe55885..704158a1af 100644 --- a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs +++ b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs @@ -1,50 +1,55 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Defines the base contract for and +/// +public interface IMembershipUser : IEntity { + string Username { get; set; } + + string Email { get; set; } + + DateTime? EmailConfirmedDate { get; set; } + /// - /// Defines the base contract for and + /// Gets or sets the raw password value /// - public interface IMembershipUser : IEntity - { - string Username { get; set; } - string Email { get; set; } - DateTime? EmailConfirmedDate { get; set; } + string? RawPasswordValue { get; set; } - /// - /// Gets or sets the raw password value - /// - string? RawPasswordValue { get; set; } + /// + /// The user's specific password config (i.e. algorithm type, etc...) + /// + string? PasswordConfiguration { get; set; } - /// - /// The user's specific password config (i.e. algorithm type, etc...) - /// - string? PasswordConfiguration { get; set; } + string? Comments { get; set; } - string? Comments { get; set; } - bool IsApproved { get; set; } - bool IsLockedOut { get; set; } - DateTime? LastLoginDate { get; set; } - DateTime? LastPasswordChangeDate { get; set; } - DateTime? LastLockoutDate { get; set; } + bool IsApproved { get; set; } - /// - /// Gets or sets the number of failed password attempts. - /// This is the number of times the password was entered incorrectly upon login. - /// - /// - /// Alias: umbracoMemberFailedPasswordAttempts - /// Part of the standard properties collection. - /// - int FailedPasswordAttempts { get; set; } + bool IsLockedOut { get; set; } - /// - /// Gets or sets the security stamp used by ASP.NET Identity - /// - string? SecurityStamp { get; set; } + DateTime? LastLoginDate { get; set; } - //object ProfileId { get; set; } - //IEnumerable Groups { get; set; } - } + DateTime? LastPasswordChangeDate { get; set; } + + DateTime? LastLockoutDate { get; set; } + + /// + /// Gets or sets the number of failed password attempts. + /// This is the number of times the password was entered incorrectly upon login. + /// + /// + /// Alias: umbracoMemberFailedPasswordAttempts + /// Part of the standard properties collection. + /// + int FailedPasswordAttempts { get; set; } + + /// + /// Gets or sets the security stamp used by ASP.NET Identity + /// + string? SecurityStamp { get; set; } + + // object ProfileId { get; set; } + // IEnumerable Groups { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/IProfile.cs b/src/Umbraco.Core/Models/Membership/IProfile.cs index 395ebe0de8..f30bfd1225 100644 --- a/src/Umbraco.Core/Models/Membership/IProfile.cs +++ b/src/Umbraco.Core/Models/Membership/IProfile.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Defines the User Profile interface +/// +public interface IProfile { - /// - /// Defines the User Profile interface - /// - public interface IProfile - { - int Id { get; } - string? Name { get; } - } + int Id { get; } + + string? Name { get; } } diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index be84b4bca6..2096ec3d67 100644 --- a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -1,31 +1,33 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +/// +/// A readonly user group providing basic information +/// +public interface IReadOnlyUserGroup { + string? Name { get; } + + string? Icon { get; } + + int Id { get; } + + int? StartContentId { get; } + + int? StartMediaId { get; } + /// - /// A readonly user group providing basic information + /// The alias /// - public interface IReadOnlyUserGroup - { - string? Name { get; } - string? Icon { get; } - int Id { get; } - int? StartContentId { get; } - int? StartMediaId { get; } + string Alias { get; } - /// - /// The alias - /// - string Alias { get; } + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more + /// flexible permissions structure in the future. + /// + IEnumerable? Permissions { get; set; } - /// - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - IEnumerable? Permissions { get; set; } - - IEnumerable AllowedSections { get; } - } + IEnumerable AllowedSections { get; } } diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index c7c68dabda..6fc409a0c0 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -1,50 +1,52 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Defines the interface for a +/// +/// Will be left internal until a proper Membership implementation is part of the roadmap +public interface IUser : IMembershipUser, IRememberBeingDirty { + UserState UserState { get; } + + string? Name { get; set; } + + int SessionTimeout { get; set; } + + int[]? StartContentIds { get; set; } + + int[]? StartMediaIds { get; set; } + + string? Language { get; set; } + + DateTime? InvitedDate { get; set; } /// - /// Defines the interface for a + /// Gets the groups that user is part of /// - /// Will be left internal until a proper Membership implementation is part of the roadmap - public interface IUser : IMembershipUser, IRememberBeingDirty, ICanBeDirty - { - UserState UserState { get; } + IEnumerable Groups { get; } - string? Name { get; set; } - int SessionTimeout { get; set; } - int[]? StartContentIds { get; set; } - int[]? StartMediaIds { get; set; } - string? Language { get; set; } + IEnumerable AllowedSections { get; } - DateTime? InvitedDate { get; set; } + /// + /// Exposes the basic profile data + /// + IProfile ProfileData { get; } - /// - /// Gets the groups that user is part of - /// - IEnumerable Groups { get; } + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + string? Avatar { get; set; } - void RemoveGroup(string group); - void ClearGroups(); - void AddGroup(IReadOnlyUserGroup group); + /// + /// A Json blob stored for recording tour data for a user + /// + string? TourData { get; set; } - IEnumerable AllowedSections { get; } + void RemoveGroup(string group); - /// - /// Exposes the basic profile data - /// - IProfile ProfileData { get; } + void ClearGroups(); - /// - /// Will hold the media file system relative path of the users custom avatar if they uploaded one - /// - string? Avatar { get; set; } - - /// - /// A Json blob stored for recording tour data for a user - /// - string? TourData { get; set; } - } + void AddGroup(IReadOnlyUserGroup group); } diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index 96ae3c6dfb..71ef6a7a12 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -1,44 +1,44 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +public interface IUserGroup : IEntity, IRememberBeingDirty { - public interface IUserGroup : IEntity, IRememberBeingDirty - { - string Alias { get; set; } + string Alias { get; set; } - int? StartContentId { get; set; } - int? StartMediaId { get; set; } + int? StartContentId { get; set; } - /// - /// The icon - /// - string? Icon { get; set; } + int? StartMediaId { get; set; } - /// - /// The name - /// - string? Name { get; set; } + /// + /// The icon + /// + string? Icon { get; set; } - /// - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - IEnumerable? Permissions { get; set; } + /// + /// The name + /// + string? Name { get; set; } - IEnumerable AllowedSections { get; } + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more + /// flexible permissions structure in the future. + /// + IEnumerable? Permissions { get; set; } - void RemoveAllowedSection(string sectionAlias); + IEnumerable AllowedSections { get; } - void AddAllowedSection(string sectionAlias); + /// + /// Specifies the number of users assigned to this group + /// + int UserCount { get; } - void ClearAllowedSections(); + void RemoveAllowedSection(string sectionAlias); - /// - /// Specifies the number of users assigned to this group - /// - int UserCount { get; } - } + void AddAllowedSection(string sectionAlias); + + void ClearAllowedSections(); } diff --git a/src/Umbraco.Core/Models/Membership/MemberCountType.cs b/src/Umbraco.Core/Models/Membership/MemberCountType.cs index 89990994e8..6ff29bdee2 100644 --- a/src/Umbraco.Core/Models/Membership/MemberCountType.cs +++ b/src/Umbraco.Core/Models/Membership/MemberCountType.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// The types of members to count +/// +public enum MemberCountType { - /// - /// The types of members to count - /// - public enum MemberCountType - { - All, - LockedOut, - Approved - } + All, + LockedOut, + Approved, } diff --git a/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs b/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs index fec933190c..a34f1a8d1d 100644 --- a/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs +++ b/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs @@ -1,14 +1,16 @@ -using System; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +public class MemberExportProperty { - public class MemberExportProperty - { - public int Id { get; set; } - public string? Alias { get; set; } - public string? Name { get; set; } - public object? Value { get; set; } - public DateTime? CreateDate { get; set; } - public DateTime? UpdateDate { get; set; } - } + public int Id { get; set; } + + public string? Alias { get; set; } + + public string? Name { get; set; } + + public object? Value { get; set; } + + public DateTime? CreateDate { get; set; } + + public DateTime? UpdateDate { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/PersistedPasswordSettings.cs b/src/Umbraco.Core/Models/Membership/PersistedPasswordSettings.cs index 3e4831d9c3..f1c0463bdd 100644 --- a/src/Umbraco.Core/Models/Membership/PersistedPasswordSettings.cs +++ b/src/Umbraco.Core/Models/Membership/PersistedPasswordSettings.cs @@ -1,22 +1,22 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// The data stored against the user for their password configuration +/// +[DataContract(Name = "userPasswordSettings", Namespace = "")] +public class PersistedPasswordSettings { /// - /// The data stored against the user for their password configuration + /// The algorithm name /// - [DataContract(Name = "userPasswordSettings", Namespace = "")] - public class PersistedPasswordSettings - { - /// - /// The algorithm name - /// - /// - /// This doesn't explicitly need to map to a 'true' algorithm name, this may match an algorithm name alias that - /// uses many different options such as PBKDF2.ASPNETCORE.V3 which would map to the aspnetcore's v3 implementation of PBKDF2 - /// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. - /// - [DataMember(Name = "hashAlgorithm")] - public string? HashAlgorithm { get; set; } - } + /// + /// This doesn't explicitly need to map to a 'true' algorithm name, this may match an algorithm name alias that + /// uses many different options such as PBKDF2.ASPNETCORE.V3 which would map to the aspnetcore's v3 implementation of + /// PBKDF2 + /// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. + /// + [DataMember(Name = "hashAlgorithm")] + public string? HashAlgorithm { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs index 24543337ba..2e32f4172b 100644 --- a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs @@ -1,70 +1,82 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable { - public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable + public ReadOnlyUserGroup(int id, string? name, string? icon, int? startContentId, int? startMediaId, string? alias, IEnumerable allowedSections, IEnumerable? permissions) { - public ReadOnlyUserGroup(int id, string? name, string? icon, int? startContentId, int? startMediaId, string? @alias, - IEnumerable allowedSections, IEnumerable? permissions) - { - Name = name ?? string.Empty; - Icon = icon; - Id = id; - Alias = alias ?? string.Empty; - AllowedSections = allowedSections.ToArray(); - Permissions = permissions?.ToArray(); + Name = name ?? string.Empty; + Icon = icon; + Id = id; + Alias = alias ?? string.Empty; + AllowedSections = allowedSections.ToArray(); + Permissions = permissions?.ToArray(); - //Zero is invalid and will be treated as Null - StartContentId = startContentId == 0 ? null : startContentId; - StartMediaId = startMediaId == 0 ? null : startMediaId; - } - - public int Id { get; private set; } - public string Name { get; private set; } - public string? Icon { get; private set; } - public int? StartContentId { get; private set; } - public int? StartMediaId { get; private set; } - public string Alias { get; private set; } - - /// - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - public IEnumerable? Permissions { get; set; } - public IEnumerable AllowedSections { get; private set; } - - public bool Equals(ReadOnlyUserGroup? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(Alias, other.Alias); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((ReadOnlyUserGroup) obj); - } - - public override int GetHashCode() - { - return Alias?.GetHashCode() ?? base.GetHashCode(); - } - - public static bool operator ==(ReadOnlyUserGroup left, ReadOnlyUserGroup right) - { - return Equals(left, right); - } - - public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) - { - return !Equals(left, right); - } + // Zero is invalid and will be treated as Null + StartContentId = startContentId == 0 ? null : startContentId; + StartMediaId = startMediaId == 0 ? null : startMediaId; } + + public int Id { get; } + + public bool Equals(ReadOnlyUserGroup? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return string.Equals(Alias, other.Alias); + } + + public string Name { get; } + + public string? Icon { get; } + + public int? StartContentId { get; } + + public int? StartMediaId { get; } + + public string Alias { get; } + + /// + /// The set of default permissions + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more + /// flexible permissions structure in the future. + /// + public IEnumerable? Permissions { get; set; } + + public IEnumerable AllowedSections { get; } + + public static bool operator ==(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => Equals(left, right); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((ReadOnlyUserGroup)obj); + } + + public override int GetHashCode() => Alias?.GetHashCode() ?? base.GetHashCode(); + + public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 463b44c73e..4607b7c811 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -1,421 +1,466 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Represents a backoffice user +/// +[Serializable] +[DataContract(IsReference = true)] +public class User : EntityBase, IUser, IProfile { + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> IntegerEnumerableComparer = + new( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); + + private IEnumerable? _allowedSections; + private string? _avatar; + private string _email; + private DateTime? _emailConfirmedDate; + private int _failedLoginAttempts; + private DateTime? _invitedDate; + private bool _isApproved; + private bool _isLockedOut; + private string? _language; + private DateTime? _lastLockoutDate; + private DateTime? _lastLoginDate; + private DateTime? _lastPasswordChangedDate; + + private string _name; + private string? _passwordConfig; + private string? _rawPasswordValue; + private string? _securityStamp; + private int _sessionTimeout; + private int[]? _startContentIds; + private int[]? _startMediaIds; + private string? _tourData; + private HashSet _userGroups; + + private string _username; + /// - /// Represents a backoffice user + /// Constructor for creating a new/empty user /// - [Serializable] - [DataContract(IsReference = true)] - public class User : EntityBase, IUser, IProfile + public User(GlobalSettings globalSettings) { - /// - /// Constructor for creating a new/empty user - /// - public User(GlobalSettings globalSettings) + SessionTimeout = 60; + _userGroups = new HashSet(); + _language = globalSettings.DefaultUILanguage; + _isApproved = true; + _isLockedOut = false; + _startContentIds = new int[] { }; + _startMediaIds = new int[] { }; + + // cannot be null + _rawPasswordValue = string.Empty; + _username = string.Empty; + _email = string.Empty; + _name = string.Empty; + } + + /// + /// Constructor for creating a new/empty user + /// + /// + /// + /// + /// + /// + public User(GlobalSettings globalSettings, string? name, string email, string username, string rawPasswordValue) + : this(globalSettings) + { + if (string.IsNullOrWhiteSpace(name)) { - SessionTimeout = 60; - _userGroups = new HashSet(); - _language = globalSettings.DefaultUILanguage; - _isApproved = true; - _isLockedOut = false; - _startContentIds = new int[] { }; - _startMediaIds = new int[] { }; - //cannot be null - _rawPasswordValue = ""; - _username = string.Empty; - _email = string.Empty; - _name = string.Empty; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); } - /// - /// Constructor for creating a new/empty user - /// - /// - /// - /// - /// - public User(GlobalSettings globalSettings, string? name, string email, string username, string rawPasswordValue) - : this(globalSettings) + if (string.IsNullOrWhiteSpace(email)) { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(email)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - if (string.IsNullOrEmpty(rawPasswordValue)) throw new ArgumentException("Value cannot be null or empty.", nameof(rawPasswordValue)); - - _name = name; - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - _userGroups = new HashSet(); - _isApproved = true; - _isLockedOut = false; - _startContentIds = new int[] { }; - _startMediaIds = new int[] { }; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(email)); } - /// - /// Constructor for creating a new User instance for an existing user - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public User(GlobalSettings globalSettings, int id, string? name, string email, string? username, - string? rawPasswordValue, string? passwordConfig, - IEnumerable userGroups, int[] startContentIds, int[] startMediaIds) - : this(globalSettings) + if (string.IsNullOrWhiteSpace(username)) { - //we allow whitespace for this value so just check null - if (rawPasswordValue == null) throw new ArgumentNullException(nameof(rawPasswordValue)); - if (userGroups == null) throw new ArgumentNullException(nameof(userGroups)); - if (startContentIds == null) throw new ArgumentNullException(nameof(startContentIds)); - if (startMediaIds == null) throw new ArgumentNullException(nameof(startMediaIds)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - - Id = id; - _name = name; - _email = email; - _username = username; - _rawPasswordValue = rawPasswordValue; - _passwordConfig = passwordConfig; - _userGroups = new HashSet(userGroups); - _isApproved = true; - _isLockedOut = false; - _startContentIds = startContentIds; - _startMediaIds = startMediaIds; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); } - private string _name; - private string? _securityStamp; - private string? _avatar; - private string? _tourData; - private int _sessionTimeout; - private int[]? _startContentIds; - private int[]? _startMediaIds; - private int _failedLoginAttempts; - - private string _username; - private DateTime? _emailConfirmedDate; - private DateTime? _invitedDate; - private string _email; - private string? _rawPasswordValue; - private string? _passwordConfig; - private IEnumerable? _allowedSections; - private HashSet _userGroups; - private bool _isApproved; - private bool _isLockedOut; - private string? _language; - private DateTime? _lastPasswordChangedDate; - private DateTime? _lastLoginDate; - private DateTime? _lastLockoutDate; - - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> IntegerEnumerableComparer = - new DelegateEqualityComparer>( - (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), - enum1 => enum1.GetHashCode()); - - - [DataMember] - public DateTime? EmailConfirmedDate + if (string.IsNullOrEmpty(rawPasswordValue)) { - get => _emailConfirmedDate; - set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + throw new ArgumentException("Value cannot be null or empty.", nameof(rawPasswordValue)); } - [DataMember] - public DateTime? InvitedDate + _name = name; + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + _userGroups = new HashSet(); + _isApproved = true; + _isLockedOut = false; + _startContentIds = new int[] { }; + _startMediaIds = new int[] { }; + } + + /// + /// Constructor for creating a new User instance for an existing user + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public User( + GlobalSettings globalSettings, + int id, + string? name, + string email, + string? username, + string? rawPasswordValue, + string? passwordConfig, + IEnumerable userGroups, + int[] startContentIds, + int[] startMediaIds) + : this(globalSettings) + { + // we allow whitespace for this value so just check null + if (rawPasswordValue == null) { - get => _invitedDate; - set => SetPropertyValueAndDetectChanges(value, ref _invitedDate, nameof(InvitedDate)); + throw new ArgumentNullException(nameof(rawPasswordValue)); } - [DataMember] - public string Username + if (userGroups == null) { - get => _username; - set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + throw new ArgumentNullException(nameof(userGroups)); } - [DataMember] - public string Email + if (string.IsNullOrWhiteSpace(name)) { - get => _email; - set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); } - [IgnoreDataMember] - public string? RawPasswordValue + if (string.IsNullOrWhiteSpace(username)) { - get => _rawPasswordValue; - set => SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); } - [IgnoreDataMember] - public string? PasswordConfiguration - { - get => _passwordConfig; - set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); - } + Id = id; + _name = name; + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + _passwordConfig = passwordConfig; + _userGroups = new HashSet(userGroups); + _isApproved = true; + _isLockedOut = false; + _startContentIds = startContentIds ?? throw new ArgumentNullException(nameof(startContentIds)); + _startMediaIds = startMediaIds ?? throw new ArgumentNullException(nameof(startMediaIds)); + } - [DataMember] - public bool IsApproved - { - get => _isApproved; - set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); - } + [DataMember] + public DateTime? EmailConfirmedDate + { + get => _emailConfirmedDate; + set => SetPropertyValueAndDetectChanges(value, ref _emailConfirmedDate, nameof(EmailConfirmedDate)); + } - [IgnoreDataMember] - public bool IsLockedOut - { - get => _isLockedOut; - set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); - } + [DataMember] + public DateTime? InvitedDate + { + get => _invitedDate; + set => SetPropertyValueAndDetectChanges(value, ref _invitedDate, nameof(InvitedDate)); + } - [IgnoreDataMember] - public DateTime? LastLoginDate - { - get => _lastLoginDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); - } + [DataMember] + public string Username + { + get => _username; + set => SetPropertyValueAndDetectChanges(value, ref _username!, nameof(Username)); + } - [IgnoreDataMember] - public DateTime? LastPasswordChangeDate - { - get => _lastPasswordChangedDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangedDate, nameof(LastPasswordChangeDate)); - } + [DataMember] + public string Email + { + get => _email; + set => SetPropertyValueAndDetectChanges(value, ref _email!, nameof(Email)); + } - [IgnoreDataMember] - public DateTime? LastLockoutDate - { - get => _lastLockoutDate; - set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); - } + [IgnoreDataMember] + public string? RawPasswordValue + { + get => _rawPasswordValue; + set => SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue)); + } - [IgnoreDataMember] - public int FailedPasswordAttempts - { - get => _failedLoginAttempts; - set => SetPropertyValueAndDetectChanges(value, ref _failedLoginAttempts, nameof(FailedPasswordAttempts)); - } + [IgnoreDataMember] + public string? PasswordConfiguration + { + get => _passwordConfig; + set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration)); + } - [IgnoreDataMember] - public string? Comments { get; set; } + [DataMember] + public bool IsApproved + { + get => _isApproved; + set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved)); + } - public UserState UserState + [IgnoreDataMember] + public bool IsLockedOut + { + get => _isLockedOut; + set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut)); + } + + [IgnoreDataMember] + public DateTime? LastLoginDate + { + get => _lastLoginDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate)); + } + + [IgnoreDataMember] + public DateTime? LastPasswordChangeDate + { + get => _lastPasswordChangedDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangedDate, nameof(LastPasswordChangeDate)); + } + + [IgnoreDataMember] + public DateTime? LastLockoutDate + { + get => _lastLockoutDate; + set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate)); + } + + [IgnoreDataMember] + public int FailedPasswordAttempts + { + get => _failedLoginAttempts; + set => SetPropertyValueAndDetectChanges(value, ref _failedLoginAttempts, nameof(FailedPasswordAttempts)); + } + + [IgnoreDataMember] + public string? Comments { get; set; } + + public UserState UserState + { + get { - get + if (LastLoginDate == default && IsApproved == false && InvitedDate != null) { - if (LastLoginDate == default && IsApproved == false && InvitedDate != null) - return UserState.Invited; - - if (IsLockedOut) - return UserState.LockedOut; - if (IsApproved == false) - return UserState.Disabled; - - // User is not disabled or locked and has never logged in before - if (LastLoginDate == default && IsApproved && IsLockedOut == false) - return UserState.Inactive; - - return UserState.Active; + return UserState.Invited; } - } - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } - - public IEnumerable AllowedSections - { - get { return _allowedSections ?? (_allowedSections = new List(_userGroups.SelectMany(x => x.AllowedSections).Distinct())); } - } - - public IProfile ProfileData => new WrappedUserProfile(this); - - /// - /// The security stamp used by ASP.Net identity - /// - [IgnoreDataMember] - public string? SecurityStamp - { - get => _securityStamp; - set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); - } - - [DataMember] - public string? Avatar - { - get => _avatar; - set => SetPropertyValueAndDetectChanges(value, ref _avatar, nameof(Avatar)); - } - - /// - /// A Json blob stored for recording tour data for a user - /// - [DataMember] - public string? TourData - { - get => _tourData; - set => SetPropertyValueAndDetectChanges(value, ref _tourData, nameof(TourData)); - } - - /// - /// Gets or sets the session timeout. - /// - /// - /// The session timeout. - /// - [DataMember] - public int SessionTimeout - { - get => _sessionTimeout; - set => SetPropertyValueAndDetectChanges(value, ref _sessionTimeout, nameof(SessionTimeout)); - } - - /// - /// Gets or sets the start content id. - /// - /// - /// The start content id. - /// - [DataMember] - [DoNotClone] - public int[]? StartContentIds - { - get => _startContentIds; - set => SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), IntegerEnumerableComparer); - } - - /// - /// Gets or sets the start media id. - /// - /// - /// The start media id. - /// - [DataMember] - [DoNotClone] - public int[]? StartMediaIds - { - get => _startMediaIds; - set => SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), IntegerEnumerableComparer); - } - - [DataMember] - public string? Language - { - get => _language; - set => SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); - } - - /// - /// Gets the groups that user is part of - /// - [DataMember] - public IEnumerable Groups => _userGroups; - - public void RemoveGroup(string group) - { - foreach (var userGroup in _userGroups.ToArray()) + if (IsLockedOut) { - if (userGroup.Alias == group) - { - _userGroups.Remove(userGroup); - //reset this flag so it's rebuilt with the assigned groups - _allowedSections = null; - OnPropertyChanged(nameof(Groups)); - } + return UserState.LockedOut; } - } - public void ClearGroups() - { - if (_userGroups.Count > 0) + if (IsApproved == false) { - _userGroups.Clear(); - //reset this flag so it's rebuilt with the assigned groups + return UserState.Disabled; + } + + // User is not disabled or locked and has never logged in before + if (LastLoginDate == default && IsApproved && IsLockedOut == false) + { + return UserState.Inactive; + } + + return UserState.Active; + } + } + + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } + + public IEnumerable AllowedSections => _allowedSections ??= new List(_userGroups + .SelectMany(x => x.AllowedSections).Distinct()); + + public IProfile ProfileData => new WrappedUserProfile(this); + + /// + /// The security stamp used by ASP.Net identity + /// + [IgnoreDataMember] + public string? SecurityStamp + { + get => _securityStamp; + set => SetPropertyValueAndDetectChanges(value, ref _securityStamp, nameof(SecurityStamp)); + } + + [DataMember] + public string? Avatar + { + get => _avatar; + set => SetPropertyValueAndDetectChanges(value, ref _avatar, nameof(Avatar)); + } + + /// + /// A Json blob stored for recording tour data for a user + /// + [DataMember] + public string? TourData + { + get => _tourData; + set => SetPropertyValueAndDetectChanges(value, ref _tourData, nameof(TourData)); + } + + /// + /// Gets or sets the session timeout. + /// + /// + /// The session timeout. + /// + [DataMember] + public int SessionTimeout + { + get => _sessionTimeout; + set => SetPropertyValueAndDetectChanges(value, ref _sessionTimeout, nameof(SessionTimeout)); + } + + /// + /// Gets or sets the start content id. + /// + /// + /// The start content id. + /// + [DataMember] + [DoNotClone] + public int[]? StartContentIds + { + get => _startContentIds; + set => SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), IntegerEnumerableComparer); + } + + /// + /// Gets or sets the start media id. + /// + /// + /// The start media id. + /// + [DataMember] + [DoNotClone] + public int[]? StartMediaIds + { + get => _startMediaIds; + set => SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), IntegerEnumerableComparer); + } + + [DataMember] + public string? Language + { + get => _language; + set => SetPropertyValueAndDetectChanges(value, ref _language, nameof(Language)); + } + + /// + /// Gets the groups that user is part of + /// + [DataMember] + public IEnumerable Groups => _userGroups; + + public void RemoveGroup(string group) + { + foreach (IReadOnlyUserGroup userGroup in _userGroups.ToArray()) + { + if (userGroup.Alias == group) + { + _userGroups.Remove(userGroup); + + // reset this flag so it's rebuilt with the assigned groups _allowedSections = null; OnPropertyChanged(nameof(Groups)); } } + } - public void AddGroup(IReadOnlyUserGroup group) + public void ClearGroups() + { + if (_userGroups.Count > 0) { - if (_userGroups.Add(group)) + _userGroups.Clear(); + + // reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; + OnPropertyChanged(nameof(Groups)); + } + } + + public void AddGroup(IReadOnlyUserGroup group) + { + if (_userGroups.Add(group)) + { + // reset this flag so it's rebuilt with the assigned groups + _allowedSections = null; + OnPropertyChanged(nameof(Groups)); + } + } + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (User)clone; + + // manually clone the start node props + clonedEntity._startContentIds = _startContentIds?.ToArray(); + clonedEntity._startMediaIds = _startMediaIds?.ToArray(); + + // need to create new collections otherwise they'll get copied by ref + clonedEntity._userGroups = new HashSet(_userGroups); + clonedEntity._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; + } + + /// + /// Internal class used to wrap the user in a profile + /// + private class WrappedUserProfile : IProfile + { + private readonly IUser _user; + + public WrappedUserProfile(IUser user) => _user = user; + + public int Id => _user.Id; + + public string? Name => _user.Name; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - //reset this flag so it's rebuilt with the assigned groups - _allowedSections = null; - OnPropertyChanged(nameof(Groups)); + return false; } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((WrappedUserProfile)obj); } - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (User)clone; - - //manually clone the start node props - clonedEntity._startContentIds = _startContentIds?.ToArray(); - clonedEntity._startMediaIds = _startMediaIds?.ToArray(); - //need to create new collections otherwise they'll get copied by ref - clonedEntity._userGroups = new HashSet(_userGroups); - clonedEntity._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; - - } - - /// - /// Internal class used to wrap the user in a profile - /// - private class WrappedUserProfile : IProfile - { - private readonly IUser _user; - - public WrappedUserProfile(IUser user) - { - _user = user; - } - - public int Id => _user.Id; - - public string? Name => _user.Name; - - private bool Equals(WrappedUserProfile other) - { - return _user.Equals(other._user); - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((WrappedUserProfile) obj); - } - - public override int GetHashCode() - { - return _user.GetHashCode(); - } - } + private bool Equals(WrappedUserProfile other) => _user.Equals(other._user); + public override int GetHashCode() => _user.GetHashCode(); } } diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 5807a83abe..fcc12912cc 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -1,144 +1,141 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// Represents a Group for a Backoffice User +/// +[Serializable] +[DataContract(IsReference = true)] +public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup { + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> StringEnumerableComparer = + new( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode()); + + private readonly IShortStringHelper _shortStringHelper; + private string _alias; + private string? _icon; + private string _name; + private IEnumerable? _permissions; + private List _sectionCollection; + private int? _startContentId; + private int? _startMediaId; + /// - /// Represents a Group for a Backoffice User + /// Constructor to create a new user group /// - [Serializable] - [DataContract(IsReference = true)] - public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup + public UserGroup(IShortStringHelper shortStringHelper) { - private readonly IShortStringHelper _shortStringHelper; - private int? _startContentId; - private int? _startMediaId; - private string _alias; - private string? _icon; - private string _name; - private IEnumerable? _permissions; - private List _sectionCollection; + _alias = string.Empty; + _name = string.Empty; + _shortStringHelper = shortStringHelper; + _sectionCollection = new List(); + } - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> StringEnumerableComparer = - new DelegateEqualityComparer>( - (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), - enum1 => enum1.GetHashCode()); + /// + /// Constructor to create an existing user group + /// + /// + /// + /// + /// + /// + /// + public UserGroup(IShortStringHelper shortStringHelper, int userCount, string? alias, string? name, IEnumerable permissions, string? icon) + : this(shortStringHelper) + { + UserCount = userCount; + _alias = alias ?? string.Empty; + _name = name ?? string.Empty; + _permissions = permissions; + _icon = icon; + } - /// - /// Constructor to create a new user group - /// - public UserGroup(IShortStringHelper shortStringHelper) + [DataMember] + public int? StartMediaId + { + get => _startMediaId; + set => SetPropertyValueAndDetectChanges(value, ref _startMediaId, nameof(StartMediaId)); + } + + [DataMember] + public int? StartContentId + { + get => _startContentId; + set => SetPropertyValueAndDetectChanges(value, ref _startContentId, nameof(StartContentId)); + } + + [DataMember] + public string? Icon + { + get => _icon; + set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); + } + + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges( + value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), ref _alias!, nameof(Alias)); + } + + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } + + /// + /// The set of default permissions for the user group + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more + /// flexible permissions structure in the future. + /// + [DataMember] + public IEnumerable? Permissions + { + get => _permissions; + set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), StringEnumerableComparer); + } + + public IEnumerable AllowedSections => _sectionCollection; + + public int UserCount { get; } + + public void RemoveAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias)) { - _alias = string.Empty; - _name = string.Empty; - _shortStringHelper = shortStringHelper; - _sectionCollection = new List(); - } - - /// - /// Constructor to create an existing user group - /// - /// - /// - /// - /// - /// - public UserGroup(IShortStringHelper shortStringHelper, int userCount, string? alias, string? name, IEnumerable permissions, string? icon) - : this(shortStringHelper) - { - UserCount = userCount; - _alias = alias ?? string.Empty; - _name = name ?? string.Empty; - _permissions = permissions; - _icon = icon; - } - - [DataMember] - public int? StartMediaId - { - get => _startMediaId; - set => SetPropertyValueAndDetectChanges(value, ref _startMediaId, nameof(StartMediaId)); - } - - [DataMember] - public int? StartContentId - { - get => _startContentId; - set => SetPropertyValueAndDetectChanges(value, ref _startContentId, nameof(StartContentId)); - } - - [DataMember] - public string? Icon - { - get => _icon; - set => SetPropertyValueAndDetectChanges(value, ref _icon, nameof(Icon)); - } - - [DataMember] - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), ref _alias!, nameof(Alias)); - } - - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } - - /// - /// The set of default permissions for the user group - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. - /// - [DataMember] - public IEnumerable? Permissions - { - get => _permissions; - set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), StringEnumerableComparer); - } - - public IEnumerable AllowedSections - { - get => _sectionCollection; - } - - public void RemoveAllowedSection(string sectionAlias) - { - if (_sectionCollection.Contains(sectionAlias)) - _sectionCollection.Remove(sectionAlias); - } - - public void AddAllowedSection(string sectionAlias) - { - if (_sectionCollection.Contains(sectionAlias) == false) - _sectionCollection.Add(sectionAlias); - } - - public void ClearAllowedSections() - { - _sectionCollection.Clear(); - } - - public int UserCount { get; } - - protected override void PerformDeepClone(object clone) - { - - base.PerformDeepClone(clone); - - var clonedEntity = (UserGroup)clone; - - //manually clone the start node props - clonedEntity._sectionCollection = new List(_sectionCollection); + _sectionCollection.Remove(sectionAlias); } } + + public void AddAllowedSection(string sectionAlias) + { + if (_sectionCollection.Contains(sectionAlias) == false) + { + _sectionCollection.Add(sectionAlias); + } + } + + public void ClearAllowedSections() => _sectionCollection.Clear(); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (UserGroup)clone; + + // manually clone the start node props + clonedEntity._sectionCollection = new List(_sectionCollection); + } } diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs index 84b165b81e..d71c7aa4ce 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs @@ -1,31 +1,30 @@ -using Umbraco.Cms.Core; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UserGroupExtensions { - public static class UserGroupExtensions + public static IReadOnlyUserGroup ToReadOnlyGroup(this IUserGroup group) { - public static IReadOnlyUserGroup ToReadOnlyGroup(this IUserGroup group) + // this will generally always be the case + if (group is IReadOnlyUserGroup readonlyGroup) { - //this will generally always be the case - var readonlyGroup = group as IReadOnlyUserGroup; - if (readonlyGroup != null) return readonlyGroup; - - //otherwise create one - return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedSections, group.Permissions); + return readonlyGroup; } - public static bool IsSystemUserGroup(this IUserGroup group) => - IsSystemUserGroup(group.Alias); - - public static bool IsSystemUserGroup(this IReadOnlyUserGroup group) => - IsSystemUserGroup(group.Alias); - - private static bool IsSystemUserGroup(this string? groupAlias) - { - return groupAlias == Constants.Security.AdminGroupAlias - || groupAlias == Constants.Security.SensitiveDataGroupAlias - || groupAlias == Constants.Security.TranslatorGroupAlias; - } + // otherwise create one + return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedSections, group.Permissions); } + + public static bool IsSystemUserGroup(this IUserGroup group) => + IsSystemUserGroup(group.Alias); + + public static bool IsSystemUserGroup(this IReadOnlyUserGroup group) => + IsSystemUserGroup(group.Alias); + + private static bool IsSystemUserGroup(this string? groupAlias) => + groupAlias == Constants.Security.AdminGroupAlias + || groupAlias == Constants.Security.SensitiveDataGroupAlias + || groupAlias == Constants.Security.TranslatorGroupAlias; } diff --git a/src/Umbraco.Core/Models/Membership/UserProfile.cs b/src/Umbraco.Core/Models/Membership/UserProfile.cs index aca757b317..51eb882a6b 100644 --- a/src/Umbraco.Core/Models/Membership/UserProfile.cs +++ b/src/Umbraco.Core/Models/Membership/UserProfile.cs @@ -1,46 +1,55 @@ -using System; +namespace Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Models.Membership +public class UserProfile : IProfile, IEquatable { - public class UserProfile : IProfile, IEquatable + public UserProfile(int id, string? name) { - public UserProfile(int id, string? name) - { - Id = id; - Name = name; - } - - public int Id { get; private set; } - public string? Name { get; private set; } - - public bool Equals(UserProfile? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id == other.Id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((UserProfile) obj); - } - - public override int GetHashCode() - { - return Id; - } - - public static bool operator ==(UserProfile left, UserProfile right) - { - return Equals(left, right); - } - - public static bool operator !=(UserProfile left, UserProfile right) - { - return Equals(left, right) == false; - } + Id = id; + Name = name; } + + public int Id { get; } + + public string? Name { get; } + + public static bool operator ==(UserProfile left, UserProfile right) => Equals(left, right); + + public static bool operator !=(UserProfile left, UserProfile right) => Equals(left, right) == false; + + public bool Equals(UserProfile? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((UserProfile)obj); + } + + public override int GetHashCode() => Id; } diff --git a/src/Umbraco.Core/Models/Membership/UserState.cs b/src/Umbraco.Core/Models/Membership/UserState.cs index 13d2077105..e59e4d25c8 100644 --- a/src/Umbraco.Core/Models/Membership/UserState.cs +++ b/src/Umbraco.Core/Models/Membership/UserState.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Models.Membership +namespace Umbraco.Cms.Core.Models.Membership; + +/// +/// The state of a user +/// +public enum UserState { - /// - /// The state of a user - /// - public enum UserState - { - All = -1, - Active = 0, - Disabled = 1, - LockedOut = 2, - Invited = 3, - Inactive = 4 - } + All = -1, + Active = 0, + Disabled = 1, + LockedOut = 2, + Invited = 3, + Inactive = 4, } diff --git a/src/Umbraco.Core/Models/MigrationEntry.cs b/src/Umbraco.Core/Models/MigrationEntry.cs index f62dc7eb60..ab1294b13e 100644 --- a/src/Umbraco.Core/Models/MigrationEntry.cs +++ b/src/Umbraco.Core/Models/MigrationEntry.cs @@ -1,36 +1,34 @@ -using System; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class MigrationEntry : EntityBase, IMigrationEntry { - public class MigrationEntry : EntityBase, IMigrationEntry + private string? _migrationName; + private SemVersion? _version; + + public MigrationEntry() { - public MigrationEntry() - { - } + } - public MigrationEntry(int id, DateTime createDate, string migrationName, SemVersion version) - { - Id = id; - CreateDate = createDate; - _migrationName = migrationName; - _version = version; - } + public MigrationEntry(int id, DateTime createDate, string migrationName, SemVersion version) + { + Id = id; + CreateDate = createDate; + _migrationName = migrationName; + _version = version; + } - private string? _migrationName; - private SemVersion? _version; + public string? MigrationName + { + get => _migrationName; + set => SetPropertyValueAndDetectChanges(value, ref _migrationName, nameof(MigrationName)); + } - public string? MigrationName - { - get => _migrationName; - set => SetPropertyValueAndDetectChanges(value, ref _migrationName, nameof(MigrationName)); - } - - public SemVersion? Version - { - get => _version; - set => SetPropertyValueAndDetectChanges(value, ref _version, nameof(Version)); - } + public SemVersion? Version + { + get => _version; + set => SetPropertyValueAndDetectChanges(value, ref _version, nameof(Version)); } } diff --git a/src/Umbraco.Core/Models/Notification.cs b/src/Umbraco.Core/Models/Notification.cs index 95091efe1f..31d17513a6 100644 --- a/src/Umbraco.Core/Models/Notification.cs +++ b/src/Umbraco.Core/Models/Notification.cs @@ -1,20 +1,20 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class Notification { - public class Notification + public Notification(int entityId, int userId, string action, Guid? entityType) { - public Notification(int entityId, int userId, string action, Guid? entityType) - { - EntityId = entityId; - UserId = userId; - Action = action; - EntityType = entityType; - } - - public int EntityId { get; private set; } - public int UserId { get; private set; } - public string Action { get; private set; } - public Guid? EntityType { get; private set; } + EntityId = entityId; + UserId = userId; + Action = action; + EntityType = entityType; } + + public int EntityId { get; } + + public int UserId { get; } + + public string Action { get; } + + public Guid? EntityType { get; } } diff --git a/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs b/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs index 5174ee636b..85e2cfdcd6 100644 --- a/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs +++ b/src/Umbraco.Core/Models/NotificationEmailBodyParams.cs @@ -1,32 +1,35 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class NotificationEmailBodyParams { - public class NotificationEmailBodyParams + public NotificationEmailBodyParams(string? recipientName, string? action, string? itemName, string itemId, string itemUrl, string? editedUser, string siteUrl, string summary) { - public NotificationEmailBodyParams(string? recipientName, string? action, string? itemName, string itemId, string itemUrl, string? editedUser, string siteUrl, string summary) - { - RecipientName = recipientName ?? throw new ArgumentNullException(nameof(recipientName)); - Action = action ?? throw new ArgumentNullException(nameof(action)); - ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); - ItemId = itemId ?? throw new ArgumentNullException(nameof(itemId)); - ItemUrl = itemUrl ?? throw new ArgumentNullException(nameof(itemUrl)); - Summary = summary ?? throw new ArgumentNullException(nameof(summary)); - EditedUser = editedUser ?? throw new ArgumentNullException(nameof(editedUser)); - SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); - } - - public string RecipientName { get; } - public string Action { get; } - public string ItemName { get; } - public string ItemId { get; } - public string ItemUrl { get; } - - /// - /// This will either be an HTML or text based summary depending on the email type being sent - /// - public string Summary { get; } - public string EditedUser { get; } - public string SiteUrl { get; } + RecipientName = recipientName ?? throw new ArgumentNullException(nameof(recipientName)); + Action = action ?? throw new ArgumentNullException(nameof(action)); + ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); + ItemId = itemId ?? throw new ArgumentNullException(nameof(itemId)); + ItemUrl = itemUrl ?? throw new ArgumentNullException(nameof(itemUrl)); + Summary = summary ?? throw new ArgumentNullException(nameof(summary)); + EditedUser = editedUser ?? throw new ArgumentNullException(nameof(editedUser)); + SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); } + + public string RecipientName { get; } + + public string Action { get; } + + public string ItemName { get; } + + public string ItemId { get; } + + public string ItemUrl { get; } + + /// + /// This will either be an HTML or text based summary depending on the email type being sent + /// + public string Summary { get; } + + public string EditedUser { get; } + + public string SiteUrl { get; } } diff --git a/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs b/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs index c644f7c1a6..51b1e4031e 100644 --- a/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs +++ b/src/Umbraco.Core/Models/NotificationEmailSubjectParams.cs @@ -1,19 +1,17 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class NotificationEmailSubjectParams { - - public class NotificationEmailSubjectParams + public NotificationEmailSubjectParams(string siteUrl, string? action, string? itemName) { - public NotificationEmailSubjectParams(string siteUrl, string? action, string? itemName) - { - SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); - Action = action ?? throw new ArgumentNullException(nameof(action)); - ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); - } - - public string SiteUrl { get; } - public string Action { get; } - public string ItemName { get; } + SiteUrl = siteUrl ?? throw new ArgumentNullException(nameof(siteUrl)); + Action = action ?? throw new ArgumentNullException(nameof(action)); + ItemName = itemName ?? throw new ArgumentNullException(nameof(itemName)); } + + public string SiteUrl { get; } + + public string Action { get; } + + public string ItemName { get; } } diff --git a/src/Umbraco.Core/Models/ObjectTypes.cs b/src/Umbraco.Core/Models/ObjectTypes.cs index 8e4eef3246..0f44a269cc 100644 --- a/src/Umbraco.Core/Models/ObjectTypes.cs +++ b/src/Umbraco.Core/Models/ObjectTypes.cs @@ -1,163 +1,153 @@ -using System; using System.Collections.Concurrent; using System.Reflection; using Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.Models -{ - /// - /// Provides utilities and extension methods to handle object types. - /// - public static class ObjectTypes - { - // must be concurrent to avoid thread collisions! - private static readonly ConcurrentDictionary UmbracoGuids = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary UmbracoUdiTypes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary UmbracoFriendlyNames = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary UmbracoTypes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary GuidUdiTypes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary GuidObjectTypes = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary GuidTypes = new ConcurrentDictionary(); +namespace Umbraco.Cms.Core.Models; - private static FieldInfo? GetEnumField(string name) +/// +/// Provides utilities and extension methods to handle object types. +/// +public static class ObjectTypes +{ + // must be concurrent to avoid thread collisions! + private static readonly ConcurrentDictionary UmbracoGuids = new(); + private static readonly ConcurrentDictionary UmbracoUdiTypes = new(); + private static readonly ConcurrentDictionary UmbracoFriendlyNames = new(); + private static readonly ConcurrentDictionary UmbracoTypes = new(); + private static readonly ConcurrentDictionary GuidUdiTypes = new(); + private static readonly ConcurrentDictionary GuidObjectTypes = new(); + private static readonly ConcurrentDictionary GuidTypes = new(); + + /// + /// Gets the Umbraco object type corresponding to a name. + /// + public static UmbracoObjectTypes GetUmbracoObjectType(string name) => + (UmbracoObjectTypes)Enum.Parse(typeof(UmbracoObjectTypes), name, true); + + private static FieldInfo? GetEnumField(string name) => + typeof(UmbracoObjectTypes).GetField(name, BindingFlags.Public | BindingFlags.Static); + + private static FieldInfo? GetEnumField(Guid guid) + { + FieldInfo[] fields = typeof(UmbracoObjectTypes).GetFields(BindingFlags.Public | BindingFlags.Static); + foreach (FieldInfo field in fields) { - return typeof (UmbracoObjectTypes).GetField(name, BindingFlags.Public | BindingFlags.Static); + UmbracoObjectTypeAttribute? attribute = field.GetCustomAttribute(false); + if (attribute != null && attribute.ObjectId == guid) + { + return field; + } } - private static FieldInfo? GetEnumField(Guid guid) + return null; + } + + #region Guid object type utilities + + /// + /// Gets the Umbraco object type corresponding to an object type Guid. + /// + public static UmbracoObjectTypes GetUmbracoObjectType(Guid objectType) => + GuidObjectTypes.GetOrAdd(objectType, t => { - var fields = typeof (UmbracoObjectTypes).GetFields(BindingFlags.Public | BindingFlags.Static); - foreach (var field in fields) + FieldInfo? field = GetEnumField(objectType); + if (field == null) { - var attribute = field.GetCustomAttribute(false); - if (attribute != null && attribute.ObjectId == guid) return field; + return UmbracoObjectTypes.Unknown; } - return null; - } + return (UmbracoObjectTypes?)field.GetValue(null) ?? UmbracoObjectTypes.Unknown; + }); - /// - /// Gets the Umbraco object type corresponding to a name. - /// - public static UmbracoObjectTypes GetUmbracoObjectType(string name) + /// + /// Gets the Udi type corresponding to an object type Guid. + /// + public static string GetUdiType(Guid objectType) => + GuidUdiTypes.GetOrAdd(objectType, t => { - return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, true); - } - - #region Guid object type utilities - - /// - /// Gets the Umbraco object type corresponding to an object type Guid. - /// - public static UmbracoObjectTypes GetUmbracoObjectType(Guid objectType) - { - return GuidObjectTypes.GetOrAdd(objectType, t => + FieldInfo? field = GetEnumField(objectType); + if (field == null) { - var field = GetEnumField(objectType); - if (field == null) return UmbracoObjectTypes.Unknown; + return Constants.UdiEntityType.Unknown; + } - return (UmbracoObjectTypes?)field.GetValue(null) ?? UmbracoObjectTypes.Unknown; - }); - } + UmbracoUdiTypeAttribute? attribute = field.GetCustomAttribute(false); + return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; + }); - /// - /// Gets the Udi type corresponding to an object type Guid. - /// - public static string GetUdiType(Guid objectType) + /// + /// Gets the CLR type corresponding to an object type Guid. + /// + public static Type? GetClrType(Guid objectType) => + GuidTypes.GetOrAdd(objectType, t => { - return GuidUdiTypes.GetOrAdd(objectType, t => + FieldInfo? field = GetEnumField(objectType); + if (field == null) { - var field = GetEnumField(objectType); - if (field == null) return Constants.UdiEntityType.Unknown; + return null; + } - var attribute = field.GetCustomAttribute(false); - return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; - }); - } + UmbracoObjectTypeAttribute? attribute = field.GetCustomAttribute(false); + return attribute?.ModelType; + }); - /// - /// Gets the CLR type corresponding to an object type Guid. - /// - public static Type? GetClrType(Guid objectType) + #endregion + + #region UmbracoObjectTypes extension methods + + /// + /// Gets the object type Guid corresponding to this Umbraco object type. + /// + public static Guid GetGuid(this UmbracoObjectTypes objectType) => + UmbracoGuids.GetOrAdd(objectType, t => { - return GuidTypes.GetOrAdd(objectType, t => - { - var field = GetEnumField(objectType); - if (field == null) return null; + FieldInfo? field = GetEnumField(t.ToString()); + UmbracoObjectTypeAttribute? attribute = field?.GetCustomAttribute(false); - var attribute = field.GetCustomAttribute(false); - return attribute?.ModelType; - }); - } + return attribute?.ObjectId ?? Guid.Empty; + }); - #endregion - - #region UmbracoObjectTypes extension methods - - /// - /// Gets the object type Guid corresponding to this Umbraco object type. - /// - public static Guid GetGuid(this UmbracoObjectTypes objectType) + /// + /// Gets the Udi type corresponding to this Umbraco object type. + /// + public static string GetUdiType(this UmbracoObjectTypes objectType) => + UmbracoUdiTypes.GetOrAdd(objectType, t => { - return UmbracoGuids.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); + FieldInfo? field = GetEnumField(t.ToString()); + UmbracoUdiTypeAttribute? attribute = field?.GetCustomAttribute(false); - return attribute?.ObjectId ?? Guid.Empty; - }); - } + return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; + }); - /// - /// Gets the Udi type corresponding to this Umbraco object type. - /// - public static string GetUdiType(this UmbracoObjectTypes objectType) + /// + /// Gets the name corresponding to this Umbraco object type. + /// + public static string? GetName(this UmbracoObjectTypes objectType) => + Enum.GetName(typeof(UmbracoObjectTypes), objectType); + + /// + /// Gets the friendly name corresponding to this Umbraco object type. + /// + public static string GetFriendlyName(this UmbracoObjectTypes objectType) => + UmbracoFriendlyNames.GetOrAdd(objectType, t => { - return UmbracoUdiTypes.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); + FieldInfo? field = GetEnumField(t.ToString()); + FriendlyNameAttribute? attribute = field?.GetCustomAttribute(false); - return attribute?.UdiType ?? Constants.UdiEntityType.Unknown; - }); - } + return attribute?.ToString() ?? string.Empty; + }); - /// - /// Gets the name corresponding to this Umbraco object type. - /// - public static string? GetName(this UmbracoObjectTypes objectType) + /// + /// Gets the CLR type corresponding to this Umbraco object type. + /// + public static Type? GetClrType(this UmbracoObjectTypes objectType) => + UmbracoTypes.GetOrAdd(objectType, t => { - return Enum.GetName(typeof (UmbracoObjectTypes), objectType); - } + FieldInfo? field = GetEnumField(t.ToString()); + UmbracoObjectTypeAttribute? attribute = field?.GetCustomAttribute(false); - /// - /// Gets the friendly name corresponding to this Umbraco object type. - /// - public static string GetFriendlyName(this UmbracoObjectTypes objectType) - { - return UmbracoFriendlyNames.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); + return attribute?.ModelType; + }); - return attribute?.ToString() ?? string.Empty; - }); - } - - /// - /// Gets the CLR type corresponding to this Umbraco object type. - /// - public static Type? GetClrType(this UmbracoObjectTypes objectType) - { - return UmbracoTypes.GetOrAdd(objectType, t => - { - var field = GetEnumField(t.ToString()); - var attribute = field?.GetCustomAttribute(false); - - return attribute?.ModelType; - }); - } - - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs b/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs index e6c430627c..6119d2cea1 100644 --- a/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs +++ b/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs @@ -1,30 +1,41 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Xml.Linq; -namespace Umbraco.Cms.Core.Models.Packaging +namespace Umbraco.Cms.Core.Models.Packaging; + +/// +/// The model of the umbraco package data manifest (xml file) +/// +public class CompiledPackage { - /// - /// The model of the umbraco package data manifest (xml file) - /// - public class CompiledPackage - { - public FileInfo? PackageFile { get; set; } - public string Name { get; set; } = null!; - public InstallWarnings Warnings { get; set; } = new InstallWarnings(); - public IEnumerable Macros { get; set; } = null!; // TODO: make strongly typed - public IEnumerable MacroPartialViews { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Templates { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Stylesheets { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Scripts { get; set; } = null!; // TODO: make strongly typed - public IEnumerable PartialViews { get; set; } = null!; // TODO: make strongly typed - public IEnumerable DataTypes { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Languages { get; set; } = null!; // TODO: make strongly typed - public IEnumerable DictionaryItems { get; set; } = null!; // TODO: make strongly typed - public IEnumerable DocumentTypes { get; set; } = null!; // TODO: make strongly typed - public IEnumerable MediaTypes { get; set; } = null!; // TODO: make strongly typed - public IEnumerable Documents { get; set; } = null!; - public IEnumerable Media { get; set; } = null!; - } + public FileInfo? PackageFile { get; set; } + + public string Name { get; set; } = null!; + + public InstallWarnings Warnings { get; set; } = new(); + + public IEnumerable Macros { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable MacroPartialViews { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Templates { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Stylesheets { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Scripts { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable PartialViews { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable DataTypes { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Languages { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable DictionaryItems { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable DocumentTypes { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable MediaTypes { get; set; } = null!; // TODO: make strongly typed + + public IEnumerable Documents { get; set; } = null!; + + public IEnumerable Media { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/Packaging/CompiledPackageContentBase.cs b/src/Umbraco.Core/Models/Packaging/CompiledPackageContentBase.cs index 0fb1c60908..794262406a 100644 --- a/src/Umbraco.Core/Models/Packaging/CompiledPackageContentBase.cs +++ b/src/Umbraco.Core/Models/Packaging/CompiledPackageContentBase.cs @@ -1,25 +1,20 @@ using System.Xml.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Packaging +namespace Umbraco.Cms.Core.Models.Packaging; + +/// +/// Compiled representation of a content base (Document or Media) +/// +public class CompiledPackageContentBase { + public string? ImportMode { get; set; } // this is never used + /// - /// Compiled representation of a content base (Document or Media) + /// The serialized version of the content /// - public class CompiledPackageContentBase - { - public static CompiledPackageContentBase Create(XElement xml) => - new CompiledPackageContentBase - { - XmlData = xml, - ImportMode = xml.AttributeValue("importMode") - }; + public XElement XmlData { get; set; } = null!; - public string? ImportMode { get; set; } //this is never used - - /// - /// The serialized version of the content - /// - public XElement XmlData { get; set; } = null!; - } + public static CompiledPackageContentBase Create(XElement xml) => + new() { XmlData = xml, ImportMode = xml.AttributeValue("importMode") }; } diff --git a/src/Umbraco.Core/Models/Packaging/InstallWarnings.cs b/src/Umbraco.Core/Models/Packaging/InstallWarnings.cs index 7cad9b5b9a..d1154f1b30 100644 --- a/src/Umbraco.Core/Models/Packaging/InstallWarnings.cs +++ b/src/Umbraco.Core/Models/Packaging/InstallWarnings.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Models.Packaging; -namespace Umbraco.Cms.Core.Models.Packaging +public class InstallWarnings { + // TODO: Shouldn't we detect other conflicting entities too ? + public IEnumerable? ConflictingMacros { get; set; } = Enumerable.Empty(); - public class InstallWarnings - { - // TODO: Shouldn't we detect other conflicting entities too ? - public IEnumerable? ConflictingMacros { get; set; } = Enumerable.Empty(); - public IEnumerable? ConflictingTemplates { get; set; } = Enumerable.Empty(); - public IEnumerable? ConflictingStylesheets { get; set; } = Enumerable.Empty(); - } + public IEnumerable? ConflictingTemplates { get; set; } = Enumerable.Empty(); + + public IEnumerable? ConflictingStylesheets { get; set; } = Enumerable.Empty(); } diff --git a/src/Umbraco.Core/Models/PagedResult.cs b/src/Umbraco.Core/Models/PagedResult.cs index f15768cc2d..6dbe6dd703 100644 --- a/src/Umbraco.Core/Models/PagedResult.cs +++ b/src/Umbraco.Core/Models/PagedResult.cs @@ -1,56 +1,55 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a paged result for a model collection +/// +/// +[DataContract(Name = "pagedCollection", Namespace = "")] +public abstract class PagedResult { - /// - /// Represents a paged result for a model collection - /// - /// - [DataContract(Name = "pagedCollection", Namespace = "")] - public abstract class PagedResult + public PagedResult(long totalItems, long pageNumber, long pageSize) { - public PagedResult(long totalItems, long pageNumber, long pageSize) - { - TotalItems = totalItems; - PageNumber = pageNumber; - PageSize = pageSize; + TotalItems = totalItems; + PageNumber = pageNumber; + PageSize = pageSize; - if (pageSize > 0) - { - TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); - } - else - { - TotalPages = 1; - } + if (pageSize > 0) + { + TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); } - - [DataMember(Name = "pageNumber")] - public long PageNumber { get; private set; } - - [DataMember(Name = "pageSize")] - public long PageSize { get; private set; } - - [DataMember(Name = "totalPages")] - public long TotalPages { get; private set; } - - [DataMember(Name = "totalItems")] - public long TotalItems { get; private set; } - - /// - /// Calculates the skip size based on the paged parameters specified - /// - /// - /// Returns 0 if the page number or page size is zero - /// - public int GetSkipSize() + else { - if (PageNumber > 0 && PageSize > 0) - { - return Convert.ToInt32((PageNumber - 1) * PageSize); - } - return 0; + TotalPages = 1; } } + + [DataMember(Name = "pageNumber")] + public long PageNumber { get; private set; } + + [DataMember(Name = "pageSize")] + public long PageSize { get; private set; } + + [DataMember(Name = "totalPages")] + public long TotalPages { get; private set; } + + [DataMember(Name = "totalItems")] + public long TotalItems { get; private set; } + + /// + /// Calculates the skip size based on the paged parameters specified + /// + /// + /// Returns 0 if the page number or page size is zero + /// + public int GetSkipSize() + { + if (PageNumber > 0 && PageSize > 0) + { + return Convert.ToInt32((PageNumber - 1) * PageSize); + } + + return 0; + } } diff --git a/src/Umbraco.Core/Models/PagedResultOfT.cs b/src/Umbraco.Core/Models/PagedResultOfT.cs index 125256ec3b..c2d11a4f82 100644 --- a/src/Umbraco.Core/Models/PagedResultOfT.cs +++ b/src/Umbraco.Core/Models/PagedResultOfT.cs @@ -1,20 +1,19 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models -{ - /// - /// Represents a paged result for a model collection - /// - /// - [DataContract(Name = "pagedCollection", Namespace = "")] - public class PagedResult : PagedResult - { - public PagedResult(long totalItems, long pageNumber, long pageSize) - : base(totalItems, pageNumber, pageSize) - { } +namespace Umbraco.Cms.Core.Models; - [DataMember(Name = "items")] - public IEnumerable? Items { get; set; } +/// +/// Represents a paged result for a model collection +/// +/// +[DataContract(Name = "pagedCollection", Namespace = "")] +public class PagedResult : PagedResult +{ + public PagedResult(long totalItems, long pageNumber, long pageSize) + : base(totalItems, pageNumber, pageSize) + { } + + [DataMember(Name = "items")] + public IEnumerable? Items { get; set; } } diff --git a/src/Umbraco.Core/Models/PartialView.cs b/src/Umbraco.Core/Models/PartialView.cs index ffa9412c51..2900674570 100644 --- a/src/Umbraco.Core/Models/PartialView.cs +++ b/src/Umbraco.Core/Models/PartialView.cs @@ -1,25 +1,22 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Partial View file +/// +[Serializable] +[DataContract(IsReference = true)] +public class PartialView : File, IPartialView { - /// - /// Represents a Partial View file - /// - [Serializable] - [DataContract(IsReference = true)] - public class PartialView : File, IPartialView + public PartialView(PartialViewType viewType, string path) + : this(viewType, path, null) { - public PartialView(PartialViewType viewType, string path) - : this(viewType, path, null) - { } - - public PartialView(PartialViewType viewType, string path, Func? getFileContent) - : base(path, getFileContent) - { - ViewType = viewType; - } - - public PartialViewType ViewType { get; set; } } + + public PartialView(PartialViewType viewType, string path, Func? getFileContent) + : base(path, getFileContent) => + ViewType = viewType; + + public PartialViewType ViewType { get; set; } } diff --git a/src/Umbraco.Core/Models/PartialViewMacroModel.cs b/src/Umbraco.Core/Models/PartialViewMacroModel.cs index 662894b39f..0d999d5dd6 100644 --- a/src/Umbraco.Core/Models/PartialViewMacroModel.cs +++ b/src/Umbraco.Core/Models/PartialViewMacroModel.cs @@ -1,31 +1,33 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The model used when rendering Partial View Macros +/// +public class PartialViewMacroModel : IContentModel { - /// - /// The model used when rendering Partial View Macros - /// - public class PartialViewMacroModel : IContentModel + public PartialViewMacroModel( + IPublishedContent page, + int macroId, + string? macroAlias, + string? macroName, + IDictionary macroParams) { - - public PartialViewMacroModel(IPublishedContent page, - int macroId, - string? macroAlias, - string? macroName, - IDictionary macroParams) - { - Content = page; - MacroParameters = macroParams; - MacroName = macroName; - MacroAlias = macroAlias; - MacroId = macroId; - } - - public IPublishedContent Content { get; } - public string? MacroName { get; } - public string? MacroAlias { get; } - public int MacroId { get; } - public IDictionary MacroParameters { get; } + Content = page; + MacroParameters = macroParams; + MacroName = macroName; + MacroAlias = macroAlias; + MacroId = macroId; } + + public string? MacroName { get; } + + public string? MacroAlias { get; } + + public int MacroId { get; } + + public IDictionary MacroParameters { get; } + + public IPublishedContent Content { get; } } diff --git a/src/Umbraco.Core/Models/PartialViewMacroModelExtensions.cs b/src/Umbraco.Core/Models/PartialViewMacroModelExtensions.cs index aea801719f..ecbf22323b 100644 --- a/src/Umbraco.Core/Models/PartialViewMacroModelExtensions.cs +++ b/src/Umbraco.Core/Models/PartialViewMacroModelExtensions.cs @@ -1,38 +1,39 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for the PartialViewMacroModel object +/// +public static class PartialViewMacroModelExtensions { /// - /// Extension methods for the PartialViewMacroModel object + /// Attempt to get a Macro parameter from a PartialViewMacroModel and return a default value otherwise /// - public static class PartialViewMacroModelExtensions + /// + /// + /// + /// Parameter value if available, the default value that was passed otherwise. + public static T? GetParameterValue(this PartialViewMacroModel partialViewMacroModel, string parameterAlias, T defaultValue) { - /// - /// Attempt to get a Macro parameter from a PartialViewMacroModel and return a default value otherwise - /// - /// - /// - /// - /// Parameter value if available, the default value that was passed otherwise. - public static T? GetParameterValue(this PartialViewMacroModel partialViewMacroModel, string parameterAlias, T defaultValue) + if (partialViewMacroModel.MacroParameters.ContainsKey(parameterAlias) == false || + string.IsNullOrEmpty(partialViewMacroModel.MacroParameters[parameterAlias]?.ToString())) { - if (partialViewMacroModel.MacroParameters.ContainsKey(parameterAlias) == false || string.IsNullOrEmpty(partialViewMacroModel.MacroParameters[parameterAlias]?.ToString())) - return defaultValue; - - var attempt = partialViewMacroModel.MacroParameters[parameterAlias].TryConvertTo(typeof(T)); - - return attempt.Success ? (T?) attempt.Result : defaultValue; + return defaultValue; } - /// - /// Attempt to get a Macro parameter from a PartialViewMacroModel - /// - /// - /// - /// Parameter value if available, the default value for the type otherwise. - public static T? GetParameterValue(this PartialViewMacroModel partialViewMacroModel, string parameterAlias) - { - return partialViewMacroModel.GetParameterValue(parameterAlias, default(T)); - } + Attempt attempt = partialViewMacroModel.MacroParameters[parameterAlias].TryConvertTo(typeof(T)); + + return attempt.Success ? (T?)attempt.Result : defaultValue; } + + /// + /// Attempt to get a Macro parameter from a PartialViewMacroModel + /// + /// + /// + /// Parameter value if available, the default value for the type otherwise. + public static T? GetParameterValue(this PartialViewMacroModel partialViewMacroModel, string parameterAlias) => + partialViewMacroModel.GetParameterValue(parameterAlias, default(T)); } diff --git a/src/Umbraco.Core/Models/PartialViewType.cs b/src/Umbraco.Core/Models/PartialViewType.cs index 5dc6dbc59c..65499be9a2 100644 --- a/src/Umbraco.Core/Models/PartialViewType.cs +++ b/src/Umbraco.Core/Models/PartialViewType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public enum PartialViewType : byte { - public enum PartialViewType : byte - { - Unknown = 0, // default - PartialView = 1, - PartialViewMacro = 2 - } + Unknown = 0, // default + PartialView = 1, + PartialViewMacro = 2, } diff --git a/src/Umbraco.Core/Models/PasswordChangedModel.cs b/src/Umbraco.Core/Models/PasswordChangedModel.cs index 231940f105..0cd405e604 100644 --- a/src/Umbraco.Core/Models/PasswordChangedModel.cs +++ b/src/Umbraco.Core/Models/PasswordChangedModel.cs @@ -1,20 +1,19 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing an attempt at changing a password +/// +public class PasswordChangedModel { /// - /// A model representing an attempt at changing a password + /// The error affiliated with the failing password changes, null if changing was successful /// - public class PasswordChangedModel - { - /// - /// The error affiliated with the failing password changes, null if changing was successful - /// - public ValidationResult? ChangeError { get; set; } + public ValidationResult? ChangeError { get; set; } - /// - /// If the password was reset, this is the value it has been changed to - /// - public string? ResetPassword { get; set; } - } + /// + /// If the password was reset, this is the value it has been changed to + /// + public string? ResetPassword { get; set; } } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index f4bba10c2c..195772be3a 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -1,557 +1,658 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a property. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Property : EntityBase, IProperty { + private static readonly DelegateEqualityComparer PropertyValueComparer = new( + (o, o1) => + { + if (o == null && o1 == null) + { + return true; + } + + // custom comparer for strings. + // if one is null and another is empty then they are the same + if (o is string || o1 is string) + { + return ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) || + (o != null && o1 != null && o.Equals(o1)); + } + + if (o == null || o1 == null) + { + return false; + } + + // custom comparer for enumerable + // ReSharper disable once MergeCastWithTypeCheck + if (o is IEnumerable && o1 is IEnumerable enumerable) + { + return ((IEnumerable)o).Cast().UnsortedSequenceEqual(enumerable.Cast()); + } + + return o.Equals(o1); + }, + o => o!.GetHashCode()); + + // _pvalue contains the invariant-neutral property value + private IPropertyValue? _pvalue; + + // _values contains all property values, including the invariant-neutral value + private List _values = new(); + + // _vvalues contains the (indexed) variant property values + private Dictionary? _vvalues; + /// - /// Represents a property. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class Property : EntityBase, IProperty + public Property(IPropertyType propertyType) => PropertyType = propertyType; + + /// + /// Initializes a new instance of the class. + /// + public Property(int id, IPropertyType propertyType) { - // _values contains all property values, including the invariant-neutral value - private List _values = new List(); + Id = id; + PropertyType = propertyType; + } - // _pvalue contains the invariant-neutral property value - private IPropertyValue? _pvalue; + /// + /// Returns the PropertyType, which this Property is based on + /// + [IgnoreDataMember] + public IPropertyType PropertyType { get; private set; } - // _vvalues contains the (indexed) variant property values - private Dictionary? _vvalues; - - /// - /// Initializes a new instance of the class. - /// - public Property(IPropertyType propertyType) + /// + /// Gets the list of values. + /// + [DataMember] + public IReadOnlyCollection Values + { + get => _values; + set { - PropertyType = propertyType; - } - - /// - /// Initializes a new instance of the class. - /// - public Property(int id, IPropertyType propertyType) - { - Id = id; - PropertyType = propertyType; - } - - /// - /// Creates a new instance for existing - /// - /// - /// - /// - /// Generally will contain a published and an unpublished property values - /// - /// - public static Property CreateWithValues(int id, IPropertyType propertyType, params InitialPropertyValue[] values) - { - var property = new Property(propertyType); - try - { - property.DisableChangeTracking(); - property.Id = id; - foreach(var value in values) - { - property.FactorySetValue(value.Culture, value.Segment, value.Published, value.Value); - } - property.ResetDirtyProperties(false); - return property; - } - finally - { - property.EnableChangeTracking(); - } - } - - /// - /// Used for constructing a new instance - /// - public class InitialPropertyValue - { - public InitialPropertyValue(string? culture, string? segment, bool published, object? value) - { - Culture = culture; - Segment = segment; - Published = published; - Value = value; - } - - public string? Culture { get; } - public string? Segment { get; } - public bool Published { get; } - public object? Value { get; } - } - - /// - /// Represents a property value. - /// - public class PropertyValue : IPropertyValue, IDeepCloneable, IEquatable - { - // TODO: Either we allow change tracking at this class level, or we add some special change tracking collections to the Property - // class to deal with change tracking which variants have changed - - private string? _culture; - private string? _segment; - - /// - /// Gets or sets the culture of the property. - /// - /// The culture is either null (invariant) or a non-empty string. If the property is - /// set with an empty or whitespace value, its value is converted to null. - public string? Culture - { - get => _culture; - set => _culture = value.IsNullOrWhiteSpace() ? null : value!.ToLowerInvariant(); - } - - /// - /// Gets or sets the segment of the property. - /// - /// The segment is either null (neutral) or a non-empty string. If the property is - /// set with an empty or whitespace value, its value is converted to null. - public string? Segment - { - get => _segment; - set => _segment = value?.ToLowerInvariant(); - } - - /// - /// Gets or sets the edited value of the property. - /// - public object? EditedValue { get; set; } - - /// - /// Gets or sets the published value of the property. - /// - public object? PublishedValue { get; set; } - - /// - /// Clones the property value. - /// - public IPropertyValue Clone() - => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; - - public object DeepClone() => Clone(); - - public override bool Equals(object? obj) - { - return Equals(obj as PropertyValue); - } - - public bool Equals(PropertyValue? other) - { - return other != null && - _culture == other._culture && - _segment == other._segment && - EqualityComparer.Default.Equals(EditedValue, other.EditedValue) && - EqualityComparer.Default.Equals(PublishedValue, other.PublishedValue); - } - - public override int GetHashCode() - { - var hashCode = 1885328050; - if (_culture is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_culture); - } - - if (_segment is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(_segment); - } - - if (EditedValue is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(EditedValue); - } - - if (PublishedValue is not null) - { - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(PublishedValue); - } - return hashCode; - } - } - - private static readonly DelegateEqualityComparer PropertyValueComparer = new DelegateEqualityComparer( - (o, o1) => - { - if (o == null && o1 == null) return true; - - // custom comparer for strings. - // if one is null and another is empty then they are the same - if (o is string || o1 is string) - return ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) || (o != null && o1 != null && o.Equals(o1)); - - if (o == null || o1 == null) return false; - - // custom comparer for enumerable - // ReSharper disable once MergeCastWithTypeCheck - if (o is IEnumerable && o1 is IEnumerable enumerable) - return ((IEnumerable)o).Cast().UnsortedSequenceEqual(enumerable.Cast()); - - return o.Equals(o1); - }, o => o!.GetHashCode()); - - /// - /// Returns the PropertyType, which this Property is based on - /// - [IgnoreDataMember] - public IPropertyType PropertyType { get; private set; } - - /// - /// Gets the list of values. - /// - [DataMember] - public IReadOnlyCollection Values - { - get => _values; - set - { - // make sure we filter out invalid variations - // make sure we leave _vvalues null if possible - _values = value.Where(x => PropertyType?.SupportsVariation(x.Culture, x.Segment) ?? false).ToList(); - _pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null); - _vvalues = _values.Count > (_pvalue == null ? 0 : 1) - ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeNStringNStringKey(x.Culture, x.Segment), x => x) - : null; - } - } - - /// - /// Returns the Alias of the PropertyType, which this Property is based on - /// - [DataMember] - public string Alias => PropertyType.Alias; - - /// - /// Returns the Id of the PropertyType, which this Property is based on - /// - [IgnoreDataMember] - public int PropertyTypeId => PropertyType.Id; - - /// - /// Returns the DatabaseType that the underlaying DataType is using to store its values - /// - /// - /// Only used internally when saving the property value. - /// - [IgnoreDataMember] - public ValueStorageType ValueStorageType => PropertyType.ValueStorageType; - - /// - /// Gets the value. - /// - public object? GetValue(string? culture = null, string? segment = null, bool published = false) - { - // ensure null or whitespace are nulls - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - if (!PropertyType.SupportsVariation(culture, segment)) return null; - if (culture == null && segment == null) return GetPropertyValue(_pvalue, published); - if (_vvalues == null) return null; - return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out var pvalue) - ? GetPropertyValue(pvalue, published) + // make sure we filter out invalid variations + // make sure we leave _vvalues null if possible + _values = value.Where(x => PropertyType?.SupportsVariation(x.Culture, x.Segment) ?? false).ToList(); + _pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null); + _vvalues = _values.Count > (_pvalue == null ? 0 : 1) + ? _values.Where(x => x != _pvalue) + .ToDictionary(x => new CompositeNStringNStringKey(x.Culture, x.Segment), x => x) : null; } + } - private object? GetPropertyValue(IPropertyValue? pvalue, bool published) + /// + /// Returns the Alias of the PropertyType, which this Property is based on + /// + [DataMember] + public string Alias => PropertyType.Alias; + + /// + /// Returns the Id of the PropertyType, which this Property is based on + /// + [IgnoreDataMember] + public int PropertyTypeId => PropertyType.Id; + + /// + /// Returns the DatabaseType that the underlaying DataType is using to store its values + /// + /// + /// Only used internally when saving the property value. + /// + [IgnoreDataMember] + public ValueStorageType ValueStorageType => PropertyType.ValueStorageType; + + /// + /// Creates a new instance for existing + /// + /// + /// + /// + /// Generally will contain a published and an unpublished property values + /// + /// + public static Property CreateWithValues(int id, IPropertyType propertyType, params InitialPropertyValue[] values) + { + var property = new Property(propertyType); + try { - if (pvalue == null) return null; - - return PropertyType.SupportsPublishing - ? (published ? pvalue.PublishedValue : pvalue.EditedValue) - : pvalue.EditedValue; - } - - // internal - must be invoked by the content item - // does *not* validate the value - content item must validate first - public void PublishValues(string? culture = "*", string? segment = "*") - { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - // if invariant or all, and invariant-neutral is supported, publish invariant-neutral - if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) - PublishValue(_pvalue); - - // then deal with everything that varies - if (_vvalues == null) return; - - // get the property values that are still relevant (wrt the property type variation), - // and match the specified culture and segment (or anything when '*'). - var pvalues = _vvalues.Where(x => - PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok - (culture == "*" || (x.Value.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches - (segment == "*" || (x.Value.Segment?.InvariantEquals(segment) ?? false))) // the segment matches - .Select(x => x.Value); - - foreach (var pvalue in pvalues) - PublishValue(pvalue); - } - - // internal - must be invoked by the content item - public void UnpublishValues(string? culture = "*", string? segment = "*") - { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - // if invariant or all, and invariant-neutral is supported, publish invariant-neutral - if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) - UnpublishValue(_pvalue); - - // then deal with everything that varies - if (_vvalues == null) return; - - // get the property values that are still relevant (wrt the property type variation), - // and match the specified culture and segment (or anything when '*'). - var pvalues = _vvalues.Where(x => - PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok - (culture == "*" || (x.Value.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches - (segment == "*" || (x.Value.Segment?.InvariantEquals(segment) ?? false))) // the segment matches - .Select(x => x.Value); - - foreach (var pvalue in pvalues) - UnpublishValue(pvalue); - } - - private void PublishValue(IPropertyValue? pvalue) - { - if (pvalue == null) return; - - if (!PropertyType.SupportsPublishing) - throw new NotSupportedException("Property type does not support publishing."); - var origValue = pvalue.PublishedValue; - pvalue.PublishedValue = ConvertAssignedValue(pvalue.EditedValue); - DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); - } - - private void UnpublishValue(IPropertyValue? pvalue) - { - if (pvalue == null) return; - - if (!PropertyType.SupportsPublishing) - throw new NotSupportedException("Property type does not support publishing."); - var origValue = pvalue.PublishedValue; - pvalue.PublishedValue = ConvertAssignedValue(null); - DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); - } - - /// - /// Sets a value. - /// - public void SetValue(object? value, string? culture = null, string? segment = null) - { - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - if (!PropertyType.SupportsVariation(culture, segment)) - throw new NotSupportedException($"Variation \"{culture??""},{segment??""}\" is not supported by the property type."); - - var (pvalue, change) = GetPValue(culture, segment, true); - - if (pvalue is not null) + property.DisableChangeTracking(); + property.Id = id; + foreach (InitialPropertyValue value in values) { - var origValue = pvalue.EditedValue; - var setValue = ConvertAssignedValue(value); + property.FactorySetValue(value.Culture, value.Segment, value.Published, value.Value); + } - pvalue.EditedValue = setValue; + property.ResetDirtyProperties(false); + return property; + } + finally + { + property.EnableChangeTracking(); + } + } - DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); + /// + /// Gets the value. + /// + public object? GetValue(string? culture = null, string? segment = null, bool published = false) + { + // ensure null or whitespace are nulls + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) + { + return null; + } + + if (culture == null && segment == null) + { + return GetPropertyValue(_pvalue, published); + } + + if (_vvalues == null) + { + return null; + } + + return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out IPropertyValue? pvalue) + ? GetPropertyValue(pvalue, published) + : null; + } + + // internal - must be invoked by the content item + // does *not* validate the value - content item must validate first + public void PublishValues(string? culture = "*", string? segment = "*") + { + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + PropertyType.SupportsVariation(null, null)) + { + PublishValue(_pvalue); + } + + // then deal with everything that varies + if (_vvalues == null) + { + return; + } + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + IEnumerable pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok + (culture == "*" || (x.Value.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches + (segment == "*" || (x.Value.Segment?.InvariantEquals(segment) ?? false))) // the segment matches + .Select(x => x.Value); + + foreach (IPropertyValue pvalue in pvalues) + { + PublishValue(pvalue); + } + } + + // internal - must be invoked by the content item + public void UnpublishValues(string? culture = "*", string? segment = "*") + { + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + PropertyType.SupportsVariation(null, null)) + { + UnpublishValue(_pvalue); + } + + // then deal with everything that varies + if (_vvalues == null) + { + return; + } + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + IEnumerable pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok + (culture == "*" || (x.Value.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches + (segment == "*" || (x.Value.Segment?.InvariantEquals(segment) ?? false))) // the segment matches + .Select(x => x.Value); + + foreach (IPropertyValue pvalue in pvalues) + { + UnpublishValue(pvalue); + } + } + + /// + /// Sets a value. + /// + public void SetValue(object? value, string? culture = null, string? segment = null) + { + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) + { + throw new NotSupportedException( + $"Variation \"{culture ?? ""},{segment ?? ""}\" is not supported by the property type."); + } + + (IPropertyValue? pvalue, var change) = GetPValue(culture, segment, true); + + if (pvalue is not null) + { + var origValue = pvalue.EditedValue; + var setValue = ConvertAssignedValue(value); + + pvalue.EditedValue = setValue; + + DetectChanges(setValue, origValue, nameof(Values), PropertyValueComparer, change); + } + } + + public object? ConvertAssignedValue(object? value) => + TryConvertAssignedValue(value, true, out var converted) ? converted : null; + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (Property)clone; + + // need to manually assign since this is a readonly property + clonedEntity.PropertyType = (PropertyType)PropertyType.DeepClone(); + } + + private object? GetPropertyValue(IPropertyValue? pvalue, bool published) + { + if (pvalue == null) + { + return null; + } + + return PropertyType.SupportsPublishing + ? published ? pvalue.PublishedValue : pvalue.EditedValue + : pvalue.EditedValue; + } + + private void PublishValue(IPropertyValue? pvalue) + { + if (pvalue == null) + { + return; + } + + if (!PropertyType.SupportsPublishing) + { + throw new NotSupportedException("Property type does not support publishing."); + } + + var origValue = pvalue.PublishedValue; + pvalue.PublishedValue = ConvertAssignedValue(pvalue.EditedValue); + DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); + } + + private void UnpublishValue(IPropertyValue? pvalue) + { + if (pvalue == null) + { + return; + } + + if (!PropertyType.SupportsPublishing) + { + throw new NotSupportedException("Property type does not support publishing."); + } + + var origValue = pvalue.PublishedValue; + pvalue.PublishedValue = ConvertAssignedValue(null); + DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); + } + + // bypasses all changes detection and is the *only* way to set the published value + private void FactorySetValue(string? culture, string? segment, bool published, object? value) + { + (IPropertyValue? pvalue, _) = GetPValue(culture, segment, true); + + if (pvalue is not null) + { + if (published && PropertyType.SupportsPublishing) + { + pvalue.PublishedValue = value; + } + else + { + pvalue.EditedValue = value; } } + } - // bypasses all changes detection and is the *only* way to set the published value - private void FactorySetValue(string? culture, string? segment, bool published, object? value) + private (IPropertyValue?, bool) GetPValue(bool create) + { + var change = false; + if (_pvalue == null) { - var (pvalue, _) = GetPValue(culture, segment, true); - - if (pvalue is not null) + if (!create) { - if (published && PropertyType.SupportsPublishing) - pvalue.PublishedValue = value; - else - pvalue.EditedValue = value; + return (null, false); } + + _pvalue = new PropertyValue(); + _values.Add(_pvalue); + change = true; } - private (IPropertyValue?, bool) GetPValue(bool create) + return (_pvalue, change); + } + + private (IPropertyValue?, bool) GetPValue(string? culture, string? segment, bool create) + { + if (culture == null && segment == null) { - var change = false; - if (_pvalue == null) - { - if (!create) return (null, false); - _pvalue = new PropertyValue(); - _values.Add(_pvalue); - change = true; - } - return (_pvalue, change); + return GetPValue(create); } - private (IPropertyValue?, bool) GetPValue(string? culture, string? segment, bool create) + var change = false; + if (_vvalues == null) { - if (culture == null && segment == null) - return GetPValue(create); + if (!create) + { + return (null, false); + } - var change = false; - if (_vvalues == null) - { - if (!create) return (null, false); - _vvalues = new Dictionary(); - change = true; - } - var k = new CompositeNStringNStringKey(culture, segment); - if (!_vvalues.TryGetValue(k, out var pvalue)) - { - if (!create) return (null, false); - pvalue = _vvalues[k] = new PropertyValue(); - pvalue.Culture = culture; - pvalue.Segment = segment; - _values.Add(pvalue); - change = true; - } - return (pvalue, change); + _vvalues = new Dictionary(); + change = true; } - /// - public object? ConvertAssignedValue(object? value) => TryConvertAssignedValue(value, true, out var converted) ? converted : null; - - /// - /// Tries to convert a value assigned to a property. - /// - /// - /// - /// - private bool TryConvertAssignedValue(object? value, bool throwOnError, out object? converted) + var k = new CompositeNStringNStringKey(culture, segment); + if (!_vvalues.TryGetValue(k, out IPropertyValue? pvalue)) { - var isOfExpectedType = IsOfExpectedPropertyType(value); - if (isOfExpectedType) + if (!create) { - converted = value; + return (null, false); + } + + pvalue = _vvalues[k] = new PropertyValue(); + pvalue.Culture = culture; + pvalue.Segment = segment; + _values.Add(pvalue); + change = true; + } + + return (pvalue, change); + } + + private static void ThrowTypeException(object? value, Type expected, string alias) => + throw new InvalidOperationException( + $"Cannot assign value \"{value}\" of type \"{value?.GetType()}\" to property \"{alias}\" expecting type \"{expected}\"."); + + /// + /// Tries to convert a value assigned to a property. + /// + /// + /// + /// + private bool TryConvertAssignedValue(object? value, bool throwOnError, out object? converted) + { + var isOfExpectedType = IsOfExpectedPropertyType(value); + if (isOfExpectedType) + { + converted = value; + return true; + } + + // isOfExpectedType is true if value is null - so if false, value is *not* null + // "garbage-in", accept what we can & convert + // throw only if conversion is not possible + var s = value?.ToString(); + converted = null; + + switch (ValueStorageType) + { + case ValueStorageType.Nvarchar: + case ValueStorageType.Ntext: + { + converted = s; return true; } - // isOfExpectedType is true if value is null - so if false, value is *not* null - // "garbage-in", accept what we can & convert - // throw only if conversion is not possible + case ValueStorageType.Integer: + if (s.IsNullOrWhiteSpace()) + { + return true; // assume empty means null + } - var s = value?.ToString(); - converted = null; + Attempt convInt = value.TryConvertTo(); + if (convInt.Success) + { + converted = convInt.Result; + return true; + } - switch (ValueStorageType) - { - case ValueStorageType.Nvarchar: - case ValueStorageType.Ntext: - { - converted = s; - return true; - } + if (throwOnError) + { + ThrowTypeException(value, typeof(int), Alias); + } - case ValueStorageType.Integer: - if (s.IsNullOrWhiteSpace()) - return true; // assume empty means null - var convInt = value.TryConvertTo(); - if (convInt.Success) - { - converted = convInt.Result; - return true; - } + return false; - if (throwOnError) - ThrowTypeException(value, typeof(int), Alias ?? string.Empty); - return false; + case ValueStorageType.Decimal: + if (s.IsNullOrWhiteSpace()) + { + return true; // assume empty means null + } - case ValueStorageType.Decimal: - if (s.IsNullOrWhiteSpace()) - return true; // assume empty means null - var convDecimal = value.TryConvertTo(); - if (convDecimal.Success) - { - // need to normalize the value (change the scaling factor and remove trailing zeros) - // because the underlying database is going to mess with the scaling factor anyways. - converted = convDecimal.Result.Normalize(); - return true; - } + Attempt convDecimal = value.TryConvertTo(); + if (convDecimal.Success) + { + // need to normalize the value (change the scaling factor and remove trailing zeros) + // because the underlying database is going to mess with the scaling factor anyways. + converted = convDecimal.Result.Normalize(); + return true; + } - if (throwOnError) - ThrowTypeException(value, typeof(decimal), Alias ?? string.Empty); - return false; + if (throwOnError) + { + ThrowTypeException(value, typeof(decimal), Alias); + } - case ValueStorageType.Date: - if (s.IsNullOrWhiteSpace()) - return true; // assume empty means null - var convDateTime = value.TryConvertTo(); - if (convDateTime.Success) - { - converted = convDateTime.Result; - return true; - } + return false; - if (throwOnError) - ThrowTypeException(value, typeof(DateTime), Alias ?? string.Empty); - return false; + case ValueStorageType.Date: + if (s.IsNullOrWhiteSpace()) + { + return true; // assume empty means null + } - default: - throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); - } + Attempt convDateTime = value.TryConvertTo(); + if (convDateTime.Success) + { + converted = convDateTime.Result; + return true; + } + + if (throwOnError) + { + ThrowTypeException(value, typeof(DateTime), Alias); + } + + return false; + + default: + throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); + } + } + + /// + /// Determines whether a value is of the expected type for this property type. + /// + /// + /// + /// If the value is of the expected type, it can be directly assigned to the property. + /// Otherwise, some conversion is required. + /// + /// + private bool IsOfExpectedPropertyType(object? value) + { + // null values are assumed to be ok + if (value == null) + { + return true; } - private static void ThrowTypeException(object? value, Type expected, string alias) + // check if the type of the value matches the type from the DataType/PropertyEditor + // then it can be directly assigned, anything else requires conversion + Type valueType = value.GetType(); + switch (ValueStorageType) { - throw new InvalidOperationException($"Cannot assign value \"{value}\" of type \"{value?.GetType()}\" to property \"{alias}\" expecting type \"{expected}\"."); + case ValueStorageType.Integer: + return valueType == typeof(int); + case ValueStorageType.Decimal: + return valueType == typeof(decimal); + case ValueStorageType.Date: + return valueType == typeof(DateTime); + case ValueStorageType.Nvarchar: + return valueType == typeof(string); + case ValueStorageType.Ntext: + return valueType == typeof(string); + default: + throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); + } + } + + /// + /// Used for constructing a new instance + /// + public class InitialPropertyValue + { + public InitialPropertyValue(string? culture, string? segment, bool published, object? value) + { + Culture = culture; + Segment = segment; + Published = published; + Value = value; + } + + public string? Culture { get; } + + public string? Segment { get; } + + public bool Published { get; } + + public object? Value { get; } + } + + /// + /// Represents a property value. + /// + public class PropertyValue : IPropertyValue, IDeepCloneable, IEquatable + { + // TODO: Either we allow change tracking at this class level, or we add some special change tracking collections to the Property + // class to deal with change tracking which variants have changed + private string? _culture; + private string? _segment; + + /// + /// Gets or sets the culture of the property. + /// + /// + /// The culture is either null (invariant) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. + /// + public string? Culture + { + get => _culture; + set => _culture = value.IsNullOrWhiteSpace() ? null : value!.ToLowerInvariant(); + } + + public object DeepClone() => Clone(); + + public bool Equals(PropertyValue? other) => + other != null && + _culture == other._culture && + _segment == other._segment && + EqualityComparer.Default.Equals(EditedValue, other.EditedValue) && + EqualityComparer.Default.Equals(PublishedValue, other.PublishedValue); + + /// + /// Gets or sets the segment of the property. + /// + /// + /// The segment is either null (neutral) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. + /// + public string? Segment + { + get => _segment; + set => _segment = value?.ToLowerInvariant(); } /// - /// Determines whether a value is of the expected type for this property type. + /// Gets or sets the edited value of the property. /// - /// - /// If the value is of the expected type, it can be directly assigned to the property. - /// Otherwise, some conversion is required. - /// - private bool IsOfExpectedPropertyType(object? value) - { - // null values are assumed to be ok - if (value == null) - return true; + public object? EditedValue { get; set; } - // check if the type of the value matches the type from the DataType/PropertyEditor - // then it can be directly assigned, anything else requires conversion - var valueType = value.GetType(); - switch (ValueStorageType) + /// + /// Gets or sets the published value of the property. + /// + public object? PublishedValue { get; set; } + + /// + /// Clones the property value. + /// + public IPropertyValue Clone() + => new PropertyValue { - case ValueStorageType.Integer: - return valueType == typeof(int); - case ValueStorageType.Decimal: - return valueType == typeof(decimal); - case ValueStorageType.Date: - return valueType == typeof(DateTime); - case ValueStorageType.Nvarchar: - return valueType == typeof(string); - case ValueStorageType.Ntext: - return valueType == typeof(string); - default: - throw new NotSupportedException($"Not supported storage type \"{ValueStorageType}\"."); - } - } + _culture = _culture, + _segment = _segment, + PublishedValue = PublishedValue, + EditedValue = EditedValue, + }; + public override bool Equals(object? obj) => Equals(obj as PropertyValue); - protected override void PerformDeepClone(object clone) + public override int GetHashCode() { - base.PerformDeepClone(clone); + var hashCode = 1885328050; + if (_culture is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(_culture); + } - var clonedEntity = (Property)clone; + if (_segment is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(_segment); + } - //need to manually assign since this is a readonly property - clonedEntity.PropertyType = (PropertyType) PropertyType.DeepClone(); + if (EditedValue is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(EditedValue); + } + + if (PublishedValue is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(PublishedValue); + } + + return hashCode; } } } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index 49b392ba67..dbb648df29 100644 --- a/src/Umbraco.Core/Models/PropertyCollection.cs +++ b/src/Umbraco.Core/Models/PropertyCollection.cs @@ -1,215 +1,217 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a collection of property values. +/// +[Serializable] +[DataContract(IsReference = true)] +public class PropertyCollection : KeyedCollection, IPropertyCollection { + private readonly object _addLocker = new(); /// - /// Represents a collection of property values. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class PropertyCollection : KeyedCollection, IPropertyCollection + public PropertyCollection() + : base(StringComparer.InvariantCultureIgnoreCase) { - private readonly object _addLocker = new object(); + } - /// - /// Initializes a new instance of the class. - /// - public PropertyCollection() - : base(StringComparer.InvariantCultureIgnoreCase) - { } + /// + /// Initializes a new instance of the class. + /// + public PropertyCollection(IEnumerable properties) + : this() => + Reset(properties); - /// - /// Initializes a new instance of the class. - /// - public PropertyCollection(IEnumerable properties) - : this() + /// + /// Occurs when the collection changes. + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + /// + /// Gets the property with the specified PropertyType. + /// + internal IProperty? this[IPropertyType propertyType] => + this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyType.Alias)); + + /// + public new void Add(IProperty property) + { + // TODO: why are we locking here and not everywhere else?! + lock (_addLocker) { - Reset(properties); - } - - /// - /// Replaces all properties, whilst maintaining validation delegates. - /// - private void Reset(IEnumerable properties) - { - //collection events will be raised in each of these calls - Clear(); - - //collection events will be raised in each of these calls - foreach (var property in properties) - Add(property); - } - - /// - /// Replaces the property at the specified index with the specified property. - /// - protected override void SetItem(int index, IProperty property) - { - var oldItem = index >= 0 ? this[index] : property; - base.SetItem(index, property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem)); - } - - /// - /// Removes the property at the specified index. - /// - protected override void RemoveItem(int index) - { - var removed = this[index]; - base.RemoveItem(index); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); - } - - /// - /// Inserts the specified property at the specified index. - /// - protected override void InsertItem(int index, IProperty property) - { - base.InsertItem(index, property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property)); - } - - /// - /// Removes all properties. - /// - protected override void ClearItems() - { - base.ClearItems(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - /// - public new void Add(IProperty property) - { - lock (_addLocker) // TODO: why are we locking here and not everywhere else?! + var key = GetKeyForItem(property); + if (key != null) { - var key = GetKeyForItem(property); - if (key != null) + if (Contains(key)) { - if (Contains(key)) + // transfer id and values if ... + IProperty existing = this[key]; + + if (property.Id == 0 && existing.Id != 0) { - // transfer id and values if ... - var existing = this[key]; - - if (property.Id == 0 && existing.Id != 0) - property.Id = existing.Id; - - if (property.Values.Count == 0 && existing.Values.Count > 0) - property.Values = existing.Values.Select(x => x.Clone()).ToList(); - - // replace existing with property and return, - // SetItem invokes OnCollectionChanged (but not OnAdd) - SetItem(IndexOfKey(key), property); - return; + property.Id = existing.Id; } + + if (property.Values.Count == 0 && existing.Values.Count > 0) + { + property.Values = existing.Values.Select(x => x.Clone()).ToList(); + } + + // replace existing with property and return, + // SetItem invokes OnCollectionChanged (but not OnAdd) + SetItem(IndexOfKey(key), property); + return; } - - //collection events will be raised in InsertItem with Add - base.Add(property); - } - } - - /// - /// Gets the index for a specified property alias. - /// - private int IndexOfKey(string key) - { - for (var i = 0; i < Count; i++) - { - if (this[i].Alias?.InvariantEquals(key) ?? false) - return i; - } - return -1; - } - - protected override string GetKeyForItem(IProperty item) - { - return item.Alias!; - } - - /// - /// Gets the property with the specified PropertyType. - /// - internal IProperty? this[IPropertyType propertyType] - { - get - { - return this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyType.Alias)); - } - } - - public bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property) - { - property = this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - return property != null; - } - - /// - /// Occurs when the collection changes. - /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - public void ClearCollectionChangedEvents() => CollectionChanged = null; - - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) - { - CollectionChanged?.Invoke(this, args); - } - - - /// - public void EnsurePropertyTypes(IEnumerable propertyTypes) - { - if (propertyTypes == null) - return; - - foreach (var propertyType in propertyTypes) - Add(new Property(propertyType)); - } - - - /// - public void EnsureCleanPropertyTypes(IEnumerable propertyTypes) - { - if (propertyTypes == null) - return; - - var propertyTypesA = propertyTypes.ToArray(); - - var thisAliases = this.Select(x => x.Alias); - var typeAliases = propertyTypesA.Select(x => x.Alias); - var remove = thisAliases.Except(typeAliases).ToArray(); - foreach (var alias in remove) - { - if (alias is not null) - { - Remove(alias); - } - } - - foreach (var propertyType in propertyTypesA) - Add(new Property(propertyType)); - } - - /// - /// Deep clones. - /// - public object DeepClone() - { - var clone = new PropertyCollection(); - foreach (var property in this) - clone.Add((Property) property.DeepClone()); - return clone; + // collection events will be raised in InsertItem with Add + base.Add(property); } } + + public new bool TryGetValue(string propertyTypeAlias, [MaybeNullWhen(false)] out IProperty property) + { + property = this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + return property != null; + } + + public void ClearCollectionChangedEvents() => CollectionChanged = null; + + /// + public void EnsurePropertyTypes(IEnumerable propertyTypes) + { + if (propertyTypes == null) + { + return; + } + + foreach (IPropertyType propertyType in propertyTypes) + { + Add(new Property(propertyType)); + } + } + + /// + public void EnsureCleanPropertyTypes(IEnumerable propertyTypes) + { + if (propertyTypes == null) + { + return; + } + + IPropertyType[] propertyTypesA = propertyTypes.ToArray(); + + IEnumerable thisAliases = this.Select(x => x.Alias); + IEnumerable typeAliases = propertyTypesA.Select(x => x.Alias); + var remove = thisAliases.Except(typeAliases).ToArray(); + foreach (var alias in remove) + { + if (alias is not null) + { + Remove(alias); + } + } + + foreach (IPropertyType propertyType in propertyTypesA) + { + Add(new Property(propertyType)); + } + } + + /// + /// Deep clones. + /// + public object DeepClone() + { + var clone = new PropertyCollection(); + foreach (IProperty property in this) + { + clone.Add((Property)property.DeepClone()); + } + + return clone; + } + + /// + /// Replaces the property at the specified index with the specified property. + /// + protected override void SetItem(int index, IProperty property) + { + IProperty oldItem = index >= 0 ? this[index] : property; + base.SetItem(index, property); + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem)); + } + + /// + /// Replaces all properties, whilst maintaining validation delegates. + /// + private void Reset(IEnumerable properties) + { + // collection events will be raised in each of these calls + Clear(); + + // collection events will be raised in each of these calls + foreach (IProperty property in properties) + { + Add(property); + } + } + + /// + /// Removes the property at the specified index. + /// + protected override void RemoveItem(int index) + { + IProperty removed = this[index]; + base.RemoveItem(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } + + /// + /// Inserts the specified property at the specified index. + /// + protected override void InsertItem(int index, IProperty property) + { + base.InsertItem(index, property); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property)); + } + + /// + /// Removes all properties. + /// + protected override void ClearItems() + { + base.ClearItems(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + protected override string GetKeyForItem(IProperty item) => item.Alias; + + /// + /// Gets the index for a specified property alias. + /// + private int IndexOfKey(string key) + { + for (var i = 0; i < Count; i++) + { + if (this[i].Alias?.InvariantEquals(key) ?? false) + { + return i; + } + } + + return -1; + } + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => + CollectionChanged?.Invoke(this, args); } diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs index 17e6603284..034770cdfc 100644 --- a/src/Umbraco.Core/Models/PropertyGroup.cs +++ b/src/Umbraco.Core/Models/PropertyGroup.cs @@ -1,159 +1,157 @@ -using System; using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a group of property types. +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] +public class PropertyGroup : EntityBase, IEquatable { - /// - /// Represents a group of property types. - /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] - public class PropertyGroup : EntityBase, IEquatable + [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "This field is for internal use only (to allow changing item keys).")] + internal PropertyGroupCollection? Collection; + + private string _alias; + private string? _name; + private PropertyTypeCollection? _propertyTypes; + private int _sortOrder; + + private PropertyGroupType _type; + + public PropertyGroup(bool isPublishing) + : this(new PropertyTypeCollection(isPublishing)) { - [SuppressMessage("Style", "IDE1006:Naming Styles", - Justification = "This field is for internal use only (to allow changing item keys).")] - internal PropertyGroupCollection? Collection; + } - private PropertyGroupType _type; - private string? _name; - private string _alias; - private int _sortOrder; - private PropertyTypeCollection? _propertyTypes; + public PropertyGroup(PropertyTypeCollection propertyTypeCollection) + { + PropertyTypes = propertyTypeCollection; + _alias = string.Empty; + } - public PropertyGroup(bool isPublishing) - : this(new PropertyTypeCollection(isPublishing)) + /// + /// Gets or sets the type of the group. + /// + /// + /// The type. + /// + [DataMember] + public PropertyGroupType Type + { + get => _type; + set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type)); + } + + /// + /// Gets or sets the name of the group. + /// + /// + /// The name. + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Gets or sets the alias of the group. + /// + /// + /// The alias. + /// + [DataMember] + public string Alias + { + get => _alias; + set { + // If added to a collection, ensure the key is changed before setting it (this ensures the internal lookup dictionary is updated) + Collection?.ChangeKey(this, value); + + SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); } + } - public PropertyGroup(PropertyTypeCollection propertyTypeCollection) - { - PropertyTypes = propertyTypeCollection; - _alias = string.Empty; - } + /// + /// Gets or sets the sort order of the group. + /// + /// + /// The sort order. + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } - private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) + /// + /// Gets or sets a collection of property types for the group. + /// + /// + /// The property types. + /// + /// + /// Marked with DoNotClone, because we will manually deal with cloning and the event handlers. + /// + [DataMember] + [DoNotClone] + public PropertyTypeCollection? PropertyTypes + { + get => _propertyTypes; + set { - OnPropertyChanged(nameof(PropertyTypes)); - } - - /// - /// Gets or sets the type of the group. - /// - /// - /// The type. - /// - [DataMember] - public PropertyGroupType Type - { - get => _type; - set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type)); - } - - /// - /// Gets or sets the name of the group. - /// - /// - /// The name. - /// - [DataMember] - public string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - /// - /// Gets or sets the alias of the group. - /// - /// - /// The alias. - /// - [DataMember] - public string Alias - { - get => _alias; - set + if (_propertyTypes != null) { - // If added to a collection, ensure the key is changed before setting it (this ensures the internal lookup dictionary is updated) - Collection?.ChangeKey(this, value); - - SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + _propertyTypes.ClearCollectionChangedEvents(); } - } - /// - /// Gets or sets the sort order of the group. - /// - /// - /// The sort order. - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } + _propertyTypes = value; - /// - /// Gets or sets a collection of property types for the group. - /// - /// - /// The property types. - /// - /// - /// Marked with DoNotClone, because we will manually deal with cloning and the event handlers. - /// - [DataMember] - [DoNotClone] - public PropertyTypeCollection? PropertyTypes - { - get => _propertyTypes; - set + if (_propertyTypes is not null) { - if (_propertyTypes != null) + // since we're adding this collection to this group, + // we need to ensure that all the lazy values are set. + foreach (IPropertyType propertyType in _propertyTypes) { - _propertyTypes.ClearCollectionChangedEvents(); + propertyType.PropertyGroupId = new Lazy(() => Id); } - _propertyTypes = value; - - if (_propertyTypes is not null) - { - // since we're adding this collection to this group, - // we need to ensure that all the lazy values are set. - foreach (var propertyType in _propertyTypes) - propertyType.PropertyGroupId = new Lazy(() => Id); - - OnPropertyChanged(nameof(PropertyTypes)); - _propertyTypes.CollectionChanged += PropertyTypesChanged; - } - } - } - - public bool Equals(PropertyGroup? other) => - base.Equals(other) || (other != null && Type == other.Type && Alias == other.Alias); - - public override int GetHashCode() => (base.GetHashCode(), Type, Alias).GetHashCode(); - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (PropertyGroup)clone; - clonedEntity.Collection = null; - - if (clonedEntity._propertyTypes != null) - { - clonedEntity._propertyTypes.ClearCollectionChangedEvents(); //clear this event handler if any - clonedEntity._propertyTypes = (PropertyTypeCollection)_propertyTypes!.DeepClone(); //manually deep clone - clonedEntity._propertyTypes.CollectionChanged += - clonedEntity.PropertyTypesChanged; //re-assign correct event handler + OnPropertyChanged(nameof(PropertyTypes)); + _propertyTypes.CollectionChanged += PropertyTypesChanged; } } } + + public bool Equals(PropertyGroup? other) => + base.Equals(other) || (other != null && Type == other.Type && Alias == other.Alias); + + public override int GetHashCode() => (base.GetHashCode(), Type, Alias).GetHashCode(); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (PropertyGroup)clone; + clonedEntity.Collection = null; + + if (clonedEntity._propertyTypes != null) + { + clonedEntity._propertyTypes.ClearCollectionChangedEvents(); // clear this event handler if any + clonedEntity._propertyTypes = (PropertyTypeCollection)_propertyTypes!.DeepClone(); // manually deep clone + clonedEntity._propertyTypes.CollectionChanged += + clonedEntity.PropertyTypesChanged; // re-assign correct event handler + } + } + + private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) => + OnPropertyChanged(nameof(PropertyTypes)); } diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index f248b12811..5e4479ec37 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -1,156 +1,162 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a collection of objects +/// +[Serializable] +[DataContract] + +// TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details +public class PropertyGroupCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { /// - /// Represents a collection of objects + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract] - // TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details - public class PropertyGroupCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable + public PropertyGroupCollection() { - /// - /// Initializes a new instance of the class. - /// - public PropertyGroupCollection() - { } + } - /// - /// Initializes a new instance of the class. - /// - /// The groups. - public PropertyGroupCollection(IEnumerable groups) => Reset(groups); + /// + /// Initializes a new instance of the class. + /// + /// The groups. + public PropertyGroupCollection(IEnumerable groups) => Reset(groups); - /// - /// Resets the collection to only contain the instances referenced in the parameter. - /// - /// The property groups. - /// - internal void Reset(IEnumerable groups) + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + public object DeepClone() + { + var clone = new PropertyGroupCollection(); + foreach (PropertyGroup group in this) { - // Collection events will be raised in each of these calls - Clear(); - - // Collection events will be raised in each of these calls - foreach (var group in groups) - Add(group); + clone.Add((PropertyGroup)group.DeepClone()); } - protected override void SetItem(int index, PropertyGroup item) + return clone; + } + + public new void Add(PropertyGroup item) + { + // Ensure alias is set + if (string.IsNullOrEmpty(item.Alias)) { - var oldItem = index >= 0 ? this[index] : item; - - base.SetItem(index, item); - - oldItem.Collection = null; - item.Collection = this; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + throw new InvalidOperationException("Set an alias before adding the property group."); } - protected override void RemoveItem(int index) + // Note this is done to ensure existing groups can be renamed + if (item.HasIdentity && item.Id > 0) { - var removed = this[index]; - - base.RemoveItem(index); - - removed.Collection = null; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); - } - - protected override void InsertItem(int index, PropertyGroup item) - { - base.InsertItem(index, item); - - item.Collection = this; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); - } - - protected override void ClearItems() - { - foreach (var item in this) + var index = IndexOfKey(item.Id); + if (index != -1) { - item.Collection = null; - } - - base.ClearItems(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - public new void Add(PropertyGroup item) - { - // Ensure alias is set - if (string.IsNullOrEmpty(item.Alias)) - { - throw new InvalidOperationException("Set an alias before adding the property group."); - } - - // Note this is done to ensure existing groups can be renamed - if (item.HasIdentity && item.Id > 0) - { - var index = IndexOfKey(item.Id); - if (index != -1) + var keyExists = Contains(item.Alias); + if (keyExists) { - var keyExists = Contains(item.Alias); - if (keyExists) - throw new ArgumentException($"Naming conflict: changing the alias of property group '{item.Name}' would result in duplicates."); - - // Collection events will be raised in SetItem - SetItem(index, item); - return; + throw new ArgumentException( + $"Naming conflict: changing the alias of property group '{item.Name}' would result in duplicates."); } + + // Collection events will be raised in SetItem + SetItem(index, item); + return; } - else + } + else + { + var index = IndexOfKey(item.Alias); + if (index != -1) { - var index = IndexOfKey(item.Alias); - if (index != -1) - { - // Collection events will be raised in SetItem - SetItem(index, item); - return; - } + // Collection events will be raised in SetItem + SetItem(index, item); + return; } - - // Collection events will be raised in InsertItem - base.Add(item); } - internal void ChangeKey(PropertyGroup item, string newKey) => ChangeItemKey(item, newKey); + // Collection events will be raised in InsertItem + base.Add(item); + } - public bool Contains(int id) => this.IndexOfKey(id) != -1; + public bool Contains(int id) => IndexOfKey(id) != -1; - public int IndexOfKey(string key) => this.FindIndex(x => x.Alias == key); + /// + /// Resets the collection to only contain the instances referenced in the + /// parameter. + /// + /// The property groups. + /// + internal void Reset(IEnumerable groups) + { + // Collection events will be raised in each of these calls + Clear(); - public int IndexOfKey(int id) => this.FindIndex(x => x.Id == id); - - protected override string GetKeyForItem(PropertyGroup item) => item.Alias; - - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; - - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); - - public object DeepClone() + // Collection events will be raised in each of these calls + foreach (PropertyGroup group in groups) { - var clone = new PropertyGroupCollection(); - foreach (var group in this) - { - clone.Add((PropertyGroup)group.DeepClone()); - } - - return clone; + Add(group); } } + + protected override void SetItem(int index, PropertyGroup item) + { + PropertyGroup oldItem = index >= 0 ? this[index] : item; + + base.SetItem(index, item); + + oldItem.Collection = null; + item.Collection = this; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + } + + protected override void RemoveItem(int index) + { + PropertyGroup removed = this[index]; + + base.RemoveItem(index); + + removed.Collection = null; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } + + protected override void InsertItem(int index, PropertyGroup item) + { + base.InsertItem(index, item); + + item.Collection = this; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + } + + protected override void ClearItems() + { + foreach (PropertyGroup item in this) + { + item.Collection = null; + } + + base.ClearItems(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + internal void ChangeKey(PropertyGroup item, string newKey) => ChangeItemKey(item, newKey); + + public int IndexOfKey(string key) => this.FindIndex(x => x.Alias == key); + + public int IndexOfKey(int id) => this.FindIndex(x => x.Id == id); + + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => CollectionChanged = null; + + protected override string GetKeyForItem(PropertyGroup item) => item.Alias; + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => + CollectionChanged?.Invoke(this, args); } diff --git a/src/Umbraco.Core/Models/PropertyGroupExtensions.cs b/src/Umbraco.Core/Models/PropertyGroupExtensions.cs index bb12e1bc1b..95f3bce75b 100644 --- a/src/Umbraco.Core/Models/PropertyGroupExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyGroupExtensions.cs @@ -1,83 +1,82 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public static class PropertyGroupExtensions { - public static class PropertyGroupExtensions + private const char AliasSeparator = '/'; + + /// + /// Gets the local alias. + /// + /// The property group. + /// + /// The local alias. + /// + public static string? GetLocalAlias(this PropertyGroup propertyGroup) => GetLocalAlias(propertyGroup.Alias); + + internal static string? GetLocalAlias(string alias) { - private const char AliasSeparator = '/'; - - internal static string? GetLocalAlias(string alias) + var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; + if (lastIndex != -1) { - var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; - if (lastIndex != -1) - { - return alias?.Substring(lastIndex + 1); - } - - return alias; + return alias?.Substring(lastIndex + 1); } - internal static string? GetParentAlias(string? alias) - { - var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; - if (lastIndex == -1) - { - return null; - } + return alias; + } - return alias?.Substring(0, lastIndex); + internal static string? GetParentAlias(string? alias) + { + var lastIndex = alias?.LastIndexOf(AliasSeparator) ?? -1; + if (lastIndex == -1) + { + return null; } - /// - /// Gets the local alias. - /// - /// The property group. - /// - /// The local alias. - /// - public static string? GetLocalAlias(this PropertyGroup propertyGroup) => GetLocalAlias(propertyGroup.Alias); + return alias?.Substring(0, lastIndex); + } - /// - /// Updates the local alias. - /// - /// The property group. - /// The local alias. - public static void UpdateLocalAlias(this PropertyGroup propertyGroup, string localAlias) + /// + /// Updates the local alias. + /// + /// The property group. + /// The local alias. + public static void UpdateLocalAlias(this PropertyGroup propertyGroup, string localAlias) + { + var parentAlias = propertyGroup.GetParentAlias(); + if (string.IsNullOrEmpty(parentAlias)) { - var parentAlias = propertyGroup.GetParentAlias(); - if (string.IsNullOrEmpty(parentAlias)) - { - propertyGroup.Alias = localAlias; - } - else - { - propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; - } + propertyGroup.Alias = localAlias; } - - /// - /// Gets the parent alias. - /// - /// The property group. - /// - /// The parent alias. - /// - public static string? GetParentAlias(this PropertyGroup propertyGroup) => GetParentAlias(propertyGroup.Alias); - - /// - /// Updates the parent alias. - /// - /// The property group. - /// The parent alias. - public static void UpdateParentAlias(this PropertyGroup propertyGroup, string parentAlias) + else { - var localAlias = propertyGroup.GetLocalAlias(); - if (string.IsNullOrEmpty(parentAlias)) - { - propertyGroup.Alias = localAlias!; - } - else - { - propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; - } + propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; + } + } + + /// + /// Gets the parent alias. + /// + /// The property group. + /// + /// The parent alias. + /// + public static string? GetParentAlias(this PropertyGroup propertyGroup) => GetParentAlias(propertyGroup.Alias); + + /// + /// Updates the parent alias. + /// + /// The property group. + /// The parent alias. + public static void UpdateParentAlias(this PropertyGroup propertyGroup, string parentAlias) + { + var localAlias = propertyGroup.GetLocalAlias(); + if (string.IsNullOrEmpty(parentAlias)) + { + propertyGroup.Alias = localAlias!; + } + else + { + propertyGroup.Alias = parentAlias + AliasSeparator + localAlias; } } } diff --git a/src/Umbraco.Core/Models/PropertyGroupType.cs b/src/Umbraco.Core/Models/PropertyGroupType.cs index 03bcbc08f0..9111bf9bb4 100644 --- a/src/Umbraco.Core/Models/PropertyGroupType.cs +++ b/src/Umbraco.Core/Models/PropertyGroupType.cs @@ -1,17 +1,17 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the type of a property group. +/// +public enum PropertyGroupType : short { /// - /// Represents the type of a property group. + /// Display property types in a group. /// - public enum PropertyGroupType : short - { - /// - /// Display property types in a group. - /// - Group = 0, - /// - /// Display property types in a tab. - /// - Tab = 1 - } + Group = 0, + + /// + /// Display property types in a tab. + /// + Tab = 1, } diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index 7bd3a49baf..9ad98d66c0 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -8,235 +5,291 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the class to manage tags. +/// +public static class PropertyTagsExtensions { - /// - /// Provides extension methods for the class to manage tags. - /// - public static class PropertyTagsExtensions + // gets the tag configuration for a property + // from the datatype configuration, and the editor tag configuration attribute + public static TagConfiguration? GetTagConfiguration(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) { - // gets the tag configuration for a property - // from the datatype configuration, and the editor tag configuration attribute - public static TagConfiguration? GetTagConfiguration(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) + if (property == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var editor = propertyEditors[property.PropertyType?.PropertyEditorAlias]; - var tagAttribute = editor?.GetTagAttribute(); - if (tagAttribute == null) return null; - - var configurationObject = property.PropertyType is null ? null : dataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; - var configuration = ConfigurationEditor.ConfigurationAs(configurationObject); - - if (configuration?.Delimiter == default && configuration?.Delimiter is not null) - configuration.Delimiter = tagAttribute.Delimiter; - - return configuration; + throw new ArgumentNullException(nameof(property)); } - /// - /// Assign tags. - /// - /// The property. - /// - /// The tags. - /// A value indicating whether to merge the tags with existing tags instead of replacing them. - /// A culture, for multi-lingual properties. - /// - /// - public static void AssignTags(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, IEnumerable tags, bool merge = false, string? culture = null) + IDataEditor? editor = propertyEditors[property.PropertyType?.PropertyEditorAlias]; + TagsPropertyEditorAttribute? tagAttribute = editor?.GetTagAttribute(); + if (tagAttribute == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); - if (configuration == null) - throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - - property.AssignTags(tags, merge, configuration.StorageType, serializer, configuration.Delimiter, culture); + return null; } - // assumes that parameters are consistent with the datatype configuration - private static void AssignTags(this IProperty property, IEnumerable tags, bool merge, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + var configurationObject = property.PropertyType is null + ? null + : dataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; + TagConfiguration? configuration = ConfigurationEditor.ConfigurationAs(configurationObject); + + if (configuration?.Delimiter == default && configuration?.Delimiter is not null) { - // set the property value - var trimmedTags = tags.Select(x => x.Trim()).ToArray(); - - if (merge) - { - var currentTags = property.GetTagsValue(storageType, serializer, delimiter); - - switch (storageType) - { - case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string - break; - - case TagsStorageType.Json: - var updatedTags = currentTags.Union(trimmedTags).ToArray(); - var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); - property.SetValue(updatedValue, culture); // json array - break; - } - } - else - { - switch (storageType) - { - case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(), culture); // csv string - break; - - case TagsStorageType.Json: - var updatedValue = trimmedTags.Length == 0 ? null : serializer.Serialize(trimmedTags); - property.SetValue(updatedValue, culture); // json array - break; - } - } + configuration.Delimiter = tagAttribute.Delimiter; } - /// - /// Removes tags. - /// - /// The property. - /// - /// The tags. - /// A culture, for multi-lingual properties. - /// - /// - public static void RemoveTags(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, IEnumerable tags, string? culture = null) + return configuration; + } + + /// + /// Assign tags. + /// + /// The property. + /// + /// The tags. + /// A value indicating whether to merge the tags with existing tags instead of replacing them. + /// A culture, for multi-lingual properties. + /// + /// + public static void AssignTags(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, IEnumerable tags, bool merge = false, string? culture = null) + { + if (property == null) { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); - if (configuration == null) - throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - - property.RemoveTags(tags, configuration.StorageType, serializer, configuration.Delimiter, culture); + throw new ArgumentNullException(nameof(property)); } - // assumes that parameters are consistent with the datatype configuration - private static void RemoveTags(this IProperty property, IEnumerable tags, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) { - // already empty = nothing to do - var value = property.GetValue(culture)?.ToString(); - if (string.IsNullOrWhiteSpace(value)) return; + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } + + property.AssignTags(tags, merge, configuration.StorageType, serializer, configuration.Delimiter, culture); + } + + /// + /// Removes tags. + /// + /// The property. + /// + /// The tags. + /// A culture, for multi-lingual properties. + /// + /// + public static void RemoveTags(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, IEnumerable tags, string? culture = null) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) + { + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } + + property.RemoveTags(tags, configuration.StorageType, serializer, configuration.Delimiter, culture); + } + + // used by ContentRepositoryBase + public static IEnumerable GetTagsValue(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, string? culture = null) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + TagConfiguration? configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); + if (configuration == null) + { + throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); + } + + return property.GetTagsValue(configuration.StorageType, serializer, configuration.Delimiter, culture); + } + + /// + /// Sets tags on a content property, based on the property editor tags configuration. + /// + /// The property. + /// + /// The property value. + /// The datatype configuration. + /// A culture, for multi-lingual properties. + /// + /// The value is either a string (delimited string) or an enumeration of strings (tag list). + /// + /// This is used both by the content repositories to initialize a property with some tag values, and by the + /// content controllers to update a property with values received from the property editor. + /// + /// + public static void SetTagsValue(this IProperty property, IJsonSerializer serializer, object? value, TagConfiguration? tagConfiguration, string? culture) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + if (tagConfiguration == null) + { + throw new ArgumentNullException(nameof(tagConfiguration)); + } + + TagsStorageType storageType = tagConfiguration.StorageType; + var delimiter = tagConfiguration.Delimiter; + + SetTagsValue(property, value, storageType, serializer, delimiter, culture); + } + + // assumes that parameters are consistent with the datatype configuration + private static void AssignTags(this IProperty property, IEnumerable tags, bool merge, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + { + // set the property value + var trimmedTags = tags.Select(x => x.Trim()).ToArray(); + + if (merge) + { + IEnumerable currentTags = property.GetTagsValue(storageType, serializer, delimiter); - // set the property value - var trimmedTags = tags.Select(x => x.Trim()).ToArray(); - var currentTags = property.GetTagsValue(storageType, serializer, delimiter, culture); switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string + property.SetValue( + string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)).NullOrWhiteSpaceAsNull(), + culture); // csv string break; case TagsStorageType.Json: - var updatedTags = currentTags.Except(trimmedTags).ToArray(); + var updatedTags = currentTags.Union(trimmedTags).ToArray(); var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); property.SetValue(updatedValue, culture); // json array break; } } - - // used by ContentRepositoryBase - public static IEnumerable GetTagsValue(this IProperty property, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IJsonSerializer serializer, string? culture = null) + else { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var configuration = property.GetTagConfiguration(propertyEditors, dataTypeService); - if (configuration == null) - throw new NotSupportedException($"Property with alias \"{property.Alias}\" does not support tags."); - - return property.GetTagsValue(configuration.StorageType, serializer, configuration.Delimiter, culture); - } - - private static IEnumerable GetTagsValue(this IProperty property, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture = null) - { - if (property == null) throw new ArgumentNullException(nameof(property)); - - var value = property.GetValue(culture)?.ToString(); - if (string.IsNullOrWhiteSpace(value)) return Enumerable.Empty(); - switch (storageType) { case TagsStorageType.Csv: - return value.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); - - case TagsStorageType.Json: - try - { - return serializer.Deserialize(value)?.Select(x => x.Trim()) ?? Enumerable.Empty(); - } - catch (Exception) - { - //cannot parse, malformed - return Enumerable.Empty(); - } - - default: - throw new NotSupportedException($"Value \"{storageType}\" is not a valid TagsStorageType."); - } - } - - /// - /// Sets tags on a content property, based on the property editor tags configuration. - /// - /// The property. - /// The property value. - /// The datatype configuration. - /// A culture, for multi-lingual properties. - /// - /// The value is either a string (delimited string) or an enumeration of strings (tag list). - /// This is used both by the content repositories to initialize a property with some tag values, and by the - /// content controllers to update a property with values received from the property editor. - /// - public static void SetTagsValue(this IProperty property, IJsonSerializer serializer, object? value, TagConfiguration? tagConfiguration, string? culture) - { - if (property == null) throw new ArgumentNullException(nameof(property)); - if (tagConfiguration == null) throw new ArgumentNullException(nameof(tagConfiguration)); - - var storageType = tagConfiguration.StorageType; - var delimiter = tagConfiguration.Delimiter; - - SetTagsValue(property, value, storageType, serializer, delimiter, culture); - } - - // assumes that parameters are consistent with the datatype configuration - // value can be an enumeration of string, or a serialized value using storageType format - private static void SetTagsValue(IProperty property, object? value, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) - { - if (value == null) value = Enumerable.Empty(); - - // if value is already an enumeration of strings, just use it - if (value is IEnumerable tags1) - { - property.AssignTags(tags1, false, storageType, serializer, delimiter, culture); - return; - } - - // otherwise, deserialize value based upon storage type - switch (storageType) - { - case TagsStorageType.Csv: - var tags2 = value.ToString()!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - property.AssignTags(tags2, false, storageType, serializer, delimiter, culture); + property.SetValue( + string.Join(delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(), + culture); // csv string break; case TagsStorageType.Json: - try - { - var tags3 = serializer.Deserialize>(value.ToString()!); - property.AssignTags(tags3 ?? Enumerable.Empty(), false, storageType, serializer, delimiter, culture); - } - catch (Exception ex) - { - StaticApplicationLogging.Logger.LogWarning(ex, "Could not automatically convert stored json value to an enumerable string '{Json}'", value.ToString()); - } + var updatedValue = trimmedTags.Length == 0 ? null : serializer.Serialize(trimmedTags); + property.SetValue(updatedValue, culture); // json array break; - - default: - throw new ArgumentOutOfRangeException(nameof(storageType)); } } } + + // assumes that parameters are consistent with the datatype configuration + private static void RemoveTags(this IProperty property, IEnumerable tags, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + { + // already empty = nothing to do + var value = property.GetValue(culture)?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + // set the property value + var trimmedTags = tags.Select(x => x.Trim()).ToArray(); + IEnumerable currentTags = property.GetTagsValue(storageType, serializer, delimiter, culture); + switch (storageType) + { + case TagsStorageType.Csv: + property.SetValue( + string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)).NullOrWhiteSpaceAsNull(), + culture); // csv string + break; + + case TagsStorageType.Json: + var updatedTags = currentTags.Except(trimmedTags).ToArray(); + var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); + property.SetValue(updatedValue, culture); // json array + break; + } + } + + private static IEnumerable GetTagsValue(this IProperty property, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture = null) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + var value = property.GetValue(culture)?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Enumerable.Empty(); + } + + switch (storageType) + { + case TagsStorageType.Csv: + return value.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); + + case TagsStorageType.Json: + try + { + return serializer.Deserialize(value)?.Select(x => x.Trim()) ?? Enumerable.Empty(); + } + catch (Exception) + { + // cannot parse, malformed + return Enumerable.Empty(); + } + + default: + throw new NotSupportedException($"Value \"{storageType}\" is not a valid TagsStorageType."); + } + } + + // assumes that parameters are consistent with the datatype configuration + // value can be an enumeration of string, or a serialized value using storageType format + private static void SetTagsValue(IProperty property, object? value, TagsStorageType storageType, IJsonSerializer serializer, char delimiter, string? culture) + { + if (value == null) + { + value = Enumerable.Empty(); + } + + // if value is already an enumeration of strings, just use it + if (value is IEnumerable tags1) + { + property.AssignTags(tags1, false, storageType, serializer, delimiter, culture); + return; + } + + // otherwise, deserialize value based upon storage type + switch (storageType) + { + case TagsStorageType.Csv: + var tags2 = value.ToString()!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); + property.AssignTags(tags2, false, storageType, serializer, delimiter, culture); + break; + + case TagsStorageType.Json: + try + { + IEnumerable? tags3 = serializer.Deserialize>(value.ToString()!); + property.AssignTags(tags3 ?? Enumerable.Empty(), false, storageType, serializer, delimiter, culture); + } + catch (Exception ex) + { + StaticApplicationLogging.Logger.LogWarning( + ex, + "Could not automatically convert stored json value to an enumerable string '{Json}'", + value.ToString()); + } + + break; + + default: + throw new ArgumentOutOfRangeException(nameof(storageType)); + } + } } diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 3acbad2720..0699ecbc0d 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -1,295 +1,305 @@ -using System; using System.Diagnostics; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a property type. +/// +[Serializable] +[DataContract(IsReference = true)] +[DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] +public class PropertyType : EntityBase, IPropertyType, IEquatable { + private readonly bool _forceValueStorageType; + private readonly IShortStringHelper _shortStringHelper; + private string _alias; + private int _dataTypeId; + private Guid _dataTypeKey; + private string? _description; + private bool _labelOnTop; + private bool _mandatory; + private string? _mandatoryMessage; + private string _name; + private string _propertyEditorAlias; + private Lazy? _propertyGroupId; + private int _sortOrder; + private string? _validationRegExp; + private string? _validationRegExpMessage; + private ValueStorageType _valueStorageType; + private ContentVariation _variations; + /// - /// Represents a property type. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")] - public class PropertyType : EntityBase, IPropertyType, IEquatable + public PropertyType(IShortStringHelper shortStringHelper, IDataType dataType) { - private readonly IShortStringHelper _shortStringHelper; - private readonly bool _forceValueStorageType; - private string _name; - private string _alias; - private string? _description; - private int _dataTypeId; - private Guid _dataTypeKey; - private Lazy? _propertyGroupId; - private string _propertyEditorAlias; - private ValueStorageType _valueStorageType; - private bool _mandatory; - private string? _mandatoryMessage; - private int _sortOrder; - private string? _validationRegExp; - private string? _validationRegExpMessage; - private ContentVariation _variations; - private bool _labelOnTop; - - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper, IDataType dataType) + if (dataType == null) { - if (dataType == null) throw new ArgumentNullException(nameof(dataType)); - _shortStringHelper = shortStringHelper; - - if (dataType.HasIdentity) - _dataTypeId = dataType.Id; - - _propertyEditorAlias = dataType.EditorAlias; - _valueStorageType = dataType.DatabaseType; - _variations = ContentVariation.Nothing; - _alias = string.Empty; - _name = string.Empty; + throw new ArgumentNullException(nameof(dataType)); } - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper, IDataType dataType, string propertyTypeAlias) - : this(shortStringHelper, dataType) + _shortStringHelper = shortStringHelper; + + if (dataType.HasIdentity) { - _alias = SanitizeAlias(propertyTypeAlias); + _dataTypeId = dataType.Id; } - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper,string propertyEditorAlias, ValueStorageType valueStorageType) - : this(shortStringHelper, propertyEditorAlias, valueStorageType, false) - { - } + _propertyEditorAlias = dataType.EditorAlias; + _valueStorageType = dataType.DatabaseType; + _variations = ContentVariation.Nothing; + _alias = string.Empty; + _name = string.Empty; + } - /// - /// Initializes a new instance of the class. - /// - public PropertyType(IShortStringHelper shortStringHelper,string propertyEditorAlias, ValueStorageType valueStorageType, string propertyTypeAlias) - : this(shortStringHelper, propertyEditorAlias, valueStorageType, false, propertyTypeAlias) - { - } + /// + /// Initializes a new instance of the class. + /// + public PropertyType(IShortStringHelper shortStringHelper, IDataType dataType, string propertyTypeAlias) + : this(shortStringHelper, dataType) => + _alias = SanitizeAlias(propertyTypeAlias); - /// - /// Initializes a new instance of the class. - /// - /// Set to true to force the value storage type. Values assigned to - /// the property, eg from the underlying datatype, will be ignored. - public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType, bool forceValueStorageType, string? propertyTypeAlias = null) - { - _shortStringHelper = shortStringHelper; - _propertyEditorAlias = propertyEditorAlias; - _valueStorageType = valueStorageType; - _forceValueStorageType = forceValueStorageType; - _alias = propertyTypeAlias == null ? string.Empty : SanitizeAlias(propertyTypeAlias); - _variations = ContentVariation.Nothing; - _name = string.Empty; - } + /// + /// Initializes a new instance of the class. + /// + public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType) + : this(shortStringHelper, propertyEditorAlias, valueStorageType, false) + { + } - /// - /// Gets a value indicating whether the content type owning this property type is publishing. - /// - /// - /// A publishing content type supports draft and published values for properties. - /// It is possible to retrieve either the draft (default) or published value of a property. - /// Setting the value always sets the draft value, which then needs to be published. - /// A non-publishing content type only supports one value for properties. Getting - /// the draft or published value of a property returns the same thing, and publishing - /// a value property has no effect. - /// When true, getting the property value returns the edited value by default, but - /// it is possible to get the published value using the appropriate 'published' method - /// parameter. - /// When false, getting the property value always return the edited value, - /// regardless of the 'published' method parameter. - /// - public bool SupportsPublishing { get; set; } + /// + /// Initializes a new instance of the class. + /// + public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType, string propertyTypeAlias) + : this(shortStringHelper, propertyEditorAlias, valueStorageType, false, propertyTypeAlias) + { + } - /// - [DataMember] - public string Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); - } + /// + /// Initializes a new instance of the class. + /// + /// + /// Set to true to force the value storage type. Values assigned to + /// the property, eg from the underlying datatype, will be ignored. + /// + public PropertyType(IShortStringHelper shortStringHelper, string propertyEditorAlias, ValueStorageType valueStorageType, bool forceValueStorageType, string? propertyTypeAlias = null) + { + _shortStringHelper = shortStringHelper; + _propertyEditorAlias = propertyEditorAlias; + _valueStorageType = valueStorageType; + _forceValueStorageType = forceValueStorageType; + _alias = propertyTypeAlias == null ? string.Empty : SanitizeAlias(propertyTypeAlias); + _variations = ContentVariation.Nothing; + _name = string.Empty; + } - /// - [DataMember] - public virtual string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(SanitizeAlias(value), ref _alias!, nameof(Alias)); - } + /// + /// Gets a value indicating whether the content type owning this property type is publishing. + /// + /// + /// + /// A publishing content type supports draft and published values for properties. + /// It is possible to retrieve either the draft (default) or published value of a property. + /// Setting the value always sets the draft value, which then needs to be published. + /// + /// + /// A non-publishing content type only supports one value for properties. Getting + /// the draft or published value of a property returns the same thing, and publishing + /// a value property has no effect. + /// + /// + /// When true, getting the property value returns the edited value by default, but + /// it is possible to get the published value using the appropriate 'published' method + /// parameter. + /// + /// + /// When false, getting the property value always return the edited value, + /// regardless of the 'published' method parameter. + /// + /// + public bool SupportsPublishing { get; set; } - /// - [DataMember] - public string? Description - { - get => _description; - set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); - } + /// + public bool Equals(PropertyType? other) => + other != null && (base.Equals(other) || (Alias?.InvariantEquals(other.Alias) ?? false)); - /// - [DataMember] - public int DataTypeId - { - get => _dataTypeId; - set => SetPropertyValueAndDetectChanges(value, ref _dataTypeId, nameof(DataTypeId)); - } + /// + [DataMember] + public string Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } - [DataMember] - public Guid DataTypeKey - { - get => _dataTypeKey; - set => SetPropertyValueAndDetectChanges(value, ref _dataTypeKey, nameof(DataTypeKey)); - } + /// + [DataMember] + public virtual string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(SanitizeAlias(value), ref _alias!, nameof(Alias)); + } - /// - [DataMember] - public string PropertyEditorAlias - { - get => _propertyEditorAlias; - set => SetPropertyValueAndDetectChanges(value, ref _propertyEditorAlias!, nameof(PropertyEditorAlias)); - } + /// + [DataMember] + public string? Description + { + get => _description; + set => SetPropertyValueAndDetectChanges(value, ref _description, nameof(Description)); + } - /// - [DataMember] - public ValueStorageType ValueStorageType + /// + [DataMember] + public int DataTypeId + { + get => _dataTypeId; + set => SetPropertyValueAndDetectChanges(value, ref _dataTypeId, nameof(DataTypeId)); + } + + [DataMember] + public Guid DataTypeKey + { + get => _dataTypeKey; + set => SetPropertyValueAndDetectChanges(value, ref _dataTypeKey, nameof(DataTypeKey)); + } + + /// + [DataMember] + public string PropertyEditorAlias + { + get => _propertyEditorAlias; + set => SetPropertyValueAndDetectChanges(value, ref _propertyEditorAlias!, nameof(PropertyEditorAlias)); + } + + /// + [DataMember] + public ValueStorageType ValueStorageType + { + get => _valueStorageType; + set { - get => _valueStorageType; - set + if (_forceValueStorageType) { - if (_forceValueStorageType) return; // ignore changes - SetPropertyValueAndDetectChanges(value, ref _valueStorageType, nameof(ValueStorageType)); + return; // ignore changes } - } - /// - [DataMember] - [DoNotClone] - public Lazy? PropertyGroupId - { - get => _propertyGroupId; - set => SetPropertyValueAndDetectChanges(value, ref _propertyGroupId, nameof(PropertyGroupId)); - } - - /// - [DataMember] - public bool Mandatory - { - get => _mandatory; - set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(Mandatory)); - } - - - /// - [DataMember] - public string? MandatoryMessage - { - get => _mandatoryMessage; - set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage)); - } - - /// - [DataMember] - public bool LabelOnTop - { - get => _labelOnTop; - set => SetPropertyValueAndDetectChanges(value, ref _labelOnTop, nameof(LabelOnTop)); - } - - /// - [DataMember] - public int SortOrder - { - get => _sortOrder; - set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); - } - - /// - [DataMember] - public string? ValidationRegExp - { - get => _validationRegExp; - set => SetPropertyValueAndDetectChanges(value, ref _validationRegExp, nameof(ValidationRegExp)); - } - - - /// - /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched - /// - [DataMember] - public string? ValidationRegExpMessage - { - get => _validationRegExpMessage; - set => SetPropertyValueAndDetectChanges(value, ref _validationRegExpMessage, nameof(ValidationRegExpMessage)); - } - - /// - public ContentVariation Variations - { - get => _variations; - set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); - } - - /// - public bool SupportsVariation(string? culture, string? segment, bool wildcards = false) - { - // exact validation: cannot accept a 'null' culture if the property type varies - // by culture, and likewise for segment - // wildcard validation: can accept a '*' culture or segment - return Variations.ValidateVariation(culture, segment, true, wildcards, false); - } - - /// - /// Sanitizes a property type alias. - /// - private string SanitizeAlias(string value) - { - //NOTE: WE are doing this because we don't want to do a ToSafeAlias when the alias is the special case of - // being prefixed with Constants.PropertyEditors.InternalGenericPropertiesPrefix - // which is used internally - - return value.StartsWith(Constants.PropertyEditors.InternalGenericPropertiesPrefix) - ? value - : value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase); - } - - /// - public bool Equals(PropertyType? other) - { - return other != null && (base.Equals(other) || (Alias?.InvariantEquals(other.Alias) ?? false)); - } - - /// - public override int GetHashCode() - { - //Get hash code for the Name field if it is not null. - int baseHash = base.GetHashCode(); - - //Get hash code for the Alias field. - int? hashAlias = Alias?.ToLowerInvariant().GetHashCode(); - - //Calculate the hash code for the product. - return baseHash ^ hashAlias ?? baseHash; - } - - /// - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var clonedEntity = (PropertyType) clone; - //need to manually assign the Lazy value as it will not be automatically mapped - if (PropertyGroupId != null) - { - clonedEntity._propertyGroupId = new Lazy(() => PropertyGroupId.Value); - } + SetPropertyValueAndDetectChanges(value, ref _valueStorageType, nameof(ValueStorageType)); } } + + /// + [DataMember] + [DoNotClone] + public Lazy? PropertyGroupId + { + get => _propertyGroupId; + set => SetPropertyValueAndDetectChanges(value, ref _propertyGroupId, nameof(PropertyGroupId)); + } + + /// + [DataMember] + public bool Mandatory + { + get => _mandatory; + set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(Mandatory)); + } + + /// + [DataMember] + public string? MandatoryMessage + { + get => _mandatoryMessage; + set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage)); + } + + /// + [DataMember] + public bool LabelOnTop + { + get => _labelOnTop; + set => SetPropertyValueAndDetectChanges(value, ref _labelOnTop, nameof(LabelOnTop)); + } + + /// + [DataMember] + public int SortOrder + { + get => _sortOrder; + set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder)); + } + + /// + [DataMember] + public string? ValidationRegExp + { + get => _validationRegExp; + set => SetPropertyValueAndDetectChanges(value, ref _validationRegExp, nameof(ValidationRegExp)); + } + + /// + /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched + /// + [DataMember] + public string? ValidationRegExpMessage + { + get => _validationRegExpMessage; + set => SetPropertyValueAndDetectChanges(value, ref _validationRegExpMessage, nameof(ValidationRegExpMessage)); + } + + /// + public ContentVariation Variations + { + get => _variations; + set => SetPropertyValueAndDetectChanges(value, ref _variations, nameof(Variations)); + } + + /// + public bool SupportsVariation(string? culture, string? segment, bool wildcards = false) => + + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + Variations.ValidateVariation(culture, segment, true, wildcards, false); + + /// + public override int GetHashCode() + { + // Get hash code for the Name field if it is not null. + var baseHash = base.GetHashCode(); + + // Get hash code for the Alias field. + var hashAlias = Alias?.ToLowerInvariant().GetHashCode(); + + // Calculate the hash code for the product. + return baseHash ^ hashAlias ?? baseHash; + } + + /// + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var clonedEntity = (PropertyType)clone; + + // need to manually assign the Lazy value as it will not be automatically mapped + if (PropertyGroupId != null) + { + clonedEntity._propertyGroupId = new Lazy(() => PropertyGroupId.Value); + } + } + + /// + /// Sanitizes a property type alias. + /// + private string SanitizeAlias(string value) => + + // NOTE: WE are doing this because we don't want to do a ToSafeAlias when the alias is the special case of + // being prefixed with Constants.PropertyEditors.InternalGenericPropertiesPrefix + // which is used internally + value.StartsWith(Constants.PropertyEditors.InternalGenericPropertiesPrefix) + ? value + : value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase); } diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 96133f6677..49c83b4c9d 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -1,179 +1,184 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.Linq; +using System.ComponentModel; using System.Runtime.Serialization; -using System.Threading; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +// public interface IPropertyTypeCollection: IEnumerable + +/// +/// Represents a collection of objects. +/// +[Serializable] +[DataContract] + +// TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details +public class PropertyTypeCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable, + ICollection { + public PropertyTypeCollection(bool supportsPublishing) => SupportsPublishing = supportsPublishing; - //public interface IPropertyTypeCollection: IEnumerable - /// - /// Represents a collection of objects. - /// - [Serializable] - [DataContract] - // TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details - public class PropertyTypeCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable, ICollection + public PropertyTypeCollection(bool supportsPublishing, IEnumerable properties) + : this(supportsPublishing) => + Reset(properties); + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + public bool SupportsPublishing { get; } + + // This baseclass calling is needed, else compiler will complain about nullability + + /// + public bool IsReadOnly => ((ICollection)this).IsReadOnly; + + // 'new' keyword is required! we can explicitly implement ICollection.Add BUT since normally a concrete PropertyType type + // is passed in, the explicit implementation doesn't get called, this ensures it does get called. + public new void Add(IPropertyType item) { - public PropertyTypeCollection(bool supportsPublishing) + item.SupportsPublishing = SupportsPublishing; + + // TODO: this is not pretty and should be refactored + var key = GetKeyForItem(item); + if (key != null) { - SupportsPublishing = supportsPublishing; - } - - public PropertyTypeCollection(bool supportsPublishing, IEnumerable properties) - : this(supportsPublishing) - { - Reset(properties); - } - - public bool SupportsPublishing { get; } - - // This baseclass calling is needed, else compiler will complain about nullability - - /// - public bool IsReadOnly => ((ICollection)this).IsReadOnly; - - /// - /// Resets the collection to only contain the instances referenced in the parameter. - /// - /// The properties. - /// - internal void Reset(IEnumerable properties) - { - //collection events will be raised in each of these calls - Clear(); - - //collection events will be raised in each of these calls - foreach (var property in properties) - Add(property); - } - - protected override void SetItem(int index, IPropertyType item) - { - item.SupportsPublishing = SupportsPublishing; - var oldItem = index >= 0 ? this[index] : item; - base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); - item.PropertyChanged += Item_PropertyChanged; - } - - protected override void RemoveItem(int index) - { - var removed = this[index]; - base.RemoveItem(index); - removed.PropertyChanged -= Item_PropertyChanged; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); - } - - protected override void InsertItem(int index, IPropertyType item) - { - item.SupportsPublishing = SupportsPublishing; - base.InsertItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); - item.PropertyChanged += Item_PropertyChanged; - } - - protected override void ClearItems() - { - base.ClearItems(); - foreach (var item in this) - item.PropertyChanged -= Item_PropertyChanged; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - // 'new' keyword is required! we can explicitly implement ICollection.Add BUT since normally a concrete PropertyType type - // is passed in, the explicit implementation doesn't get called, this ensures it does get called. - public new void Add(IPropertyType item) - { - item.SupportsPublishing = SupportsPublishing; - - // TODO: this is not pretty and should be refactored - - var key = GetKeyForItem(item); - if (key != null) + var exists = Contains(key); + if (exists) { - var exists = Contains(key); - if (exists) - { - //collection events will be raised in SetItem - SetItem(IndexOfKey(key), item); - return; - } + // collection events will be raised in SetItem + SetItem(IndexOfKey(key), item); + return; } - - //check if the item's sort order is already in use - if (this.Any(x => x.SortOrder == item.SortOrder)) - { - //make it the next iteration - item.SortOrder = this.Max(x => x.SortOrder) + 1; - } - - //collection events will be raised in InsertItem - base.Add(item); } - /// - /// Occurs when a property changes on a IPropertyType that exists in this collection - /// - /// - /// - private void Item_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + // check if the item's sort order is already in use + if (this.Any(x => x.SortOrder == item.SortOrder)) { - var propType = (IPropertyType?)sender; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType)); + // make it the next iteration + item.SortOrder = this.Max(x => x.SortOrder) + 1; } - /// - /// Determines whether this collection contains a whose alias matches the specified PropertyType. - /// - /// Alias of the PropertyType. - /// true if the collection contains the specified alias; otherwise, false. - /// - public new bool Contains(string propertyAlias) + // collection events will be raised in InsertItem + base.Add(item); + } + + public object DeepClone() + { + var clone = new PropertyTypeCollection(SupportsPublishing); + foreach (IPropertyType propertyType in this) { - return this.Any(x => x.Alias == propertyAlias); + clone.Add((IPropertyType)propertyType.DeepClone()); } - public bool RemoveItem(string propertyTypeAlias) - { - var key = IndexOfKey(propertyTypeAlias); - if (key != -1) RemoveItem(key); - return key != -1; - } + return clone; + } - public int IndexOfKey(string key) - { - for (var i = 0; i < Count; i++) - if (this[i].Alias == key) - return i; - return -1; - } + /// + /// Determines whether this collection contains a whose alias matches the specified + /// PropertyType. + /// + /// Alias of the PropertyType. + /// true if the collection contains the specified alias; otherwise, false. + /// + public new bool Contains(string propertyAlias) => this.Any(x => x.Alias == propertyAlias); - protected override string GetKeyForItem(IPropertyType item) - { - return item.Alias!; - } + /// + /// Resets the collection to only contain the instances referenced in the + /// parameter. + /// + /// The properties. + /// + internal void Reset(IEnumerable properties) + { + // collection events will be raised in each of these calls + Clear(); - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - /// - /// Clears all event handlers - /// - public void ClearCollectionChangedEvents() => CollectionChanged = null; - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + // collection events will be raised in each of these calls + foreach (IPropertyType property in properties) { - CollectionChanged?.Invoke(this, args); - } - - public object DeepClone() - { - var clone = new PropertyTypeCollection(SupportsPublishing); - foreach (var propertyType in this) - clone.Add((IPropertyType) propertyType.DeepClone()); - return clone; + Add(property); } } + + protected override void SetItem(int index, IPropertyType item) + { + item.SupportsPublishing = SupportsPublishing; + IPropertyType oldItem = index >= 0 ? this[index] : item; + base.SetItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + item.PropertyChanged += Item_PropertyChanged; + } + + protected override void RemoveItem(int index) + { + IPropertyType removed = this[index]; + base.RemoveItem(index); + removed.PropertyChanged -= Item_PropertyChanged; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } + + protected override void InsertItem(int index, IPropertyType item) + { + item.SupportsPublishing = SupportsPublishing; + base.InsertItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + item.PropertyChanged += Item_PropertyChanged; + } + + protected override void ClearItems() + { + base.ClearItems(); + foreach (IPropertyType item in this) + { + item.PropertyChanged -= Item_PropertyChanged; + } + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + /// Occurs when a property changes on a IPropertyType that exists in this collection + /// + /// + /// + private void Item_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + var propType = (IPropertyType?)sender; + OnCollectionChanged( + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType)); + } + + public bool RemoveItem(string propertyTypeAlias) + { + var key = IndexOfKey(propertyTypeAlias); + if (key != -1) + { + RemoveItem(key); + } + + return key != -1; + } + + public int IndexOfKey(string key) + { + for (var i = 0; i < Count; i++) + { + if (this[i].Alias == key) + { + return i; + } + } + + return -1; + } + + /// + /// Clears all event handlers + /// + public void ClearCollectionChangedEvents() => CollectionChanged = null; + + protected override string GetKeyForItem(IPropertyType item) => item.Alias; + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) => + CollectionChanged?.Invoke(this, args); } diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 00e05442d8..8789ef5052 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -1,158 +1,154 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class PublicAccessEntry : EntityBase { - [Serializable] - [DataContract(IsReference = true)] - public class PublicAccessEntry : EntityBase + private readonly List _removedRules = new(); + private readonly EventClearingObservableCollection _ruleCollection; + private int _loginNodeId; + private int _noAccessNodeId; + private int _protectedNodeId; + + public PublicAccessEntry(IContent protectedNode, IContent loginNode, IContent noAccessNode, IEnumerable ruleCollection) { - private readonly EventClearingObservableCollection _ruleCollection; - private int _protectedNodeId; - private int _noAccessNodeId; - private int _loginNodeId; - private readonly List _removedRules = new List(); - - public PublicAccessEntry(IContent protectedNode, IContent loginNode, IContent noAccessNode, IEnumerable ruleCollection) + if (protectedNode == null) { - if (protectedNode == null) throw new ArgumentNullException(nameof(protectedNode)); - if (loginNode == null) throw new ArgumentNullException(nameof(loginNode)); - if (noAccessNode == null) throw new ArgumentNullException(nameof(noAccessNode)); - - LoginNodeId = loginNode.Id; - NoAccessNodeId = noAccessNode.Id; - _protectedNodeId = protectedNode.Id; - - _ruleCollection = new EventClearingObservableCollection(ruleCollection); - _ruleCollection.CollectionChanged += _ruleCollection_CollectionChanged; - - foreach (var rule in _ruleCollection) - rule.AccessEntryId = Key; + throw new ArgumentNullException(nameof(protectedNode)); } - public PublicAccessEntry(Guid id, int protectedNodeId, int loginNodeId, int noAccessNodeId, IEnumerable ruleCollection) + if (loginNode == null) { - Key = id; - Id = Key.GetHashCode(); - - LoginNodeId = loginNodeId; - NoAccessNodeId = noAccessNodeId; - _protectedNodeId = protectedNodeId; - - _ruleCollection = new EventClearingObservableCollection(ruleCollection); - _ruleCollection.CollectionChanged += _ruleCollection_CollectionChanged; - - foreach (var rule in _ruleCollection) - rule.AccessEntryId = Key; + throw new ArgumentNullException(nameof(loginNode)); } - void _ruleCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + if (noAccessNode == null) { - OnPropertyChanged(nameof(Rules)); + throw new ArgumentNullException(nameof(noAccessNode)); + } - //if (e.Action == NotifyCollectionChangedAction.Add) - //{ - // var item = e.NewItems.Cast().First(); + LoginNodeId = loginNode.Id; + NoAccessNodeId = noAccessNode.Id; + _protectedNodeId = protectedNode.Id; - // if (_addedSections.Contains(item) == false) - // { - // _addedSections.Add(item); - // } - //} + _ruleCollection = new EventClearingObservableCollection(ruleCollection); + _ruleCollection.CollectionChanged += RuleCollection_CollectionChanged; - if (e.Action == NotifyCollectionChangedAction.Remove) + foreach (PublicAccessRule rule in _ruleCollection) + { + rule.AccessEntryId = Key; + } + } + + public PublicAccessEntry(Guid id, int protectedNodeId, int loginNodeId, int noAccessNodeId, IEnumerable ruleCollection) + { + Key = id; + Id = Key.GetHashCode(); + + LoginNodeId = loginNodeId; + NoAccessNodeId = noAccessNodeId; + _protectedNodeId = protectedNodeId; + + _ruleCollection = new EventClearingObservableCollection(ruleCollection); + _ruleCollection.CollectionChanged += RuleCollection_CollectionChanged; + + foreach (PublicAccessRule rule in _ruleCollection) + { + rule.AccessEntryId = Key; + } + } + + public IEnumerable RemovedRules => _removedRules; + + public IEnumerable Rules => _ruleCollection; + + [DataMember] + public int LoginNodeId + { + get => _loginNodeId; + set => SetPropertyValueAndDetectChanges(value, ref _loginNodeId, nameof(LoginNodeId)); + } + + [DataMember] + public int NoAccessNodeId + { + get => _noAccessNodeId; + set => SetPropertyValueAndDetectChanges(value, ref _noAccessNodeId, nameof(NoAccessNodeId)); + } + + [DataMember] + public int ProtectedNodeId + { + get => _protectedNodeId; + set => SetPropertyValueAndDetectChanges(value, ref _protectedNodeId, nameof(ProtectedNodeId)); + } + + public PublicAccessRule AddRule(string ruleValue, string ruleType) + { + var rule = new PublicAccessRule { AccessEntryId = Key, RuleValue = ruleValue, RuleType = ruleType }; + _ruleCollection.Add(rule); + return rule; + } + + private void RuleCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(Rules)); + + // if (e.Action == NotifyCollectionChangedAction.Add) + // { + // var item = e.NewItems.Cast().First(); + + // if (_addedSections.Contains(item) == false) + // { + // _addedSections.Add(item); + // } + // } + if (e.Action == NotifyCollectionChangedAction.Remove) + { + PublicAccessRule? item = e.OldItems?.Cast().First(); + + if (item is not null) { - var item = e.OldItems?.Cast().First(); - - if (item is not null) + if (_removedRules.Contains(item.Key) == false) { - if (_removedRules.Contains(item.Key) == false) - { - _removedRules.Add(item.Key); - } + _removedRules.Add(item.Key); } } } + } - public IEnumerable RemovedRules => _removedRules; + public void RemoveRule(PublicAccessRule rule) => _ruleCollection.Remove(rule); - public IEnumerable Rules => _ruleCollection; + public void ClearRules() => _ruleCollection.Clear(); - public PublicAccessRule AddRule(string ruleValue, string ruleType) + public override void ResetDirtyProperties(bool rememberDirty) + { + _removedRules.Clear(); + base.ResetDirtyProperties(rememberDirty); + foreach (PublicAccessRule publicAccessRule in _ruleCollection) { - var rule = new PublicAccessRule - { - AccessEntryId = Key, - RuleValue = ruleValue, - RuleType = ruleType - }; - _ruleCollection.Add(rule); - return rule; + publicAccessRule.ResetDirtyProperties(rememberDirty); } + } - public void RemoveRule(PublicAccessRule rule) + internal void ClearRemovedRules() => _removedRules.Clear(); + + protected override void PerformDeepClone(object clone) + { + base.PerformDeepClone(clone); + + var cloneEntity = (PublicAccessEntry)clone; + + if (cloneEntity._ruleCollection != null) { - _ruleCollection.Remove(rule); - } - - public void ClearRules() - { - _ruleCollection.Clear(); - } - - - internal void ClearRemovedRules() - { - _removedRules.Clear(); - } - - [DataMember] - public int LoginNodeId - { - get => _loginNodeId; - set => SetPropertyValueAndDetectChanges(value, ref _loginNodeId, nameof(LoginNodeId)); - } - - [DataMember] - public int NoAccessNodeId - { - get => _noAccessNodeId; - set => SetPropertyValueAndDetectChanges(value, ref _noAccessNodeId, nameof(NoAccessNodeId)); - } - - [DataMember] - public int ProtectedNodeId - { - get => _protectedNodeId; - set => SetPropertyValueAndDetectChanges(value, ref _protectedNodeId, nameof(ProtectedNodeId)); - } - - public override void ResetDirtyProperties(bool rememberDirty) - { - _removedRules.Clear(); - base.ResetDirtyProperties(rememberDirty); - foreach (var publicAccessRule in _ruleCollection) - { - publicAccessRule.ResetDirtyProperties(rememberDirty); - } - } - - protected override void PerformDeepClone(object clone) - { - base.PerformDeepClone(clone); - - var cloneEntity = (PublicAccessEntry)clone; - - if (cloneEntity._ruleCollection != null) - { - cloneEntity._ruleCollection.ClearCollectionChangedEvents(); //clear this event handler if any - cloneEntity._ruleCollection.CollectionChanged += cloneEntity._ruleCollection_CollectionChanged; //re-assign correct event handler - } + cloneEntity._ruleCollection.ClearCollectionChangedEvents(); // clear this event handler if any + cloneEntity._ruleCollection.CollectionChanged += + cloneEntity.RuleCollection_CollectionChanged; // re-assign correct event handler } } } diff --git a/src/Umbraco.Core/Models/PublicAccessRule.cs b/src/Umbraco.Core/Models/PublicAccessRule.cs index 790d8b6a1b..f8af1a6d98 100644 --- a/src/Umbraco.Core/Models/PublicAccessRule.cs +++ b/src/Umbraco.Core/Models/PublicAccessRule.cs @@ -1,41 +1,37 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class PublicAccessRule : EntityBase { - [Serializable] - [DataContract(IsReference = true)] - public class PublicAccessRule : EntityBase + private string? _ruleType; + private string? _ruleValue; + + public PublicAccessRule(Guid id, Guid accessEntryId) { - private string? _ruleValue; - private string? _ruleType; + AccessEntryId = accessEntryId; + Key = id; + Id = Key.GetHashCode(); + } - public PublicAccessRule(Guid id, Guid accessEntryId) - { - AccessEntryId = accessEntryId; - Key = id; - Id = Key.GetHashCode(); - } + public PublicAccessRule() + { + } - public PublicAccessRule() - { - } - - public Guid AccessEntryId { get; set; } - - public string? RuleValue - { - get => _ruleValue; - set => SetPropertyValueAndDetectChanges(value, ref _ruleValue, nameof(RuleValue)); - } - - public string? RuleType - { - get => _ruleType; - set => SetPropertyValueAndDetectChanges(value, ref _ruleType, nameof(RuleType)); - } + public Guid AccessEntryId { get; set; } + public string? RuleValue + { + get => _ruleValue; + set => SetPropertyValueAndDetectChanges(value, ref _ruleValue, nameof(RuleValue)); + } + public string? RuleType + { + get => _ruleType; + set => SetPropertyValueAndDetectChanges(value, ref _ruleType, nameof(RuleType)); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs index 1aaa0d9814..2c665f1710 100644 --- a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs @@ -1,75 +1,63 @@ -using System; using System.Collections; -using System.Collections.Generic; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Manages the built-in fallback policies. +/// +public struct Fallback : IEnumerable { /// - /// Manages the built-in fallback policies. + /// Do not fallback. /// - public struct Fallback : IEnumerable - { - private readonly int[] _values; + public const int None = 0; - /// - /// Initializes a new instance of the struct with values. - /// - private Fallback(int[] values) - { - _values = values; - } + private readonly int[] _values; - /// - /// Gets an ordered set of fallback policies. - /// - /// - public static Fallback To(params int[] values) => new Fallback(values); + /// + /// Initializes a new instance of the struct with values. + /// + private Fallback(int[] values) => _values = values; - /// - /// Do not fallback. - /// - public const int None = 0; + /// + /// Gets an ordered set of fallback policies. + /// + /// + public static Fallback To(params int[] values) => new(values); - /// - /// Fallback to default value. - /// - public const int DefaultValue = 1; + /// + /// Fallback to default value. + /// + public const int DefaultValue = 1; - /// - /// Gets the fallback to default value policy. - /// - public static Fallback ToDefaultValue => new Fallback(new[] { DefaultValue }); + /// + /// Fallback to other languages. + /// + public const int Language = 2; - /// - /// Fallback to other languages. - /// - public const int Language = 2; + /// + /// Fallback to tree ancestors. + /// + public const int Ancestors = 3; - /// - /// Gets the fallback to language policy. - /// - public static Fallback ToLanguage => new Fallback(new[] { Language }); + /// + /// Gets the fallback to default value policy. + /// + public static Fallback ToDefaultValue => new(new[] { DefaultValue }); - /// - /// Fallback to tree ancestors. - /// - public const int Ancestors = 3; + /// + /// Gets the fallback to language policy. + /// + public static Fallback ToLanguage => new(new[] { Language }); - /// - /// Gets the fallback to tree ancestors policy. - /// - public static Fallback ToAncestors => new Fallback(new[] { Ancestors }); + /// + /// Gets the fallback to tree ancestors policy. + /// + public static Fallback ToAncestors => new(new[] { Ancestors }); - /// - public IEnumerator GetEnumerator() - { - return ((IEnumerable)_values ?? Array.Empty()).GetEnumerator(); - } + /// + public IEnumerator GetEnumerator() => ((IEnumerable)_values ?? Array.Empty()).GetEnumerator(); - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Models/PublishedContent/HttpContextVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/HttpContextVariationContextAccessor.cs index 3fb18fad2d..6d8fe9e547 100644 --- a/src/Umbraco.Core/Models/PublishedContent/HttpContextVariationContextAccessor.cs +++ b/src/Umbraco.Core/Models/PublishedContent/HttpContextVariationContextAccessor.cs @@ -1,25 +1,24 @@ using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Implements on top of . +/// +public class HttpContextVariationContextAccessor : IVariationContextAccessor { + private const string ContextKey = "Umbraco.Web.Models.PublishedContent.DefaultVariationContextAccessor"; + private readonly IRequestCache _requestCache; + /// - /// Implements on top of . + /// Initializes a new instance of the class. /// - public class HttpContextVariationContextAccessor : IVariationContextAccessor + public HttpContextVariationContextAccessor(IRequestCache requestCache) => _requestCache = requestCache; + + /// + public VariationContext? VariationContext { - private readonly IRequestCache _requestCache; - private const string ContextKey = "Umbraco.Web.Models.PublishedContent.DefaultVariationContextAccessor"; - - /// - /// Initializes a new instance of the class. - /// - public HttpContextVariationContextAccessor(IRequestCache requestCache) => _requestCache = requestCache; - - /// - public VariationContext? VariationContext - { - get => (VariationContext?) _requestCache.Get(ContextKey); - set => _requestCache.Set(ContextKey, value); - } + get => (VariationContext?)_requestCache.Get(ContextKey); + set => _requestCache.Set(ContextKey, value); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/HybridVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/HybridVariationContextAccessor.cs index d974041d3b..2be9638438 100644 --- a/src/Umbraco.Core/Models/PublishedContent/HybridVariationContextAccessor.cs +++ b/src/Umbraco.Core/Models/PublishedContent/HybridVariationContextAccessor.cs @@ -1,23 +1,23 @@ using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Models.PublishedContent -{ - /// - /// Implements a hybrid . - /// - public class HybridVariationContextAccessor : HybridAccessorBase, IVariationContextAccessor - { - public HybridVariationContextAccessor(IRequestCache requestCache) - : base(requestCache) - { } +namespace Umbraco.Cms.Core.Models.PublishedContent; - /// - /// Gets or sets the object. - /// - public VariationContext? VariationContext - { - get => Value; - set => Value = value; - } +/// +/// Implements a hybrid . +/// +public class HybridVariationContextAccessor : HybridAccessorBase, IVariationContextAccessor +{ + public HybridVariationContextAccessor(IRequestCache requestCache) + : base(requestCache) + { + } + + /// + /// Gets or sets the object. + /// + public VariationContext? VariationContext + { + get => Value; + set => Value = value; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs index 2838297a8e..37ca5b3733 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IAutoPublishedModelFactory.cs @@ -1,24 +1,22 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a live published model creation service. +/// +public interface IAutoPublishedModelFactory : IPublishedModelFactory { + /// + /// Gets an object that can be used to synchronize access to the factory. + /// + object SyncRoot { get; } /// - /// Provides a live published model creation service. + /// If the live model factory /// - public interface IAutoPublishedModelFactory : IPublishedModelFactory - { - /// - /// Gets an object that can be used to synchronize access to the factory. - /// - object SyncRoot { get; } + bool Enabled { get; } - /// - /// Tells the factory that it should build a new generation of models - /// - void Reset(); - - /// - /// If the live model factory - /// - bool Enabled { get; } - } + /// + /// Tells the factory that it should build a new generation of models + /// + void Reset(); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index eb52339936..01b57f38f8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -1,150 +1,150 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// +/// Represents a published content item. +/// +/// +/// Can be a published document, media or member. +/// +public interface IPublishedContent : IPublishedElement { + // TODO: IPublishedContent properties colliding with models + // we need to find a way to remove as much clutter as possible from IPublishedContent, + // since this is preventing someone from creating a property named 'Path' and have it + // in a model, for instance. we could move them all under one unique property eg + // Infos, so we would do .Infos.SortOrder - just an idea - not going to do it in v8 - /// /// - /// Represents a published content item. + /// Gets the unique identifier of the content item. + /// + int Id { get; } + + /// + /// Gets the name of the content item for the current culture. + /// + string? Name { get; } + + /// + /// Gets the URL segment of the content item for the current culture. + /// + string? UrlSegment { get; } + + /// + /// Gets the sort order of the content item. + /// + int SortOrder { get; } + + /// + /// Gets the tree level of the content item. + /// + int Level { get; } + + /// + /// Gets the tree path of the content item. + /// + string Path { get; } + + /// + /// Gets the identifier of the template to use to render the content item. + /// + int? TemplateId { get; } + + /// + /// Gets the identifier of the user who created the content item. + /// + int CreatorId { get; } + + /// + /// Gets the date the content item was created. + /// + DateTime CreateDate { get; } + + /// + /// Gets the identifier of the user who last updated the content item. + /// + int WriterId { get; } + + /// + /// Gets the date the content item was last updated. /// /// - /// Can be a published document, media or member. + /// For published content items, this is also the date the item was published. + /// + /// This date is always global to the content item, see CultureDate() for the + /// date each culture was published. + /// /// - public interface IPublishedContent : IPublishedElement - { - #region Content + DateTime UpdateDate { get; } - // TODO: IPublishedContent properties colliding with models - // we need to find a way to remove as much clutter as possible from IPublishedContent, - // since this is preventing someone from creating a property named 'Path' and have it - // in a model, for instance. we could move them all under one unique property eg - // Infos, so we would do .Infos.SortOrder - just an idea - not going to do it in v8 + /// + /// Gets available culture infos. + /// + /// + /// + /// Contains only those culture that are available. For a published content, these are + /// the cultures that are published. For a draft content, those that are 'available' ie + /// have a non-empty content name. + /// + /// Does not contain the invariant culture. + /// // fixme? + /// + IReadOnlyDictionary Cultures { get; } - /// - /// Gets the unique identifier of the content item. - /// - int Id { get; } + /// + /// Gets the type of the content item (document, media...). + /// + PublishedItemType ItemType { get; } - /// - /// Gets the name of the content item for the current culture. - /// - string? Name { get; } + /// + /// Gets the parent of the content item. + /// + /// The parent of root content is null. + IPublishedContent? Parent { get; } - /// - /// Gets the URL segment of the content item for the current culture. - /// - string? UrlSegment { get; } + /// + /// Gets a value indicating whether the content is draft. + /// + /// + /// + /// A content is draft when it is the unpublished version of a content, which may + /// have a published version, or not. + /// + /// + /// When retrieving documents from cache in non-preview mode, IsDraft is always false, + /// as only published documents are returned. When retrieving in preview mode, IsDraft can + /// either be true (document is not published, or has been edited, and what is returned + /// is the edited version) or false (document is published, and has not been edited, and + /// what is returned is the published version). + /// + /// + bool IsDraft(string? culture = null); - /// - /// Gets the sort order of the content item. - /// - int SortOrder { get; } + /// + /// Gets a value indicating whether the content is published. + /// + /// + /// A content is published when it has a published version. + /// + /// When retrieving documents from cache in non-preview mode, IsPublished is always + /// true, as only published documents are returned. When retrieving in draft mode, IsPublished + /// can either be true (document has a published version) or false (document has no + /// published version). + /// + /// + /// It is therefore possible for both IsDraft and IsPublished to be true at the same + /// time, meaning that the content is the draft version, and a published version exists. + /// + /// + bool IsPublished(string? culture = null); - /// - /// Gets the tree level of the content item. - /// - int Level { get; } + /// + /// Gets the children of the content item that are available for the current culture. + /// + IEnumerable? Children { get; } - /// - /// Gets the tree path of the content item. - /// - string Path { get; } - - /// - /// Gets the identifier of the template to use to render the content item. - /// - int? TemplateId { get; } - - /// - /// Gets the identifier of the user who created the content item. - /// - int CreatorId { get; } - - /// - /// Gets the date the content item was created. - /// - DateTime CreateDate { get; } - - /// - /// Gets the identifier of the user who last updated the content item. - /// - int WriterId { get; } - - /// - /// Gets the date the content item was last updated. - /// - /// - /// For published content items, this is also the date the item was published. - /// This date is always global to the content item, see CultureDate() for the - /// date each culture was published. - /// - DateTime UpdateDate { get; } - - /// - /// Gets available culture infos. - /// - /// - /// Contains only those culture that are available. For a published content, these are - /// the cultures that are published. For a draft content, those that are 'available' ie - /// have a non-empty content name. - /// Does not contain the invariant culture. // fixme? - /// - IReadOnlyDictionary Cultures { get; } - - /// - /// Gets the type of the content item (document, media...). - /// - PublishedItemType ItemType { get; } - - /// - /// Gets a value indicating whether the content is draft. - /// - /// - /// A content is draft when it is the unpublished version of a content, which may - /// have a published version, or not. - /// When retrieving documents from cache in non-preview mode, IsDraft is always false, - /// as only published documents are returned. When retrieving in preview mode, IsDraft can - /// either be true (document is not published, or has been edited, and what is returned - /// is the edited version) or false (document is published, and has not been edited, and - /// what is returned is the published version). - /// - bool IsDraft(string? culture = null); - - /// - /// Gets a value indicating whether the content is published. - /// - /// - /// A content is published when it has a published version. - /// When retrieving documents from cache in non-preview mode, IsPublished is always - /// true, as only published documents are returned. When retrieving in draft mode, IsPublished - /// can either be true (document has a published version) or false (document has no - /// published version). - /// It is therefore possible for both IsDraft and IsPublished to be true at the same - /// time, meaning that the content is the draft version, and a published version exists. - /// - bool IsPublished(string? culture = null); - - #endregion - - #region Tree - - /// - /// Gets the parent of the content item. - /// - /// The parent of root content is null. - IPublishedContent? Parent { get; } - - /// - /// Gets the children of the content item that are available for the current culture. - /// - IEnumerable? Children { get; } - - /// - /// Gets all the children of the content item, regardless of whether they are available for the current culture. - /// - IEnumerable? ChildrenForAllCultures { get; } - - #endregion - } + /// + /// Gets all the children of the content item, regardless of whether they are available for the current culture. + /// + IEnumerable? ChildrenForAllCultures { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs index bd3f77152d..5ce8bef875 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs @@ -1,69 +1,67 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// Represents an type. +/// +/// +/// Instances implementing the interface should be +/// immutable, ie if the content type changes, then a new instance needs to be created. +/// +public interface IPublishedContentType { /// - /// Represents an type. + /// Gets the unique key for the content type. /// - /// Instances implementing the interface should be - /// immutable, ie if the content type changes, then a new instance needs to be created. - public interface IPublishedContentType - { - /// - /// Gets the unique key for the content type. - /// - Guid Key { get; } + Guid Key { get; } - /// - /// Gets the content type identifier. - /// - int Id { get; } + /// + /// Gets the content type identifier. + /// + int Id { get; } - /// - /// Gets the content type alias. - /// - string Alias { get; } + /// + /// Gets the content type alias. + /// + string Alias { get; } - /// - /// Gets the content item type. - /// - PublishedItemType ItemType { get; } + /// + /// Gets the content item type. + /// + PublishedItemType ItemType { get; } - /// - /// Gets the aliases of the content types participating in the composition. - /// - HashSet CompositionAliases { get; } + /// + /// Gets the aliases of the content types participating in the composition. + /// + HashSet CompositionAliases { get; } - /// - /// Gets the content variations of the content type. - /// - ContentVariation Variations { get; } + /// + /// Gets the content variations of the content type. + /// + ContentVariation Variations { get; } - /// - /// Gets a value indicating whether this content type is for an element. - /// - bool IsElement { get; } + /// + /// Gets a value indicating whether this content type is for an element. + /// + bool IsElement { get; } - /// - /// Gets the content type properties. - /// - IEnumerable PropertyTypes { get; } + /// + /// Gets the content type properties. + /// + IEnumerable PropertyTypes { get; } - /// - /// Gets a property type index. - /// - /// The alias is case-insensitive. This is the only place where alias strings are compared. - int GetPropertyIndex(string alias); + /// + /// Gets a property type index. + /// + /// The alias is case-insensitive. This is the only place where alias strings are compared. + int GetPropertyIndex(string alias); - /// - /// Gets a property type. - /// - IPublishedPropertyType? GetPropertyType(string alias); + /// + /// Gets a property type. + /// + IPublishedPropertyType? GetPropertyType(string alias); - /// - /// Gets a property type. - /// - IPublishedPropertyType? GetPropertyType(int index); - } + /// + /// Gets a property type. + /// + IPublishedPropertyType? GetPropertyType(int index); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index b1a1740b31..09e9a00389 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -1,58 +1,64 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Creates published content types. +/// +public interface IPublishedContentTypeFactory { + /// + /// Creates a published content type. + /// + /// An content type. + /// A published content type corresponding to the item type and content type. + IPublishedContentType CreateContentType(IContentTypeComposition contentType); /// - /// Creates published content types. + /// Creates a published property type. /// - public interface IPublishedContentTypeFactory - { - /// - /// Creates a published content type. - /// - /// An content type. - /// A published content type corresponding to the item type and content type. - IPublishedContentType CreateContentType(IContentTypeComposition contentType); + /// The published content type owning the property. + /// A property type. + /// Is used by constructor to create property types. + IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType); - /// - /// Creates a published property type. - /// - /// The published content type owning the property. - /// A property type. - /// Is used by constructor to create property types. - IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType); + /// + /// Creates a published property type. + /// + /// The published content type owning the property. + /// The property type alias. + /// The datatype identifier. + /// The variations. + /// Is used by constructor to create special property types. + IPublishedPropertyType CreatePropertyType( + IPublishedContentType contentType, + string propertyTypeAlias, + int dataTypeId, + ContentVariation variations); - /// - /// Creates a published property type. - /// - /// The published content type owning the property. - /// The property type alias. - /// The datatype identifier. - /// The variations. - /// Is used by constructor to create special property types. - IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + /// + /// Creates a core (non-user) published property type. + /// + /// The published content type owning the property. + /// The property type alias. + /// The datatype identifier. + /// The variations. + /// Is used by constructor to create special property types. + IPublishedPropertyType CreateCorePropertyType( + IPublishedContentType contentType, + string propertyTypeAlias, + int dataTypeId, + ContentVariation variations); - /// - /// Creates a core (non-user) published property type. - /// - /// The published content type owning the property. - /// The property type alias. - /// The datatype identifier. - /// The variations. - /// Is used by constructor to create special property types. - IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + /// + /// Gets a published datatype. + /// + PublishedDataType GetDataType(int id); - /// - /// Gets a published datatype. - /// - PublishedDataType GetDataType(int id); - - /// - /// Notifies the factory of datatype changes. - /// - /// - /// This is so the factory can flush its caches. - /// Invoked by the IPublishedSnapshotService. - /// - void NotifyDataTypeChanges(int[] ids); - } + /// + /// Notifies the factory of datatype changes. + /// + /// + /// This is so the factory can flush its caches. + /// Invoked by the IPublishedSnapshotService. + /// + void NotifyDataTypeChanges(int[] ids); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs index 767d3eadc0..a198064137 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs @@ -1,52 +1,50 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// Represents a published element. +/// +public interface IPublishedElement { + #region ContentType + /// - /// Represents a published element. + /// Gets the content type. /// - public interface IPublishedElement - { - #region ContentType + IPublishedContentType ContentType { get; } - /// - /// Gets the content type. - /// - IPublishedContentType ContentType { get; } + #endregion - #endregion + #region PublishedElement - #region PublishedElement + /// + /// Gets the unique key of the published element. + /// + Guid Key { get; } - /// - /// Gets the unique key of the published element. - /// - Guid Key { get; } + #endregion - #endregion + #region Properties - #region Properties + /// + /// Gets the properties of the element. + /// + /// + /// Contains one IPublishedProperty for each property defined for the content type, including + /// inherited properties. Some properties may have no value. + /// + IEnumerable Properties { get; } - /// - /// Gets the properties of the element. - /// - /// Contains one IPublishedProperty for each property defined for the content type, including - /// inherited properties. Some properties may have no value. - IEnumerable Properties { get; } + /// + /// Gets a property identified by its alias. + /// + /// The property alias. + /// The property identified by the alias. + /// + /// If the content type has no property with that alias, including inherited properties, returns null, + /// otherwise return a property -- that may have no value (ie HasValue is false). + /// The alias is case-insensitive. + /// + IPublishedProperty? GetProperty(string alias); - /// - /// Gets a property identified by its alias. - /// - /// The property alias. - /// The property identified by the alias. - /// - /// If the content type has no property with that alias, including inherited properties, returns null, - /// otherwise return a property -- that may have no value (ie HasValue is false). - /// The alias is case-insensitive. - /// - IPublishedProperty? GetProperty(string alias); - - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs index ba8bdc43d4..cefb51241e 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs @@ -1,31 +1,29 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IPublishedMemberCache { - public interface IPublishedMemberCache - { - /// - /// Get an from an - /// - /// - /// - IPublishedContent? Get(IMember member); + /// + /// Get an from an + /// + /// + /// + IPublishedContent? Get(IMember member); - /// - /// Gets a content type identified by its unique identifier. - /// - /// The content type unique identifier. - /// The content type, or null. - IPublishedContentType GetContentType(int id); + /// + /// Gets a content type identified by its unique identifier. + /// + /// The content type unique identifier. + /// The content type, or null. + IPublishedContentType GetContentType(int id); - /// - /// Gets a content type identified by its alias. - /// - /// The content type alias. - /// The content type, or null. - /// The alias is case-insensitive. - IPublishedContentType GetContentType(string alias); - } + /// + /// Gets a content type identified by its alias. + /// + /// The content type alias. + /// The content type, or null. + /// The alias is case-insensitive. + IPublishedContentType GetContentType(string alias); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs index c34a4a6ba4..03485f0b6c 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs @@ -1,50 +1,49 @@ using System.Collections; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides the published model creation service. +/// +public interface IPublishedModelFactory { /// - /// Provides the published model creation service. + /// Creates a strongly-typed model representing a published element. /// - public interface IPublishedModelFactory - { - /// - /// Creates a strongly-typed model representing a published element. - /// - /// The original published element. - /// - /// The strongly-typed model representing the published element, - /// or the published element itself it the factory has no model for the corresponding element type. - /// - IPublishedElement CreateModel(IPublishedElement element); + /// The original published element. + /// + /// The strongly-typed model representing the published element, + /// or the published element itself it the factory has no model for the corresponding element type. + /// + IPublishedElement CreateModel(IPublishedElement element); - /// - /// Creates a List{T} of a strongly-typed model for a model type alias. - /// - /// The model type alias. - /// - /// A List{T} of the strongly-typed model, exposed as an IList. - /// - IList? CreateModelList(string? alias); + /// + /// Creates a List{T} of a strongly-typed model for a model type alias. + /// + /// The model type alias. + /// + /// A List{T} of the strongly-typed model, exposed as an IList. + /// + IList? CreateModelList(string? alias); - /// - /// Gets the Type of a strongly-typed model for a model type alias. - /// - /// The model type alias. - /// - /// The type of the strongly-typed model. - /// - Type GetModelType(string? alias); + /// + /// Gets the Type of a strongly-typed model for a model type alias. + /// + /// The model type alias. + /// + /// The type of the strongly-typed model. + /// + Type GetModelType(string? alias); - /// - /// Maps a CLR type that may contain model types, to an actual CLR type. - /// - /// The CLR type. - /// - /// The actual CLR type. - /// - /// - /// See for more details. - /// - Type MapModelType(Type type); - } + /// + /// Maps a CLR type that may contain model types, to an actual CLR type. + /// + /// The CLR type. + /// + /// The actual CLR type. + /// + /// + /// See for more details. + /// + Type MapModelType(Type type); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs index 804d0972da..b030f145fd 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs @@ -1,62 +1,73 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a property of an IPublishedElement. +/// +public interface IPublishedProperty { + IPublishedPropertyType PropertyType { get; } + /// - /// Represents a property of an IPublishedElement. + /// Gets the alias of the property. /// - public interface IPublishedProperty - { - IPublishedPropertyType PropertyType { get; } + string Alias { get; } - /// - /// Gets the alias of the property. - /// - string Alias { get; } + /// + /// Gets a value indicating whether the property has a value. + /// + /// + /// + /// This is somewhat implementation-dependent -- depending on whatever IPublishedCache considers + /// a missing value. + /// + /// + /// The XmlPublishedCache raw values are strings, and it will consider missing, null or empty (and + /// that includes whitespace-only) strings as "no value". + /// + /// + /// Other caches that get their raw value from the database would consider that a property has "no + /// value" if it is missing, null, or an empty string (including whitespace-only). + /// + /// + bool HasValue(string? culture = null, string? segment = null); - /// - /// Gets a value indicating whether the property has a value. - /// - /// - /// This is somewhat implementation-dependent -- depending on whatever IPublishedCache considers - /// a missing value. - /// The XmlPublishedCache raw values are strings, and it will consider missing, null or empty (and - /// that includes whitespace-only) strings as "no value". - /// Other caches that get their raw value from the database would consider that a property has "no - /// value" if it is missing, null, or an empty string (including whitespace-only). - /// - bool HasValue(string? culture = null, string? segment = null); + /// + /// Gets the source value of the property. + /// + /// + /// + /// The source value is whatever was passed to the property when it was instantiated, and it is + /// somewhat implementation-dependent -- depending on how the IPublishedCache is implemented. + /// + /// The XmlPublishedCache source values are strings exclusively since they come from the Xml cache. + /// + /// For other caches that get their source value from the database, it would be either a string, + /// an integer (Int32), a date and time (DateTime) or a decimal (double). + /// + /// + /// If you're using that value, you're probably wrong, unless you're doing some internal + /// Umbraco stuff. + /// + /// + object? GetSourceValue(string? culture = null, string? segment = null); - /// - /// Gets the source value of the property. - /// - /// - /// The source value is whatever was passed to the property when it was instantiated, and it is - /// somewhat implementation-dependent -- depending on how the IPublishedCache is implemented. - /// The XmlPublishedCache source values are strings exclusively since they come from the Xml cache. - /// For other caches that get their source value from the database, it would be either a string, - /// an integer (Int32), a date and time (DateTime) or a decimal (double). - /// If you're using that value, you're probably wrong, unless you're doing some internal - /// Umbraco stuff. - /// - object? GetSourceValue(string? culture = null, string? segment = null); + /// + /// Gets the object value of the property. + /// + /// + /// The value is what you want to use when rendering content in an MVC view ie in C#. + /// It can be null, or any type of CLR object. + /// It has been fully prepared and processed by the appropriate converter. + /// + object? GetValue(string? culture = null, string? segment = null); - /// - /// Gets the object value of the property. - /// - /// - /// The value is what you want to use when rendering content in an MVC view ie in C#. - /// It can be null, or any type of CLR object. - /// It has been fully prepared and processed by the appropriate converter. - /// - object? GetValue(string? culture = null, string? segment = null); - - /// - /// Gets the XPath value of the property. - /// - /// - /// The XPath value is what you want to use when navigating content via XPath eg in the XSLT engine. - /// It must be either null, or a string, or an XPathNavigator. - /// It has been fully prepared and processed by the appropriate converter. - /// - object? GetXPathValue(string? culture = null, string? segment = null); - } + /// + /// Gets the XPath value of the property. + /// + /// + /// The XPath value is what you want to use when navigating content via XPath eg in the XSLT engine. + /// It must be either null, or a string, or an XPathNavigator. + /// It has been fully prepared and processed by the appropriate converter. + /// + object? GetXPathValue(string? culture = null, string? segment = null); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs index 3ab21d15f6..3caaee9a37 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs @@ -1,108 +1,112 @@ -using System; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a published property type. +/// +/// +/// Instances implementing the interface should be +/// immutable, ie if the property type changes, then a new instance needs to be created. +/// +public interface IPublishedPropertyType { /// - /// Represents a published property type. + /// Gets the published content type containing the property type. /// - /// Instances implementing the interface should be - /// immutable, ie if the property type changes, then a new instance needs to be created. - public interface IPublishedPropertyType - { - /// - /// Gets the published content type containing the property type. - /// - IPublishedContentType? ContentType { get; } + IPublishedContentType? ContentType { get; } - /// - /// Gets the data type. - /// - PublishedDataType DataType { get; } + /// + /// Gets the data type. + /// + PublishedDataType DataType { get; } - /// - /// Gets property type alias. - /// - string Alias { get; } + /// + /// Gets property type alias. + /// + string Alias { get; } - /// - /// Gets the property editor alias. - /// - string EditorAlias { get; } + /// + /// Gets the property editor alias. + /// + string EditorAlias { get; } - /// - /// Gets a value indicating whether the property is a user content property. - /// - /// A non-user content property is a property that has been added to a - /// published content type by Umbraco but does not corresponds to a user-defined - /// published property. - bool IsUserProperty { get; } + /// + /// Gets a value indicating whether the property is a user content property. + /// + /// + /// A non-user content property is a property that has been added to a + /// published content type by Umbraco but does not corresponds to a user-defined + /// published property. + /// + bool IsUserProperty { get; } - /// - /// Gets the content variations of the property type. - /// - ContentVariation Variations { get; } + /// + /// Gets the content variations of the property type. + /// + ContentVariation Variations { get; } - /// - /// Determines whether a value is an actual value, or not a value. - /// - /// Used by property.HasValue and, for instance, in fallback scenarios. - bool? IsValue(object? value, PropertyValueLevel level); + /// + /// Gets the property cache level. + /// + PropertyCacheLevel CacheLevel { get; } - /// - /// Gets the property cache level. - /// - PropertyCacheLevel CacheLevel { get; } + /// + /// Gets the property model CLR type. + /// + /// + /// The model CLR type may be a type, or may contain types. + /// For the actual CLR type, see . + /// + Type ModelClrType { get; } - /// - /// Converts the source value into the intermediate value. - /// - /// The published element owning the property. - /// The source value. - /// A value indicating whether content should be considered draft. - /// The intermediate value. - object? ConvertSourceToInter(IPublishedElement owner, object? source, bool preview); + /// + /// Gets the property CLR type. + /// + /// + /// Returns the actual CLR type which does not contain types. + /// + /// Mapping from may throw if some instances + /// could not be mapped to actual CLR types. + /// + /// + Type? ClrType { get; } - /// - /// Converts the intermediate value into the object value. - /// - /// The published element owning the property. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether content should be considered draft. - /// The object value. - object? ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); + /// + /// Determines whether a value is an actual value, or not a value. + /// + /// Used by property.HasValue and, for instance, in fallback scenarios. + bool? IsValue(object? value, PropertyValueLevel level); - /// - /// Converts the intermediate value into the XPath value. - /// - /// The published element owning the property. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether content should be considered draft. - /// The XPath value. - /// - /// The XPath value can be either a string or an XPathNavigator. - /// - object? ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); + /// + /// Converts the source value into the intermediate value. + /// + /// The published element owning the property. + /// The source value. + /// A value indicating whether content should be considered draft. + /// The intermediate value. + object? ConvertSourceToInter(IPublishedElement owner, object? source, bool preview); - /// - /// Gets the property model CLR type. - /// - /// - /// The model CLR type may be a type, or may contain types. - /// For the actual CLR type, see . - /// - Type ModelClrType { get; } + /// + /// Converts the intermediate value into the object value. + /// + /// The published element owning the property. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether content should be considered draft. + /// The object value. + object? ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); - /// - /// Gets the property CLR type. - /// - /// - /// Returns the actual CLR type which does not contain types. - /// Mapping from may throw if some instances - /// could not be mapped to actual CLR types. - /// - Type? ClrType { get; } - } + /// + /// Converts the intermediate value into the XPath value. + /// + /// The published element owning the property. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether content should be considered draft. + /// The XPath value. + /// + /// The XPath value can be either a string or an XPathNavigator. + /// + object? ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index c1ecf1909a..729f7dd6bc 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -1,132 +1,173 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a fallback strategy for getting values. +/// +public interface IPublishedValueFallback { /// - /// Provides a fallback strategy for getting values. + /// Tries to get a fallback value for a property. /// - public interface IPublishedValueFallback - { - /// - /// Tries to get a fallback value for a property. - /// - /// The property. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and - /// property.HasValue(culture, segment) is false. - /// It can only fallback at property level (no recurse). - /// At property level, property.GetValue() does *not* implement fallback, and one has to - /// get property.Value() or property.Value{T}() to trigger fallback. - /// Note that and may not be contextualized, - /// so the variant context should be used to contextualize them (see our default implementation in - /// the web project. - /// - bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value); + /// The property. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// + /// It can only fallback at property level (no recurse). + /// + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// + /// + /// Note that and may not be contextualized, + /// so the variant context should be used to contextualize them (see our default implementation in + /// the web project. + /// + /// + bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value); - /// - /// Tries to get a fallback value for a property. - /// - /// The type of the value. - /// The property. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and - /// property.HasValue(culture, segment) is false. - /// It can only fallback at property level (no recurse). - /// At property level, property.GetValue() does *not* implement fallback, and one has to - /// get property.Value() or property.Value{T}() to trigger fallback. - /// - bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value); + /// + /// Tries to get a fallback value for a property. + /// + /// The type of the value. + /// The property. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// + /// It can only fallback at property level (no recurse). + /// + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// + /// + bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value); - /// - /// Tries to get a fallback value for a published element property. - /// - /// The published element. - /// The property alias. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever getting the property value for the specified alias, culture and - /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. - /// It can only fallback at element level (no recurse). - /// - bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value); + /// + /// Tries to get a fallback value for a published element property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + /// It can only fallback at element level (no recurse). + /// + bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value); - /// - /// Tries to get a fallback value for a published element property. - /// - /// The type of the value. - /// The published element. - /// The property alias. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever getting the property value for the specified alias, culture and - /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. - /// It can only fallback at element level (no recurse). - /// - bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value); + /// + /// Tries to get a fallback value for a published element property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + /// It can only fallback at element level (no recurse). + /// + bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value); - /// - /// Tries to get a fallback value for a published content property. - /// - /// The published element. - /// The property alias. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// The property that does not have a value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever getting the property value for the specified alias, culture and - /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. - /// In an , because walking up the tree is possible, the content itself may not even - /// have a property with the specified alias, but such a property may exist up in the tree. The - /// parameter is used to return a property with no value. That can then be used to invoke a converter and get the - /// converter's interpretation of "no value". - /// - bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty); + /// + /// Tries to get a fallback value for a published content property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// The property that does not have a value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + /// + /// In an , because walking up the tree is possible, the content itself may not + /// even + /// have a property with the specified alias, but such a property may exist up in the tree. The + /// + /// parameter is used to return a property with no value. That can then be used to invoke a converter and get the + /// converter's interpretation of "no value". + /// + /// + bool TryGetValue( + IPublishedContent content, + string alias, + string? culture, + string? segment, + Fallback fallback, + object? defaultValue, + out object? value, + out IPublishedProperty? noValueProperty); - /// - /// Tries to get a fallback value for a published content property. - /// - /// The type of the value. - /// The published element. - /// The property alias. - /// The requested culture. - /// The requested segment. - /// A fallback strategy. - /// An optional default value. - /// The fallback value. - /// The property that does not have a value. - /// A value indicating whether a fallback value could be provided. - /// - /// This method is called whenever getting the property value for the specified alias, culture and - /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. - /// In an , because walking up the tree is possible, the content itself may not even - /// have a property with the specified alias, but such a property may exist up in the tree. The - /// parameter is used to return a property with no value. That can then be used to invoke a converter and get the - /// converter's interpretation of "no value". - /// - bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T defaultValue, out T? value, out IPublishedProperty? noValueProperty); - } + /// + /// Tries to get a fallback value for a published content property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// A fallback strategy. + /// An optional default value. + /// The fallback value. + /// The property that does not have a value. + /// A value indicating whether a fallback value could be provided. + /// + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// + /// + /// In an , because walking up the tree is possible, the content itself may not + /// even + /// have a property with the specified alias, but such a property may exist up in the tree. The + /// + /// parameter is used to return a property with no value. That can then be used to invoke a converter and get the + /// converter's interpretation of "no value". + /// + /// + bool TryGetValue( + IPublishedContent content, + string alias, + string? culture, + string? segment, + Fallback fallback, + T defaultValue, + out T? value, + out IPublishedProperty? noValueProperty); } diff --git a/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs index 83c5f19c9e..a20820d954 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IVariationContextAccessor.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Gives access to the current . +/// +public interface IVariationContextAccessor { /// - /// Gives access to the current . + /// Gets or sets the current . /// - public interface IVariationContextAccessor - { - /// - /// Gets or sets the current . - /// - VariationContext? VariationContext { get; set; } - } + VariationContext? VariationContext { get; set; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IndexedArrayItem.cs b/src/Umbraco.Core/Models/PublishedContent/IndexedArrayItem.cs index 7c7049c026..fe7fe2a474 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IndexedArrayItem.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IndexedArrayItem.cs @@ -1,444 +1,386 @@ -using System.Net; +using System.Net; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents an item in an array that stores its own index and the total count. +/// +/// The type of the content. +public class IndexedArrayItem { /// - /// Represents an item in an array that stores its own index and the total count. + /// Initializes a new instance of the class. /// - /// The type of the content. - public class IndexedArrayItem + /// The content. + /// The index. + public IndexedArrayItem(TContent content, int index) { - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The index. - public IndexedArrayItem(TContent content, int index) - { - Content = content; - Index = index; - } - - /// - /// Gets the content. - /// - /// - /// The content. - /// - public TContent Content { get; } - - /// - /// Gets the index. - /// - /// - /// The index. - /// - public int Index { get; } - - /// - /// Gets the total count. - /// - /// - /// The total count. - /// - public int TotalCount { get; set; } - - /// - /// Determines whether this item is the first. - /// - /// - /// true if this item is the first; otherwise, false. - /// - public bool IsFirst() - { - return Index == 0; - } - - /// - /// If this item is the first, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsFirst(string valueIfTrue) - { - return IsFirst(valueIfTrue, string.Empty); - } - - /// - /// If this item is the first, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsFirst(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsFirst() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is not the first. - /// - /// - /// true if this item is not the first; otherwise, false. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public bool IsNotFirst() - { - return IsFirst() == false; - } - - - /// - /// If this item is not the first, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotFirst(string valueIfTrue) - { - return IsNotFirst(valueIfTrue, string.Empty); - } - - /// - /// If this item is not the first, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotFirst(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsNotFirst() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is at the specified . - /// - /// The index. - /// - /// true if this item is at the specified ; otherwise, false. - /// - public bool IsIndex(int index) - { - return Index == index; - } - - /// - /// If this item is at the specified , the HTML encoded will be returned; otherwise, . - /// - /// The index. - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsIndex(int index, string valueIfTrue) - { - return IsIndex(index, valueIfTrue, string.Empty); - } - - /// - /// If this item is at the specified , the HTML encoded will be returned; otherwise, . - /// - /// The index. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsIndex(int index, string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsIndex(index) ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is at an index that can be divided by the specified . - /// - /// The modulus. - /// - /// true if this item is at an index that can be divided by the specified ; otherwise, false. - /// - public bool IsModZero(int modulus) - { - return Index % modulus == 0; - } - - /// - /// If this item is at an index that can be divided by the specified , the HTML encoded will be returned; otherwise, . - /// - /// The modulus. - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsModZero(int modulus, string valueIfTrue) - { - return IsModZero(modulus, valueIfTrue, string.Empty); - } - - /// - /// If this item is at an index that can be divided by the specified , the HTML encoded will be returned; otherwise, . - /// - /// The modulus. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsModZero(int modulus, string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsModZero(modulus) ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is not at an index that can be divided by the specified . - /// - /// The modulus. - /// - /// true if this item is not at an index that can be divided by the specified ; otherwise, false. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public bool IsNotModZero(int modulus) - { - return IsModZero(modulus) == false; - } - - /// - /// If this item is not at an index that can be divided by the specified , the HTML encoded will be returned; otherwise, . - /// - /// The modulus. - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotModZero(int modulus, string valueIfTrue) - { - return IsNotModZero(modulus, valueIfTrue, string.Empty); - } - - /// - /// If this item is not at an index that can be divided by the specified , the HTML encoded will be returned; otherwise, . - /// - /// The modulus. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotModZero(int modulus, string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsNotModZero(modulus) ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is not at the specified . - /// - /// The index. - /// - /// true if this item is not at the specified ; otherwise, false. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public bool IsNotIndex(int index) - { - return IsIndex(index) == false; - } - - /// - /// If this item is not at the specified , the HTML encoded will be returned; otherwise, . - /// - /// The index. - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotIndex(int index, string valueIfTrue) - { - return IsNotIndex(index, valueIfTrue, string.Empty); - } - - /// - /// If this item is at the specified , the HTML encoded will be returned; otherwise, . - /// - /// The index. - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotIndex(int index, string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsNotIndex(index) ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is the last. - /// - /// - /// true if this item is the last; otherwise, false. - /// - public bool IsLast() - { - return Index == TotalCount - 1; - } - - /// - /// If this item is the last, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsLast(string valueIfTrue) - { - return IsLast(valueIfTrue, string.Empty); - } - - /// - /// If this item is the last, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsLast(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsLast() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is not the last. - /// - /// - /// true if this item is not the last; otherwise, false. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public bool IsNotLast() - { - return IsLast() == false; - } - - /// - /// If this item is not the last, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotLast(string valueIfTrue) - { - return IsNotLast(valueIfTrue, string.Empty); - } - - /// - /// If this item is not the last, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsNotLast(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsNotLast() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is at an even index. - /// - /// - /// true if this item is at an even index; otherwise, false. - /// - public bool IsEven() - { - return Index % 2 == 0; - } - - /// - /// If this item is at an even index, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsEven(string valueIfTrue) - { - return IsEven(valueIfTrue, string.Empty); - } - - /// - /// If this item is at an even index, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsEven(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsEven() ? valueIfTrue : valueIfFalse)); - } - - /// - /// Determines whether this item is at an odd index. - /// - /// - /// true if this item is at an odd index; otherwise, false. - /// - public bool IsOdd() - { - return Index % 2 == 1; - } - - /// - /// If this item is at an odd index, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsOdd(string valueIfTrue) - { - return IsOdd(valueIfTrue, string.Empty); - } - - /// - /// If this item is at an odd index, the HTML encoded will be returned; otherwise, . - /// - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - // TODO: This method should be removed or moved to an extension method on HtmlHelper. - public IHtmlEncodedString IsOdd(string valueIfTrue, string valueIfFalse) - { - return new HtmlEncodedString(WebUtility.HtmlEncode(IsOdd() ? valueIfTrue : valueIfFalse)); - } + Content = content; + Index = index; } + + /// + /// Gets the content. + /// + /// + /// The content. + /// + public TContent Content { get; } + + /// + /// Gets the index. + /// + /// + /// The index. + /// + public int Index { get; } + + /// + /// Gets the total count. + /// + /// + /// The total count. + /// + public int TotalCount { get; set; } + + /// + /// Determines whether this item is the first. + /// + /// + /// true if this item is the first; otherwise, false. + /// + public bool IsFirst() => Index == 0; + + /// + /// If this item is the first, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsFirst(string valueIfTrue) => IsFirst(valueIfTrue, string.Empty); + + /// + /// If this item is the first, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsFirst(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsFirst() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is not the first. + /// + /// + /// true if this item is not the first; otherwise, false. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public bool IsNotFirst() => IsFirst() == false; + + /// + /// If this item is not the first, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotFirst(string valueIfTrue) => IsNotFirst(valueIfTrue, string.Empty); + + /// + /// If this item is not the first, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotFirst(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsNotFirst() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is at the specified . + /// + /// The index. + /// + /// true if this item is at the specified ; otherwise, false. + /// + public bool IsIndex(int index) => Index == index; + + /// + /// If this item is at the specified , the HTML encoded will + /// be returned; otherwise, . + /// + /// The index. + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsIndex(int index, string valueIfTrue) => IsIndex(index, valueIfTrue, string.Empty); + + /// + /// If this item is at the specified , the HTML encoded will + /// be returned; otherwise, . + /// + /// The index. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsIndex(int index, string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsIndex(index) ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is at an index that can be divided by the specified . + /// + /// The modulus. + /// + /// true if this item is at an index that can be divided by the specified ; + /// otherwise, false. + /// + public bool IsModZero(int modulus) => Index % modulus == 0; + + /// + /// If this item is at an index that can be divided by the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The modulus. + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsModZero(int modulus, string valueIfTrue) => + IsModZero(modulus, valueIfTrue, string.Empty); + + /// + /// If this item is at an index that can be divided by the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The modulus. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsModZero(int modulus, string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsModZero(modulus) ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is not at an index that can be divided by the specified . + /// + /// The modulus. + /// + /// true if this item is not at an index that can be divided by the specified ; + /// otherwise, false. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public bool IsNotModZero(int modulus) => IsModZero(modulus) == false; + + /// + /// If this item is not at an index that can be divided by the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The modulus. + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotModZero(int modulus, string valueIfTrue) => + IsNotModZero(modulus, valueIfTrue, string.Empty); + + /// + /// If this item is not at an index that can be divided by the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The modulus. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotModZero(int modulus, string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsNotModZero(modulus) ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is not at the specified . + /// + /// The index. + /// + /// true if this item is not at the specified ; otherwise, false. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public bool IsNotIndex(int index) => IsIndex(index) == false; + + /// + /// If this item is not at the specified , the HTML encoded + /// will be returned; otherwise, . + /// + /// The index. + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotIndex(int index, string valueIfTrue) => IsNotIndex(index, valueIfTrue, string.Empty); + + /// + /// If this item is at the specified , the HTML encoded will + /// be returned; otherwise, . + /// + /// The index. + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotIndex(int index, string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsNotIndex(index) ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is the last. + /// + /// + /// true if this item is the last; otherwise, false. + /// + public bool IsLast() => Index == TotalCount - 1; + + /// + /// If this item is the last, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsLast(string valueIfTrue) => IsLast(valueIfTrue, string.Empty); + + /// + /// If this item is the last, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsLast(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsLast() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is not the last. + /// + /// + /// true if this item is not the last; otherwise, false. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public bool IsNotLast() => IsLast() == false; + + /// + /// If this item is not the last, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotLast(string valueIfTrue) => IsNotLast(valueIfTrue, string.Empty); + + /// + /// If this item is not the last, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsNotLast(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsNotLast() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is at an even index. + /// + /// + /// true if this item is at an even index; otherwise, false. + /// + public bool IsEven() => Index % 2 == 0; + + /// + /// If this item is at an even index, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsEven(string valueIfTrue) => IsEven(valueIfTrue, string.Empty); + + /// + /// If this item is at an even index, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsEven(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsEven() ? valueIfTrue : valueIfFalse)); + + /// + /// Determines whether this item is at an odd index. + /// + /// + /// true if this item is at an odd index; otherwise, false. + /// + public bool IsOdd() => Index % 2 == 1; + + /// + /// If this item is at an odd index, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsOdd(string valueIfTrue) => IsOdd(valueIfTrue, string.Empty); + + /// + /// If this item is at an odd index, the HTML encoded will be returned; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + // TODO: This method should be removed or moved to an extension method on HtmlHelper. + public IHtmlEncodedString IsOdd(string valueIfTrue, string valueIfFalse) => + new HtmlEncodedString(WebUtility.HtmlEncode(IsOdd() ? valueIfTrue : valueIfFalse)); } diff --git a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs index 0de838fa0e..4588d47967 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs @@ -1,412 +1,518 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Reflection; using Umbraco.Cms.Core.Exceptions; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// +/// Represents the CLR type of a model. +/// +/// +/// ModelType.For("alias") +/// typeof (IEnumerable{}).MakeGenericType(ModelType.For("alias")) +/// Model.For("alias").MakeArrayType() +/// +public class ModelType : Type { - /// - /// - /// Represents the CLR type of a model. - /// - /// - /// ModelType.For("alias") - /// typeof (IEnumerable{}).MakeGenericType(ModelType.For("alias")) - /// Model.For("alias").MakeArrayType() - /// - public class ModelType : Type + private ModelType(string? contentTypeAlias) { - private ModelType(string? contentTypeAlias) + if (contentTypeAlias == null) { - if (contentTypeAlias == null) throw new ArgumentNullException(nameof(contentTypeAlias)); - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias)); - - ContentTypeAlias = contentTypeAlias; - Name = "{" + ContentTypeAlias + "}"; + throw new ArgumentNullException(nameof(contentTypeAlias)); } - /// - /// Gets the content type alias. - /// - public string ContentTypeAlias { get; } - - /// - public override string ToString() - => Name; - - /// - /// Gets the model type for a published element type. - /// - /// The published element type alias. - /// The model type for the published element type. - public static ModelType For(string? alias) - => new ModelType(alias); - - /// - /// Gets the actual CLR type by replacing model types, if any. - /// - /// The type. - /// The model types map. - /// The actual CLR type. - public static Type Map(Type type, Dictionary? modelTypes) - => Map(type, modelTypes, false); - - public static Type Map(Type type, Dictionary? modelTypes, bool dictionaryIsInvariant) + if (string.IsNullOrWhiteSpace(contentTypeAlias)) { - // it may be that senders forgot to send an invariant dictionary (garbage-in) - if (modelTypes is not null && !dictionaryIsInvariant) - modelTypes = new Dictionary(modelTypes, StringComparer.InvariantCultureIgnoreCase); - - if (type is ModelType modelType) - { - if (modelTypes?.TryGetValue(modelType.ContentTypeAlias, out var actualType) ?? false) - return actualType; - throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); - } - - if (type is ModelTypeArrayType arrayType) - { - if (modelTypes?.TryGetValue(arrayType.ContentTypeAlias, out var actualType) ?? false) - return actualType.MakeArrayType(); - throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); - } - - if (type.IsGenericType == false) - return type; - var def = type.GetGenericTypeDefinition(); - if (def == null) - throw new PanicException($"The type {type} has not generic type definition"); - - var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); - return def.MakeGenericType(args); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(contentTypeAlias)); } - /// - /// Gets the actual CLR type name by replacing model types, if any. - /// - /// The type. - /// The model types map. - /// The actual CLR type name. - public static string MapToName(Type type, Dictionary map) - => MapToName(type, map, false); + ContentTypeAlias = contentTypeAlias; + Name = "{" + ContentTypeAlias + "}"; + } - private static string MapToName(Type type, Dictionary map, bool dictionaryIsInvariant) + /// + /// Gets the content type alias. + /// + public string ContentTypeAlias { get; } + + /// + public override Type UnderlyingSystemType => this; + + /// + public override Type? BaseType => null; + + /// + public override string Name { get; } + + /// + public override Guid GUID { get; } = Guid.NewGuid(); + + /// + public override Module Module => GetType().Module; // hackish but FullName requires something + + /// + public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something + + /// + public override string FullName => Name; + + /// + public override string Namespace => string.Empty; + + /// + public override string AssemblyQualifiedName => Name; + + /// + /// Gets the model type for a published element type. + /// + /// The published element type alias. + /// The model type for the published element type. + public static ModelType For(string? alias) + => new(alias); + + /// + public override string ToString() + => Name; + + /// + /// Gets the actual CLR type by replacing model types, if any. + /// + /// The type. + /// The model types map. + /// The actual CLR type. + public static Type Map(Type type, Dictionary? modelTypes) + => Map(type, modelTypes, false); + + public static Type Map(Type type, Dictionary? modelTypes, bool dictionaryIsInvariant) + { + // it may be that senders forgot to send an invariant dictionary (garbage-in) + if (modelTypes is not null && !dictionaryIsInvariant) { - // it may be that senders forgot to send an invariant dictionary (garbage-in) - if (!dictionaryIsInvariant) - map = new Dictionary(map, StringComparer.InvariantCultureIgnoreCase); - - if (type is ModelType modelType) - { - if (map.TryGetValue(modelType.ContentTypeAlias, out var actualTypeName)) - return actualTypeName; - throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); - } - - if (type is ModelTypeArrayType arrayType) - { - if (map.TryGetValue(arrayType.ContentTypeAlias, out var actualTypeName)) - return actualTypeName + "[]"; - throw new InvalidOperationException($"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); - } - - if (type.IsGenericType == false) - return type.FullName!; - var def = type.GetGenericTypeDefinition(); - if (def == null) - throw new PanicException($"The type {type} has not generic type definition"); - - var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); - var defFullName = def.FullName?.Substring(0, def.FullName.IndexOf('`')); - return defFullName + "<" + string.Join(", ", args) + ">"; + modelTypes = new Dictionary(modelTypes, StringComparer.InvariantCultureIgnoreCase); } - /// - /// Gets a value indicating whether two instances are equal. - /// - /// The first instance. - /// The second instance. - /// A value indicating whether the two instances are equal. - /// Knows how to compare instances. - public static bool Equals(Type t1, Type t2) + if (type is ModelType modelType) { - if (t1 == t2) - return true; - - if (t1 is ModelType m1 && t2 is ModelType m2) - return m1.ContentTypeAlias == m2.ContentTypeAlias; - - if (t1 is ModelTypeArrayType a1 && t2 is ModelTypeArrayType a2) - return a1.ContentTypeAlias == a2.ContentTypeAlias; - - if (t1.IsGenericType == false || t2.IsGenericType == false) - return false; - - var args1 = t1.GetGenericArguments(); - var args2 = t2.GetGenericArguments(); - if (args1.Length != args2.Length) return false; - - for (var i = 0; i < args1.Length; i++) + if (modelTypes?.TryGetValue(modelType.ContentTypeAlias, out Type? actualType) ?? false) { - // ReSharper disable once CheckForReferenceEqualityInstead.2 - if (Equals(args1[i], args2[i]) == false) return false; + return actualType; } + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); + } + + if (type is ModelTypeArrayType arrayType) + { + if (modelTypes?.TryGetValue(arrayType.ContentTypeAlias, out Type? actualType) ?? false) + { + return actualType.MakeArrayType(); + } + + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); + } + + if (type.IsGenericType == false) + { + return type; + } + + Type def = type.GetGenericTypeDefinition(); + if (def == null) + { + throw new PanicException($"The type {type} has not generic type definition"); + } + + Type[] args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); + return def.MakeGenericType(args); + } + + /// + /// Gets the actual CLR type name by replacing model types, if any. + /// + /// The type. + /// The model types map. + /// The actual CLR type name. + public static string MapToName(Type type, Dictionary map) + => MapToName(type, map, false); + + /// + /// Gets a value indicating whether two instances are equal. + /// + /// The first instance. + /// The second instance. + /// A value indicating whether the two instances are equal. + /// Knows how to compare instances. + public static bool Equals(Type t1, Type t2) + { + if (t1 == t2) + { return true; } - /// - protected override TypeAttributes GetAttributeFlagsImpl() - => TypeAttributes.Class; + if (t1 is ModelType m1 && t2 is ModelType m2) + { + return m1.ContentTypeAlias == m2.ContentTypeAlias; + } - /// - public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) - => Array.Empty(); + if (t1 is ModelTypeArrayType a1 && t2 is ModelTypeArrayType a2) + { + return a1.ContentTypeAlias == a2.ContentTypeAlias; + } - /// - protected override ConstructorInfo? GetConstructorImpl(BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[] types, ParameterModifier[]? modifiers) - => null; + if (t1.IsGenericType == false || t2.IsGenericType == false) + { + return false; + } - /// - public override Type[] GetInterfaces() - => Array.Empty(); + Type[] args1 = t1.GetGenericArguments(); + Type[] args2 = t2.GetGenericArguments(); + if (args1.Length != args2.Length) + { + return false; + } - /// - public override Type? GetInterface(string name, bool ignoreCase) - => null; + for (var i = 0; i < args1.Length; i++) + { + // ReSharper disable once CheckForReferenceEqualityInstead.2 + if (Equals(args1[i], args2[i]) == false) + { + return false; + } + } - /// - public override EventInfo[] GetEvents(BindingFlags bindingAttr) - => Array.Empty(); - - /// - public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) - => null; - - /// - public override Type[] GetNestedTypes(BindingFlags bindingAttr) - => Array.Empty(); - - /// - public override Type? GetNestedType(string name, BindingFlags bindingAttr) - => null; - - /// - public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) - => Array.Empty(); - - /// - protected override PropertyInfo? GetPropertyImpl(string name, BindingFlags bindingAttr, Binder? binder, Type? returnType, Type[]? types, ParameterModifier[]? modifiers) - => null; - - /// - public override MethodInfo[] GetMethods(BindingFlags bindingAttr) - => Array.Empty(); - - /// - protected override MethodInfo? GetMethodImpl(string name, BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[]? types, ParameterModifier[]? modifiers) - => null; - - /// - public override FieldInfo[] GetFields(BindingFlags bindingAttr) - => Array.Empty(); - - /// - public override FieldInfo? GetField(string name, BindingFlags bindingAttr) - => null; - - /// - public override MemberInfo[] GetMembers(BindingFlags bindingAttr) - => Array.Empty(); - - /// - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => Array.Empty(); - - /// - public override object[] GetCustomAttributes(bool inherit) - => Array.Empty(); - - /// - public override bool IsDefined(Type attributeType, bool inherit) - => false; - - /// - public override Type? GetElementType() - => null; - - /// - protected override bool HasElementTypeImpl() - => false; - - /// - protected override bool IsArrayImpl() - => false; - - /// - protected override bool IsByRefImpl() - => false; - - /// - protected override bool IsPointerImpl() - => false; - - /// - protected override bool IsPrimitiveImpl() - => false; - - /// - protected override bool IsCOMObjectImpl() - => false; - - /// - public override object InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, object?[]? args, ParameterModifier[]? modifiers, CultureInfo? culture, string[]? namedParameters) - => throw new NotSupportedException(); - - /// - public override Type UnderlyingSystemType => this; - - /// - public override Type? BaseType => null; - - /// - public override string Name { get; } - - /// - public override Guid GUID { get; } = Guid.NewGuid(); - - /// - public override Module Module => GetType().Module; // hackish but FullName requires something - - /// - public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something - - /// - public override string FullName => Name; - - /// - public override string Namespace => string.Empty; - - /// - public override string AssemblyQualifiedName => Name; - - /// - public override Type MakeArrayType() - => new ModelTypeArrayType(this); + return true; } - internal class ModelTypeArrayType : Type + /// + public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override Type[] GetInterfaces() + => Array.Empty(); + + private static string MapToName(Type type, Dictionary map, bool dictionaryIsInvariant) { - private readonly Type _elementType; - - public ModelTypeArrayType(ModelType type) + // it may be that senders forgot to send an invariant dictionary (garbage-in) + if (!dictionaryIsInvariant) { - _elementType = type; - ContentTypeAlias = type.ContentTypeAlias; - Name = "{" + type.ContentTypeAlias + "}[*]"; + map = new Dictionary(map, StringComparer.InvariantCultureIgnoreCase); } - public string ContentTypeAlias { get; } - - public override string ToString() - => Name; - - protected override TypeAttributes GetAttributeFlagsImpl() - => TypeAttributes.Class; - - public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) - => Array.Empty(); - - protected override ConstructorInfo? GetConstructorImpl(BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[] types, ParameterModifier[]? modifiers) - => null; - - public override Type[] GetInterfaces() - => Array.Empty(); - - public override Type? GetInterface(string name, bool ignoreCase) - => null; - - public override EventInfo[] GetEvents(BindingFlags bindingAttr) - => Array.Empty(); - - public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) - => null; - - public override Type[] GetNestedTypes(BindingFlags bindingAttr) - => Array.Empty(); - - public override Type? GetNestedType(string name, BindingFlags bindingAttr) - => null; - - public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) - => Array.Empty(); - - protected override PropertyInfo? GetPropertyImpl(string name, BindingFlags bindingAttr, Binder? binder, Type? returnType, Type[]? types, ParameterModifier[]? modifiers) - => null; - - public override MethodInfo[] GetMethods(BindingFlags bindingAttr) - => Array.Empty(); - - protected override MethodInfo? GetMethodImpl(string name, BindingFlags bindingAttr, Binder? binder, CallingConventions callConvention, Type[]? types, ParameterModifier[]? modifiers) - => null; - - public override FieldInfo[] GetFields(BindingFlags bindingAttr) - => Array.Empty(); - - public override FieldInfo? GetField(string name, BindingFlags bindingAttr) - => null; - - public override MemberInfo[] GetMembers(BindingFlags bindingAttr) - => Array.Empty(); - - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => Array.Empty(); - - public override object[] GetCustomAttributes(bool inherit) - => Array.Empty(); - - public override bool IsDefined(Type attributeType, bool inherit) - => false; - - public override Type GetElementType() - => _elementType; - - protected override bool HasElementTypeImpl() - => true; - - protected override bool IsArrayImpl() - => true; - - protected override bool IsByRefImpl() - => false; - - protected override bool IsPointerImpl() - => false; - - protected override bool IsPrimitiveImpl() - => false; - - protected override bool IsCOMObjectImpl() - => false; - public override object InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, object?[]? args, ParameterModifier[]? modifiers, CultureInfo? culture, string[]? namedParameters) + if (type is ModelType modelType) { - throw new NotSupportedException(); + if (map.TryGetValue(modelType.ContentTypeAlias, out var actualTypeName)) + { + return actualTypeName; + } + + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{modelType.ContentTypeAlias}\"."); } - public override Type UnderlyingSystemType => this; - public override Type? BaseType => null; + if (type is ModelTypeArrayType arrayType) + { + if (map.TryGetValue(arrayType.ContentTypeAlias, out var actualTypeName)) + { + return actualTypeName + "[]"; + } - public override string Name { get; } - public override Guid GUID { get; } = Guid.NewGuid(); - public override Module Module =>GetType().Module; // hackish but FullName requires something - public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something - public override string FullName => Name; - public override string Namespace => string.Empty; - public override string AssemblyQualifiedName => Name; + throw new InvalidOperationException( + $"Don't know how to map ModelType with content type alias \"{arrayType.ContentTypeAlias}\"."); + } - public override int GetArrayRank() - => 1; + if (type.IsGenericType == false) + { + return type.FullName!; + } + + Type def = type.GetGenericTypeDefinition(); + if (def == null) + { + throw new PanicException($"The type {type} has not generic type definition"); + } + + var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); + var defFullName = def.FullName?[..def.FullName.IndexOf('`')]; + return defFullName + "<" + string.Join(", ", args) + ">"; } + + /// + protected override TypeAttributes GetAttributeFlagsImpl() + => TypeAttributes.Class; + + /// + protected override ConstructorInfo? GetConstructorImpl( + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[] types, + ParameterModifier[]? modifiers) + => null; + + /// + public override Type? GetInterface(string name, bool ignoreCase) + => null; + + /// + public override EventInfo[] GetEvents(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) + => null; + + /// + public override Type[] GetNestedTypes(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override Type? GetNestedType(string name, BindingFlags bindingAttr) + => null; + + /// + public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override MethodInfo[] GetMethods(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override FieldInfo[] GetFields(BindingFlags bindingAttr) + => Array.Empty(); + + /// + protected override PropertyInfo? GetPropertyImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + Type? returnType, + Type[]? types, + ParameterModifier[]? modifiers) + => null; + + /// + protected override MethodInfo? GetMethodImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[]? types, + ParameterModifier[]? modifiers) + => null; + + /// + public override FieldInfo? GetField(string name, BindingFlags bindingAttr) + => null; + + /// + public override MemberInfo[] GetMembers(BindingFlags bindingAttr) + => Array.Empty(); + + /// + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + => Array.Empty(); + + /// + public override object[] GetCustomAttributes(bool inherit) + => Array.Empty(); + + /// + public override bool IsDefined(Type attributeType, bool inherit) + => false; + + /// + public override Type? GetElementType() + => null; + + /// + public override object InvokeMember( + string name, + BindingFlags invokeAttr, + Binder? binder, + object? target, + object?[]? args, + ParameterModifier[]? modifiers, + CultureInfo? culture, + string[]? namedParameters) + => throw new NotSupportedException(); + + /// + protected override bool HasElementTypeImpl() + => false; + + /// + protected override bool IsArrayImpl() + => false; + + /// + protected override bool IsByRefImpl() + => false; + + /// + protected override bool IsPointerImpl() + => false; + + /// + protected override bool IsPrimitiveImpl() + => false; + + /// + protected override bool IsCOMObjectImpl() + => false; + + /// + public override Type MakeArrayType() + => new ModelTypeArrayType(this); +} + +/// +internal class ModelTypeArrayType : Type +{ + private readonly Type _elementType; + + public ModelTypeArrayType(ModelType type) + { + _elementType = type; + ContentTypeAlias = type.ContentTypeAlias; + Name = "{" + type.ContentTypeAlias + "}[*]"; + } + + public string ContentTypeAlias { get; } + + public override Type UnderlyingSystemType => this; + + public override Type? BaseType => null; + + public override string Name { get; } + + public override Guid GUID { get; } = Guid.NewGuid(); + + public override Module Module => GetType().Module; // hackish but FullName requires something + + public override Assembly Assembly => GetType().Assembly; // hackish but FullName requires something + + public override string FullName => Name; + + public override string Namespace => string.Empty; + + public override string AssemblyQualifiedName => Name; + + public override string ToString() + => Name; + + public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) + => Array.Empty(); + + public override Type[] GetInterfaces() + => Array.Empty(); + + protected override TypeAttributes GetAttributeFlagsImpl() + => TypeAttributes.Class; + + protected override ConstructorInfo? GetConstructorImpl( + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[] types, + ParameterModifier[]? modifiers) + => null; + + public override Type? GetInterface(string name, bool ignoreCase) + => null; + + public override EventInfo[] GetEvents(BindingFlags bindingAttr) + => Array.Empty(); + + public override EventInfo? GetEvent(string name, BindingFlags bindingAttr) + => null; + + public override Type[] GetNestedTypes(BindingFlags bindingAttr) + => Array.Empty(); + + public override Type? GetNestedType(string name, BindingFlags bindingAttr) + => null; + + public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) + => Array.Empty(); + + public override MethodInfo[] GetMethods(BindingFlags bindingAttr) + => Array.Empty(); + + public override FieldInfo[] GetFields(BindingFlags bindingAttr) + => Array.Empty(); + + protected override PropertyInfo? GetPropertyImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + Type? returnType, + Type[]? types, + ParameterModifier[]? modifiers) + => null; + + protected override MethodInfo? GetMethodImpl( + string name, + BindingFlags bindingAttr, + Binder? binder, + CallingConventions callConvention, + Type[]? types, + ParameterModifier[]? modifiers) + => null; + + public override FieldInfo? GetField(string name, BindingFlags bindingAttr) + => null; + + public override MemberInfo[] GetMembers(BindingFlags bindingAttr) + => Array.Empty(); + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + => Array.Empty(); + + public override object[] GetCustomAttributes(bool inherit) + => Array.Empty(); + + public override bool IsDefined(Type attributeType, bool inherit) + => false; + + public override Type GetElementType() + => _elementType; + + public override object InvokeMember( + string name, + BindingFlags invokeAttr, + Binder? binder, + object? target, + object?[]? args, + ParameterModifier[]? modifiers, + CultureInfo? culture, + string[]? namedParameters) => + throw new NotSupportedException(); + + protected override bool HasElementTypeImpl() + => true; + + protected override bool IsArrayImpl() + => true; + + protected override bool IsByRefImpl() + => false; + + protected override bool IsPointerImpl() + => false; + + protected override bool IsPrimitiveImpl() + => false; + + protected override bool IsCOMObjectImpl() + => false; + + public override int GetArrayRank() + => 1; } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs index 93b6948edc..5eefd1e12b 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs @@ -1,21 +1,20 @@ using System.Collections; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a no-operation factory. +public class NoopPublishedModelFactory : IPublishedModelFactory { /// - /// Represents a no-operation factory. - public class NoopPublishedModelFactory : IPublishedModelFactory - { - /// - public IPublishedElement CreateModel(IPublishedElement element) => element; + public IPublishedElement CreateModel(IPublishedElement element) => element; - /// - public IList CreateModelList(string? alias) => new List(); + /// + public IList CreateModelList(string? alias) => new List(); - /// - public Type GetModelType(string? alias) => typeof(IPublishedElement); + /// + public Type GetModelType(string? alias) => typeof(IPublishedElement); - /// - public Type MapModelType(Type type) => typeof(IPublishedElement); - } + /// + public Type MapModelType(Type type) => typeof(IPublishedElement); } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index a08a20658d..1dd2fef124 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -1,55 +1,54 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a noop implementation for . +/// +/// +/// This is for tests etc - does not implement fallback at all. +/// +public class NoopPublishedValueFallback : IPublishedValueFallback { - /// - /// Provides a noop implementation for . - /// - /// - /// This is for tests etc - does not implement fallback at all. - /// - public class NoopPublishedValueFallback : IPublishedValueFallback + /// + public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) { - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) - { - value = default; - return false; - } + value = default; + return false; + } - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) - { - value = default; - return false; - } + /// + public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + { + value = default; + return false; + } - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) - { - value = default; - return false; - } + /// + public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) + { + value = default; + return false; + } - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) - { - value = default; - return false; - } + /// + public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + { + value = default; + return false; + } - /// - public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) - { - value = default; - noValueProperty = default; - return false; - } + /// + public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) + { + value = default; + noValueProperty = default; + return false; + } - /// - public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T defaultValue, out T? value, out IPublishedProperty? noValueProperty) - { - value = default; - noValueProperty = default; - return false; - } + /// + public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T defaultValue, out T? value, out IPublishedProperty? noValueProperty) + { + value = default; + noValueProperty = default; + return false; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs index 60d3cd4a02..077b420735 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.PublishedContent @@ -15,26 +13,13 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { private readonly IVariationContextAccessor? _variationContextAccessor; - protected PublishedContentBase(IVariationContextAccessor? variationContextAccessor) - { - _variationContextAccessor = variationContextAccessor; - } - - #region ContentType + protected PublishedContentBase(IVariationContextAccessor? variationContextAccessor) => _variationContextAccessor = variationContextAccessor; public abstract IPublishedContentType ContentType { get; } - #endregion - - #region PublishedElement - /// public abstract Guid Key { get; } - #endregion - - #region PublishedContent - /// public abstract int Id { get; } @@ -80,10 +65,6 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public abstract bool IsPublished(string? culture = null); - #endregion - - #region Tree - /// public abstract IPublishedContent? Parent { get; } @@ -93,16 +74,10 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public abstract IEnumerable ChildrenForAllCultures { get; } - #endregion - - #region Properties - /// public abstract IEnumerable Properties { get; } /// public abstract IPublishedProperty? GetProperty(string alias); - - #endregion } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs index f1d348d2ff..2b123a33a9 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs @@ -1,35 +1,47 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides strongly typed published content models services. +/// +public static class PublishedContentExtensionsForModels { /// - /// Provides strongly typed published content models services. + /// Creates a strongly typed published content model for an internal published content. /// - public static class PublishedContentExtensionsForModels + /// The internal published content. + /// The published model factory + /// The strongly typed published content model. + public static IPublishedContent? CreateModel( + this IPublishedContent? content, + IPublishedModelFactory? publishedModelFactory) { - /// - /// Creates a strongly typed published content model for an internal published content. - /// - /// The internal published content. - /// The strongly typed published content model. - public static IPublishedContent? CreateModel(this IPublishedContent content, IPublishedModelFactory? publishedModelFactory) + if (publishedModelFactory == null) { - if (publishedModelFactory == null) throw new ArgumentNullException(nameof(publishedModelFactory)); - if (content == null) - return null; - - // get model - // if factory returns nothing, throw - var model = publishedModelFactory.CreateModel(content); - if (model == null) - throw new InvalidOperationException("Factory returned null."); - - // if factory returns a different type, throw - if (!(model is IPublishedContent publishedContent)) - throw new InvalidOperationException($"Factory returned model of type {model.GetType().FullName} which does not implement IPublishedContent."); - - return publishedContent; + throw new ArgumentNullException(nameof(publishedModelFactory)); } + + if (content == null) + { + return null; + } + + // get model + // if factory returns nothing, throw + IPublishedElement model = publishedModelFactory.CreateModel(content); + if (model == null) + { + throw new InvalidOperationException("Factory returned null."); + } + + // if factory returns a different type, throw + if (!(model is IPublishedContent publishedContent)) + { + throw new InvalidOperationException( + $"Factory returned model of type {model.GetType().FullName} which does not implement IPublishedContent."); + } + + return publishedContent; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs index 249c2cb465..cfa648594e 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs @@ -1,21 +1,22 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a strongly-typed published content. +/// +/// +/// Every strongly-typed published content class should inherit from PublishedContentModel +/// (or inherit from a class that inherits from... etc.) so they are picked by the factory. +/// +public abstract class PublishedContentModel : PublishedContentWrapped { /// - /// Represents a strongly-typed published content. + /// Initializes a new instance of the class with + /// an original instance. /// - /// Every strongly-typed published content class should inherit from PublishedContentModel - /// (or inherit from a class that inherits from... etc.) so they are picked by the factory. - public abstract class PublishedContentModel : PublishedContentWrapped + /// The original content. + /// the PublishedValueFallback + protected PublishedContentModel(IPublishedContent content, IPublishedValueFallback publishedValueFallback) + : base(content, publishedValueFallback) { - /// - /// Initializes a new instance of the class with - /// an original instance. - /// - /// The original content. - protected PublishedContentModel(IPublishedContent content, IPublishedValueFallback publishedValueFallback) - : base(content, publishedValueFallback) - { } - - } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 592c2eff5e..bd5e7af0a4 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.PublishedContent @@ -30,7 +27,9 @@ namespace Umbraco.Cms.Core.Models.PublishedContent .ToList(); if (ItemType == PublishedItemType.Member) + { EnsureMemberProperties(propertyTypes, factory); + } _propertyTypes = propertyTypes.ToArray(); @@ -46,9 +45,12 @@ namespace Umbraco.Cms.Core.Models.PublishedContent public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false) : this(key, id, alias, itemType, compositionAliases, variations, isElement) { - var propertyTypesA = propertyTypes.ToArray(); - foreach (var propertyType in propertyTypesA) + PublishedPropertyType[] propertyTypesA = propertyTypes.ToArray(); + foreach (PublishedPropertyType propertyType in propertyTypesA) + { propertyType.ContentType = this; + } + _propertyTypes = propertyTypesA; InitializeIndexes(); @@ -58,9 +60,12 @@ namespace Umbraco.Cms.Core.Models.PublishedContent public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false) : this (Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement) { - var propertyTypesA = propertyTypes.ToArray(); - foreach (var propertyType in propertyTypesA) + PublishedPropertyType[] propertyTypesA = propertyTypes.ToArray(); + foreach (PublishedPropertyType propertyType in propertyTypesA) + { propertyType.ContentType = this; + } + _propertyTypes = propertyTypesA; InitializeIndexes(); @@ -123,15 +128,19 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { var aliases = new HashSet(propertyTypes.Select(x => x.Alias), StringComparer.OrdinalIgnoreCase); - foreach (var (alias, dataTypeId) in BuiltinMemberProperties) + foreach (var (alias, dataTypeId) in _builtinMemberProperties) { - if (aliases.Contains(alias)) continue; + if (aliases.Contains(alias)) + { + continue; + } + propertyTypes.Add(factory.CreateCorePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); } } // TODO: this list somehow also exists in constants, see memberTypeRepository => remove duplicate! - private static readonly Dictionary BuiltinMemberProperties = new Dictionary + private static readonly Dictionary _builtinMemberProperties = new Dictionary { { nameof(IMember.Email), Constants.DataTypes.Textbox }, { nameof(IMember.Username), Constants.DataTypes.Textbox }, @@ -174,8 +183,16 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public int GetPropertyIndex(string alias) { - if (_indexes.TryGetValue(alias, out var index)) return index; // fastest - if (_indexes.TryGetValue(alias.ToLowerInvariant(), out index)) return index; // slower + if (_indexes.TryGetValue(alias, out var index)) + { + return index; // fastest + } + + if (_indexes.TryGetValue(alias.ToLowerInvariant(), out index)) + { + return index; // slower + } + return -1; } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs index 23adf358ca..957246ccfe 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeConverter.cs @@ -1,28 +1,25 @@ -using System; using System.ComponentModel; using System.Globalization; -using System.Linq; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +internal class PublishedContentTypeConverter : TypeConverter { - internal class PublishedContentTypeConverter : TypeConverter + private static readonly Type[] ConvertingTypes = { typeof(int) }; + + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) => + ConvertingTypes.Any(x => x.IsAssignableFrom(destinationType)) + || (destinationType is not null && CanConvertFrom(context, destinationType)); + + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { - private static readonly Type[] ConvertingTypes = { typeof(int) }; - - public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + if (!(value is IPublishedContent publishedContent)) { - return ConvertingTypes.Any(x => x.IsAssignableFrom(destinationType)) - || (destinationType is not null && CanConvertFrom(context, destinationType)); + return null; } - public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) - { - if (!(value is IPublishedContent publishedContent)) - return null; - - return typeof(int).IsAssignableFrom(destinationType) - ? publishedContent.Id - : base.ConvertTo(context, culture, value, destinationType); - } + return typeof(int).IsAssignableFrom(destinationType) + ? publishedContent.Id + : base.ConvertTo(context, culture, value, destinationType); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 5a43295981..f2b1b9bbca 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -1,124 +1,141 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a default implementation for . +/// +public class PublishedContentTypeFactory : IPublishedContentTypeFactory { - /// - /// Provides a default implementation for . - /// - public class PublishedContentTypeFactory : IPublishedContentTypeFactory + private readonly IDataTypeService _dataTypeService; + private readonly PropertyValueConverterCollection _propertyValueConverters; + private readonly object _publishedDataTypesLocker = new(); + private readonly IPublishedModelFactory _publishedModelFactory; + private Dictionary? _publishedDataTypes; + + public PublishedContentTypeFactory( + IPublishedModelFactory publishedModelFactory, + PropertyValueConverterCollection propertyValueConverters, + IDataTypeService dataTypeService) { - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly PropertyValueConverterCollection _propertyValueConverters; - private readonly IDataTypeService _dataTypeService; - private readonly object _publishedDataTypesLocker = new object(); - private Dictionary? _publishedDataTypes; - - public PublishedContentTypeFactory(IPublishedModelFactory publishedModelFactory, PropertyValueConverterCollection propertyValueConverters, IDataTypeService dataTypeService) - { - _publishedModelFactory = publishedModelFactory; - _propertyValueConverters = propertyValueConverters; - _dataTypeService = dataTypeService; - } - - /// - public IPublishedContentType CreateContentType(IContentTypeComposition contentType) - { - return new PublishedContentType(contentType, this); - } - - /// - /// This method is for tests and is not intended to be used directly from application code. - /// - /// Values are assumed to be consisted and are not checked. - internal IPublishedContentType CreateContentType(Guid key, int id, string alias, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) - { - return new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement); - } - - /// - /// This method is for tests and is not intended to be used directly from application code. - /// - /// Values are assumed to be consisted and are not checked. - internal IPublishedContentType CreateContentType(Guid key, int id, string alias, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) - { - return new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement); - } - - /// - public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType) - { - return new PublishedPropertyType(contentType, propertyType, _propertyValueConverters, _publishedModelFactory, this); - } - - /// - public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) - { - return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); - } - - /// - public IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) - { - return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, false, variations, _propertyValueConverters, _publishedModelFactory, this); - } - - /// - /// This method is for tests and is not intended to be used directly from application code. - /// - /// Values are assumed to be consisted and are not checked. - internal IPublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) - { - return new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); - } - - /// - public PublishedDataType GetDataType(int id) - { - Dictionary? publishedDataTypes; - lock (_publishedDataTypesLocker) - { - if (_publishedDataTypes == null) - { - var dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); - } - - publishedDataTypes = _publishedDataTypes; - } - - if (publishedDataTypes is null || !publishedDataTypes.TryGetValue(id, out var dataType)) - throw new ArgumentException($"Could not find a datatype with identifier {id}.", nameof(id)); - - return dataType; - } - - /// - public void NotifyDataTypeChanges(int[] ids) - { - lock (_publishedDataTypesLocker) - { - if (_publishedDataTypes == null) - { - var dataTypes = _dataTypeService.GetAll(); - _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); - } - else - { - foreach (var id in ids) - _publishedDataTypes.Remove(id); - - var dataTypes = _dataTypeService.GetAll(ids); - foreach (var dataType in dataTypes) - _publishedDataTypes[dataType.Id] = CreatePublishedDataType(dataType); - } - } - } - - private PublishedDataType CreatePublishedDataType(IDataType dataType) - => new PublishedDataType(dataType.Id, dataType.EditorAlias, dataType is DataType d ? d.GetLazyConfiguration() : new Lazy(() => dataType.Configuration)); + _publishedModelFactory = publishedModelFactory; + _propertyValueConverters = propertyValueConverters; + _dataTypeService = dataTypeService; } + + /// + public IPublishedContentType CreateContentType(IContentTypeComposition contentType) => + new PublishedContentType(contentType, this); + + /// + public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, IPropertyType propertyType) => + new PublishedPropertyType(contentType, propertyType, _propertyValueConverters, _publishedModelFactory, this); + + /// + public IPublishedPropertyType CreatePropertyType( + IPublishedContentType contentType, + string propertyTypeAlias, + int dataTypeId, + ContentVariation variations = ContentVariation.Nothing) => + new PublishedPropertyType( + contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); + + /// + public IPublishedPropertyType CreateCorePropertyType( + IPublishedContentType contentType, + string propertyTypeAlias, + int dataTypeId, + ContentVariation variations = ContentVariation.Nothing) => + new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, false, variations, _propertyValueConverters, _publishedModelFactory, this); + + /// + public PublishedDataType GetDataType(int id) + { + Dictionary? publishedDataTypes; + lock (_publishedDataTypesLocker) + { + if (_publishedDataTypes == null) + { + IEnumerable dataTypes = _dataTypeService.GetAll(); + _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); + } + + publishedDataTypes = _publishedDataTypes; + } + + if (publishedDataTypes is null || !publishedDataTypes.TryGetValue(id, out PublishedDataType? dataType)) + { + throw new ArgumentException($"Could not find a datatype with identifier {id}.", nameof(id)); + } + + return dataType; + } + + /// + public void NotifyDataTypeChanges(int[] ids) + { + lock (_publishedDataTypesLocker) + { + if (_publishedDataTypes == null) + { + IEnumerable dataTypes = _dataTypeService.GetAll(); + _publishedDataTypes = dataTypes.ToDictionary(x => x.Id, CreatePublishedDataType); + } + else + { + foreach (var id in ids) + { + _publishedDataTypes.Remove(id); + } + + IEnumerable dataTypes = _dataTypeService.GetAll(ids); + foreach (IDataType dataType in dataTypes) + { + _publishedDataTypes[dataType.Id] = CreatePublishedDataType(dataType); + } + } + } + } + + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedContentType CreateContentType( + Guid key, + int id, + string alias, + Func> propertyTypes, + ContentVariation variations = ContentVariation.Nothing, + bool isElement = false) => + new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement); + + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedContentType CreateContentType( + Guid key, + int id, + string alias, + IEnumerable compositionAliases, + Func> propertyTypes, + ContentVariation variations = ContentVariation.Nothing, + bool isElement = false) => + new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement); + + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedPropertyType CreatePropertyType( + string propertyTypeAlias, + int dataTypeId, + bool umbraco = false, + ContentVariation variations = ContentVariation.Nothing) => + new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); + + private PublishedDataType CreatePublishedDataType(IDataType dataType) + => new(dataType.Id, dataType.EditorAlias, dataType is DataType d ? d.GetLazyConfiguration() : new Lazy(() => dataType.Configuration)); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index 9d16de743d..b5e9a94e13 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -1,135 +1,112 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +// we cannot implement strongly-typed content by inheriting from some sort +// of "master content" because that master content depends on the actual content cache +// that is being used. It can be an XmlPublishedContent with the XmlPublishedCache, +// or just anything else. +// +// So we implement strongly-typed content by encapsulating whatever content is +// returned by the content cache, and providing extra properties (mostly) or +// methods or whatever. This class provides the base for such encapsulation. +// + +/// +/// Provides an abstract base class for IPublishedContent implementations that +/// wrap and extend another IPublishedContent. +/// +[DebuggerDisplay("{Id}: {Name} ({ContentType?.Alias})")] +public abstract class PublishedContentWrapped : IPublishedContent { - // - // we cannot implement strongly-typed content by inheriting from some sort - // of "master content" because that master content depends on the actual content cache - // that is being used. It can be an XmlPublishedContent with the XmlPublishedCache, - // or just anything else. - // - // So we implement strongly-typed content by encapsulating whatever content is - // returned by the content cache, and providing extra properties (mostly) or - // methods or whatever. This class provides the base for such encapsulation. - // + private readonly IPublishedContent _content; + private readonly IPublishedValueFallback _publishedValueFallback; /// - /// Provides an abstract base class for IPublishedContent implementations that - /// wrap and extend another IPublishedContent. + /// Initialize a new instance of the class + /// with an IPublishedContent instance to wrap. /// - [DebuggerDisplay("{Id}: {Name} ({ContentType?.Alias})")] - public abstract class PublishedContentWrapped : IPublishedContent + /// The content to wrap. + /// The published value fallback. + protected PublishedContentWrapped(IPublishedContent content, IPublishedValueFallback publishedValueFallback) { - private readonly IPublishedContent _content; - private readonly IPublishedValueFallback _publishedValueFallback; - - /// - /// Initialize a new instance of the class - /// with an IPublishedContent instance to wrap. - /// - /// The content to wrap. - /// The published value fallback. - protected PublishedContentWrapped(IPublishedContent content, IPublishedValueFallback publishedValueFallback) - { - _content = content; - _publishedValueFallback = publishedValueFallback; - } - - /// - /// Gets the wrapped content. - /// - /// The wrapped content, that was passed as an argument to the constructor. - public IPublishedContent Unwrap() => _content; - - #region ContentType - - /// - public virtual IPublishedContentType ContentType => _content.ContentType; - - #endregion - - #region PublishedElement - - /// - public Guid Key => _content.Key; - - #endregion - - #region PublishedContent - - /// - public virtual int Id => _content.Id; - - /// - public virtual string? Name => _content.Name; - - /// - public virtual string? UrlSegment => _content.UrlSegment; - - /// - public virtual int SortOrder => _content.SortOrder; - - /// - public virtual int Level => _content.Level; - - /// - public virtual string Path => _content.Path; - - /// - public virtual int? TemplateId => _content.TemplateId; - - /// - public virtual int CreatorId => _content.CreatorId; - - /// - public virtual DateTime CreateDate => _content.CreateDate; - - /// - public virtual int WriterId => _content.WriterId; - - /// - public virtual DateTime UpdateDate => _content.UpdateDate; - - /// - public IReadOnlyDictionary Cultures => _content.Cultures; - - /// - public virtual PublishedItemType ItemType => _content.ItemType; - - /// - public virtual bool IsDraft(string? culture = null) => _content.IsDraft(culture); - - /// - public virtual bool IsPublished(string? culture = null) => _content.IsPublished(culture); - - #endregion - - #region Tree - - /// - public virtual IPublishedContent? Parent => _content.Parent; - - /// - public virtual IEnumerable? Children => _content.Children; - - /// - public virtual IEnumerable? ChildrenForAllCultures => _content.ChildrenForAllCultures; - - #endregion - - #region Properties - - /// - public virtual IEnumerable Properties => _content.Properties; - - /// - public virtual IPublishedProperty? GetProperty(string alias) - { - return _content.GetProperty(alias); - } - - #endregion + _content = content; + _publishedValueFallback = publishedValueFallback; } + + /// + public virtual IPublishedContentType ContentType => _content.ContentType; + + /// + public Guid Key => _content.Key; + + #region PublishedContent + + /// + public virtual int Id => _content.Id; + + #endregion + + /// + /// Gets the wrapped content. + /// + /// The wrapped content, that was passed as an argument to the constructor. + public IPublishedContent Unwrap() => _content; + + /// + public virtual string? Name => _content.Name; + + /// + public virtual string? UrlSegment => _content.UrlSegment; + + /// + public virtual int SortOrder => _content.SortOrder; + + /// + public virtual int Level => _content.Level; + + /// + public virtual string Path => _content.Path; + + /// + public virtual int? TemplateId => _content.TemplateId; + + /// + public virtual int CreatorId => _content.CreatorId; + + /// + public virtual DateTime CreateDate => _content.CreateDate; + + /// + public virtual int WriterId => _content.WriterId; + + /// + public virtual DateTime UpdateDate => _content.UpdateDate; + + /// + public IReadOnlyDictionary Cultures => _content.Cultures; + + /// + public virtual PublishedItemType ItemType => _content.ItemType; + + /// + public virtual IPublishedContent? Parent => _content.Parent; + + /// + public virtual bool IsDraft(string? culture = null) => _content.IsDraft(culture); + + /// + public virtual bool IsPublished(string? culture = null) => _content.IsPublished(culture); + + /// + public virtual IEnumerable? Children => _content.Children; + + /// + public virtual IEnumerable? ChildrenForAllCultures => _content.ChildrenForAllCultures; + + /// + public virtual IEnumerable Properties => _content.Properties; + + /// + public virtual IPublishedProperty? GetProperty(string alias) => _content.GetProperty(alias); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs index 9525a9d7ac..1101301f36 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs @@ -1,51 +1,60 @@ -using System; using System.Diagnostics; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Contains culture specific values for . +/// +[DebuggerDisplay("{Culture}")] +public class PublishedCultureInfo { /// - /// Contains culture specific values for . + /// Initializes a new instance of the class. /// - [DebuggerDisplay("{Culture}")] - public class PublishedCultureInfo + public PublishedCultureInfo(string culture, string? name, string? urlSegment, DateTime date) { - /// - /// Initializes a new instance of the class. - /// - public PublishedCultureInfo(string culture, string? name, string? urlSegment, DateTime date) + if (name == null) { - 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)); - - Culture = culture ?? throw new ArgumentNullException(nameof(culture)); - Name = name; - UrlSegment = urlSegment; - Date = date; + throw new ArgumentNullException(nameof(name)); } - /// - /// Gets the culture. - /// - public string Culture { get; } + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } - /// - /// Gets the name of the item. - /// - public string Name { get; } - - /// - /// Gets the URL segment of the item. - /// - public string? UrlSegment { get; } - - /// - /// Gets the date associated with the culture. - /// - /// - /// For published culture, this is the date the culture was published. For draft - /// cultures, this is the date the culture was made available, ie the last time its - /// name changed. - /// - public DateTime Date { get; } + Culture = culture ?? throw new ArgumentNullException(nameof(culture)); + Name = name; + UrlSegment = urlSegment; + Date = date; } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the name of the item. + /// + public string Name { get; } + + /// + /// Gets the URL segment of the item. + /// + public string? UrlSegment { get; } + + /// + /// Gets the date associated with the culture. + /// + /// + /// + /// For published culture, this is the date the culture was published. For draft + /// cultures, this is the date the culture was made available, ie the last time its + /// name changed. + /// + /// + public DateTime Date { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs index de590c2531..8f77f404ae 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs @@ -1,64 +1,65 @@ -using System; using System.Diagnostics; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents a published data type. +/// +/// +/// +/// Instances of the class are immutable, ie +/// if the data type changes, then a new class needs to be created. +/// +/// These instances should be created by an . +/// +[DebuggerDisplay("{EditorAlias}")] +public class PublishedDataType { + private readonly Lazy _lazyConfiguration; + /// - /// Represents a published data type. + /// Initializes a new instance of the class. /// - /// - /// Instances of the class are immutable, ie - /// if the data type changes, then a new class needs to be created. - /// These instances should be created by an . - /// - [DebuggerDisplay("{EditorAlias}")] - public class PublishedDataType + public PublishedDataType(int id, string editorAlias, Lazy lazyConfiguration) { - private readonly Lazy _lazyConfiguration; + _lazyConfiguration = lazyConfiguration; - /// - /// Initializes a new instance of the class. - /// - public PublishedDataType(int id, string editorAlias, Lazy lazyConfiguration) + Id = id; + EditorAlias = editorAlias; + } + + /// + /// Gets the datatype identifier. + /// + public int Id { get; } + + /// + /// Gets the data type editor alias. + /// + public string EditorAlias { get; } + + /// + /// Gets the data type configuration. + /// + public object? Configuration => _lazyConfiguration?.Value; + + /// + /// Gets the configuration object. + /// + /// The expected type of the configuration object. + /// When the datatype configuration is not of the expected type. + public T? ConfigurationAs() + where T : class + { + switch (Configuration) { - _lazyConfiguration = lazyConfiguration; - - Id = id; - EditorAlias = editorAlias; + case null: + return null; + case T configurationAsT: + return configurationAsT; } - /// - /// Gets the datatype identifier. - /// - public int Id { get; } - - /// - /// Gets the data type editor alias. - /// - public string EditorAlias { get; } - - /// - /// Gets the data type configuration. - /// - public object? Configuration => _lazyConfiguration?.Value; - - /// - /// Gets the configuration object. - /// - /// The expected type of the configuration object. - /// When the datatype configuration is not of the expected type. - public T? ConfigurationAs() - where T : class - { - switch (Configuration) - { - case null: - return null; - case T configurationAsT: - return configurationAsT; - } - - throw new InvalidCastException($"Cannot cast dataType configuration, of type {Configuration.GetType().Name}, to {typeof(T).Name}."); - } + throw new InvalidCastException( + $"Cannot cast dataType configuration, of type {Configuration.GetType().Name}, to {typeof(T).Name}."); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs index f093e7b20c..b91171012c 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs @@ -1,22 +1,24 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// +/// Represents a strongly-typed published element. +/// +/// +/// Every strongly-typed property set class should inherit from PublishedElementModel +/// (or inherit from a class that inherits from... etc.) so they are picked by the factory. +/// +public abstract class PublishedElementModel : PublishedElementWrapped { /// /// - /// Represents a strongly-typed published element. + /// Initializes a new instance of the class with + /// an original instance. /// - /// Every strongly-typed property set class should inherit from PublishedElementModel - /// (or inherit from a class that inherits from... etc.) so they are picked by the factory. - public abstract class PublishedElementModel : PublishedElementWrapped + /// The original content. + /// The published value fallback. + protected PublishedElementModel(IPublishedElement content, IPublishedValueFallback publishedValueFallback) + : base(content, publishedValueFallback) { - /// - /// - /// Initializes a new instance of the class with - /// an original instance. - /// - /// The original content. - /// The published value fallback. - protected PublishedElementModel(IPublishedElement content, IPublishedValueFallback publishedValueFallback) - : base(content, publishedValueFallback) - { } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs index cc0c6b963a..d56230cbfa 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs @@ -1,45 +1,41 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// Provides an abstract base class for IPublishedElement implementations that +/// wrap and extend another IPublishedElement. +/// +public abstract class PublishedElementWrapped : IPublishedElement { + private readonly IPublishedElement _content; + private readonly IPublishedValueFallback _publishedValueFallback; + /// - /// Provides an abstract base class for IPublishedElement implementations that - /// wrap and extend another IPublishedElement. + /// Initializes a new instance of the class + /// with an IPublishedElement instance to wrap. /// - public abstract class PublishedElementWrapped : IPublishedElement + /// The content to wrap. + /// The published value fallback. + protected PublishedElementWrapped(IPublishedElement content, IPublishedValueFallback publishedValueFallback) { - private readonly IPublishedElement _content; - private readonly IPublishedValueFallback _publishedValueFallback; - - /// - /// Initializes a new instance of the class - /// with an IPublishedElement instance to wrap. - /// - /// The content to wrap. - /// The published value fallback. - protected PublishedElementWrapped(IPublishedElement content, IPublishedValueFallback publishedValueFallback) - { - _content = content; - _publishedValueFallback = publishedValueFallback; - } - - /// - /// Gets the wrapped content. - /// - /// The wrapped content, that was passed as an argument to the constructor. - public IPublishedElement Unwrap() => _content; - - /// - public IPublishedContentType ContentType => _content.ContentType; - - /// - public Guid Key => _content.Key; - - /// - public IEnumerable Properties => _content.Properties; - - /// - public IPublishedProperty? GetProperty(string alias) => _content.GetProperty(alias); + _content = content; + _publishedValueFallback = publishedValueFallback; } + + /// + public IPublishedContentType ContentType => _content.ContentType; + + /// + public Guid Key => _content.Key; + + /// + public IEnumerable Properties => _content.Properties; + + /// + public IPublishedProperty? GetProperty(string alias) => _content.GetProperty(alias); + + /// + /// Gets the wrapped content. + /// + /// The wrapped content, that was passed as an argument to the constructor. + public IPublishedElement Unwrap() => _content; } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs index 7d16152b6e..2204cc5107 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedItemType.cs @@ -1,34 +1,33 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// The type of published element. +/// +/// Can be a simple element, or a document, a media, a member. +public enum PublishedItemType { /// - /// The type of published element. + /// Unknown. /// - /// Can be a simple element, or a document, a media, a member. - public enum PublishedItemType - { - /// - /// Unknown. - /// - Unknown = 0, + Unknown = 0, - /// - /// An element. - /// - Element, + /// + /// An element. + /// + Element, - /// - /// A document. - /// - Content, + /// + /// A document. + /// + Content, - /// - /// A media. - /// - Media, + /// + /// A media. + /// + Media, - /// - /// A member. - /// - Member - } + /// + /// A member. + /// + Member, } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs index 035c8a213a..5048f61908 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelAttribute.cs @@ -1,32 +1,40 @@ -using System; +namespace Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Models.PublishedContent +/// +/// +/// Indicates that the class is a published content model for a specified content type. +/// +/// +/// By default, the name of the class is assumed to be the content type alias. The +/// PublishedContentModelAttribute can be used to indicate a different alias. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class PublishedModelAttribute : Attribute { /// /// - /// Indicates that the class is a published content model for a specified content type. + /// Initializes a new instance of the class with a content type alias. /// - /// By default, the name of the class is assumed to be the content type alias. The - /// PublishedContentModelAttribute can be used to indicate a different alias. - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class PublishedModelAttribute : Attribute + /// The content type alias. + public PublishedModelAttribute(string contentTypeAlias) { - /// - /// - /// Initializes a new instance of the class with a content type alias. - /// - /// The content type alias. - public PublishedModelAttribute(string contentTypeAlias) + if (contentTypeAlias == null) { - if (contentTypeAlias == null) throw new ArgumentNullException(nameof(contentTypeAlias)); - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias)); - - ContentTypeAlias = contentTypeAlias; + throw new ArgumentNullException(nameof(contentTypeAlias)); } - /// - /// Gets or sets the content type alias. - /// - public string ContentTypeAlias { get; } + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(contentTypeAlias)); + } + + ContentTypeAlias = contentTypeAlias; } + + /// + /// Gets or sets the content type alias. + /// + public string ContentTypeAlias { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs index 7053a238e6..b2d5da7876 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs @@ -1,156 +1,164 @@ using System.Collections; using System.Reflection; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Implements a strongly typed content model factory +/// +public class PublishedModelFactory : IPublishedModelFactory { + private readonly Dictionary? _modelInfos; + private readonly Dictionary _modelTypeMap; + private readonly IPublishedValueFallback _publishedValueFallback; + /// - /// Implements a strongly typed content model factory + /// Initializes a new instance of the class with types. /// - public class PublishedModelFactory : IPublishedModelFactory + /// The model types. + /// + /// + /// + /// Types must implement IPublishedContent and have a unique constructor that + /// accepts one IPublishedContent as a parameter. + /// + /// To activate, + /// + /// var types = TypeLoader.Current.GetTypes{PublishedContentModel}(); + /// var factory = new PublishedContentModelFactoryImpl(types); + /// PublishedContentModelFactoryResolver.Current.SetFactory(factory); + /// + /// + public PublishedModelFactory(IEnumerable types, IPublishedValueFallback publishedValueFallback) { - private readonly Dictionary? _modelInfos; - private readonly Dictionary _modelTypeMap; - private readonly IPublishedValueFallback _publishedValueFallback; + var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var modelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private class ModelInfo + foreach (Type type in types) { - public Type? ParameterType { get; set; } + // so... the model type has to implement a ctor with one parameter being, or inheriting from, + // IPublishedElement - but it can be IPublishedContent - so we cannot get one precise ctor, + // we have to iterate over all ctors and try to find the right one + ConstructorInfo? constructor = null; + Type? parameterType = null; - public Func? Ctor { get; set; } - - public Type? ModelType { get; set; } - - public Func? ListCtor { get; set; } - } - - /// - /// Initializes a new instance of the class with types. - /// - /// The model types. - /// - /// Types must implement IPublishedContent and have a unique constructor that - /// accepts one IPublishedContent as a parameter. - /// To activate, - /// - /// var types = TypeLoader.Current.GetTypes{PublishedContentModel}(); - /// var factory = new PublishedContentModelFactoryImpl(types); - /// PublishedContentModelFactoryResolver.Current.SetFactory(factory); - /// - /// - public PublishedModelFactory(IEnumerable types, IPublishedValueFallback publishedValueFallback) - { - var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - var modelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - foreach (var type in types) + foreach (ConstructorInfo ctor in type.GetConstructors()) { - // so... the model type has to implement a ctor with one parameter being, or inheriting from, - // IPublishedElement - but it can be IPublishedContent - so we cannot get one precise ctor, - // we have to iterate over all ctors and try to find the right one - - ConstructorInfo? constructor = null; - Type? parameterType = null; - - foreach (var ctor in type.GetConstructors()) + ParameterInfo[] parms = ctor.GetParameters(); + if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && + typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType)) { - var parms = ctor.GetParameters(); - if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType)) + if (constructor != null) { - if (constructor != null) - { - throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPublishedElement."); - } - - constructor = ctor; - parameterType = parms[0].ParameterType; + throw new InvalidOperationException( + $"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPublishedElement."); } + + constructor = ctor; + parameterType = parms[0].ParameterType; } - - if (constructor == null) - { - throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPublishedElement."); - } - - var attribute = type.GetCustomAttribute(false); - var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; - - if (modelInfos.TryGetValue(typeName, out var modelInfo)) - { - throw new InvalidOperationException($"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType?.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); - } - - // have to use an unsafe ctor because we don't know the types, really - var modelCtor = ReflectionUtilities.EmitConstructorUnsafe>(constructor); - modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type, Ctor = modelCtor }; - modelTypeMap[typeName] = type; } - _modelInfos = modelInfos.Count > 0 ? modelInfos : null; - _modelTypeMap = modelTypeMap; - _publishedValueFallback = publishedValueFallback; + if (constructor == null) + { + throw new InvalidOperationException( + $"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPublishedElement."); + } + + PublishedModelAttribute? attribute = type.GetCustomAttribute(false); + var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; + + if (modelInfos.TryGetValue(typeName, out ModelInfo? modelInfo)) + { + throw new InvalidOperationException( + $"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType?.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); + } + + // have to use an unsafe ctor because we don't know the types, really + Func modelCtor = + ReflectionUtilities.EmitConstructorUnsafe>(constructor); + modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type, Ctor = modelCtor }; + modelTypeMap[typeName] = type; } - /// - public IPublishedElement CreateModel(IPublishedElement element) + _modelInfos = modelInfos.Count > 0 ? modelInfos : null; + _modelTypeMap = modelTypeMap; + _publishedValueFallback = publishedValueFallback; + } + + /// + public IPublishedElement CreateModel(IPublishedElement element) + { + // fail fast + if (_modelInfos is null || element.ContentType.Alias is null || + !_modelInfos.TryGetValue(element.ContentType.Alias, out ModelInfo? modelInfo)) { - // fail fast - if (_modelInfos is null || element.ContentType.Alias is null || !_modelInfos.TryGetValue(element.ContentType.Alias, out var modelInfo)) - { - return element; - } - - // ReSharper disable once UseMethodIsInstanceOfType - if (modelInfo.ParameterType?.IsAssignableFrom(element.GetType()) == false) - { - throw new InvalidOperationException($"Model {modelInfo.ModelType} expects argument of type {modelInfo.ParameterType.FullName}, but got {element.GetType().FullName}."); - } - - // can cast, because we checked when creating the ctor - return (IPublishedElement)modelInfo.Ctor!(element, _publishedValueFallback); + return element; } - /// - public IList? CreateModelList(string? alias) + // ReSharper disable once UseMethodIsInstanceOfType + if (modelInfo.ParameterType?.IsAssignableFrom(element.GetType()) == false) { - // fail fast - if (_modelInfos is null || alias is null || !_modelInfos.TryGetValue(alias, out var modelInfo) || modelInfo.ModelType is null) - { - return new List(); - } - - var ctor = modelInfo.ListCtor; - if (ctor != null) - { - return ctor(); - } - - var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); - ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); - if (ctor is not null) - { - return ctor(); - } - - return null; + throw new InvalidOperationException( + $"Model {modelInfo.ModelType} expects argument of type {modelInfo.ParameterType.FullName}, but got {element.GetType().FullName}."); } - /// - public Type GetModelType(string? alias) + // can cast, because we checked when creating the ctor + return (IPublishedElement)modelInfo.Ctor!(element, _publishedValueFallback); + } + + /// + public IList? CreateModelList(string? alias) + { + // fail fast + if (_modelInfos is null || alias is null || !_modelInfos.TryGetValue(alias, out ModelInfo? modelInfo) || + modelInfo.ModelType is null) { - // fail fast - if (_modelInfos is null || - alias is null || - !_modelInfos.TryGetValue(alias, out var modelInfo) || - modelInfo.ModelType is null) - { - return typeof(IPublishedElement); - } - - return modelInfo.ModelType; + return new List(); } - /// - public Type MapModelType(Type type) - => ModelType.Map(type, _modelTypeMap); + Func? ctor = modelInfo.ListCtor; + if (ctor != null) + { + return ctor(); + } + + Type listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); + ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); + if (ctor is not null) + { + return ctor(); + } + + return null; + } + + /// + public Type GetModelType(string? alias) + { + // fail fast + if (_modelInfos is null || + alias is null || + !_modelInfos.TryGetValue(alias, out ModelInfo? modelInfo) || modelInfo.ModelType is null) + { + return typeof(IPublishedElement); + } + + return modelInfo.ModelType; + } + + /// + public Type MapModelType(Type type) + => ModelType.Map(type, _modelTypeMap); + + private class ModelInfo + { + public Type? ParameterType { get; set; } + + public Func? Ctor { get; set; } + + public Type? ModelType { get; set; } + + public Func? ListCtor { get; set; } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs index 6cdbd85c74..25cf64899b 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs @@ -1,69 +1,71 @@ -using System; using System.Diagnostics; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a base class for IPublishedProperty implementations which converts and caches +/// the value source to the actual value to use when rendering content. +/// +[DebuggerDisplay("{Alias} ({PropertyType?.EditorAlias})")] +public abstract class PublishedPropertyBase : IPublishedProperty { /// - /// Provides a base class for IPublishedProperty implementations which converts and caches - /// the value source to the actual value to use when rendering content. + /// Initializes a new instance of the class. /// - [DebuggerDisplay("{Alias} ({PropertyType?.EditorAlias})")] - public abstract class PublishedPropertyBase : IPublishedProperty + protected PublishedPropertyBase(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel) { - /// - /// Initializes a new instance of the class. - /// - protected PublishedPropertyBase(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel) + PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); + ReferenceCacheLevel = referenceCacheLevel; + + ValidateCacheLevel(ReferenceCacheLevel, true); + ValidateCacheLevel(PropertyType.CacheLevel, false); + } + + /// + /// Gets the property reference cache level. + /// + public PropertyCacheLevel ReferenceCacheLevel { get; } + + /// + /// Gets the property type. + /// + public IPublishedPropertyType PropertyType { get; } + + /// + public string Alias => PropertyType.Alias; + + /// + public abstract bool HasValue(string? culture = null, string? segment = null); + + /// + public abstract object? GetSourceValue(string? culture = null, string? segment = null); + + /// + public abstract object? GetValue(string? culture = null, string? segment = null); + + /// + public abstract object? GetXPathValue(string? culture = null, string? segment = null); + + // validates the cache level + private static void ValidateCacheLevel(PropertyCacheLevel cacheLevel, bool validateUnknown) + { + switch (cacheLevel) { - PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - ReferenceCacheLevel = referenceCacheLevel; + case PropertyCacheLevel.Element: + case PropertyCacheLevel.Elements: + case PropertyCacheLevel.Snapshot: + case PropertyCacheLevel.None: + break; + case PropertyCacheLevel.Unknown: + if (!validateUnknown) + { + goto default; + } - ValidateCacheLevel(ReferenceCacheLevel, true); - ValidateCacheLevel(PropertyType.CacheLevel, false); + break; + default: + throw new Exception($"Invalid cache level \"{cacheLevel}\"."); } - - // validates the cache level - private static void ValidateCacheLevel(PropertyCacheLevel cacheLevel, bool validateUnknown) - { - switch (cacheLevel) - { - case PropertyCacheLevel.Element: - case PropertyCacheLevel.Elements: - case PropertyCacheLevel.Snapshot: - case PropertyCacheLevel.None: - break; - case PropertyCacheLevel.Unknown: - if (!validateUnknown) goto default; - break; - default: - throw new Exception($"Invalid cache level \"{cacheLevel}\"."); - } - } - - /// - /// Gets the property type. - /// - public IPublishedPropertyType PropertyType { get; } - - /// - /// Gets the property reference cache level. - /// - public PropertyCacheLevel ReferenceCacheLevel { get; } - - /// - public string Alias => PropertyType.Alias; - - /// - public abstract bool HasValue(string? culture = null, string? segment = null); - - /// - public abstract object? GetSourceValue(string? culture = null, string? segment = null); - - /// - public abstract object? GetValue(string? culture = null, string? segment = null); - - /// - public abstract object? GetXPathValue(string? culture = null, string? segment = null); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 9420811f24..4bc4b02f68 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Xml.Linq; using System.Xml.XPath; using Umbraco.Cms.Core.PropertyEditors; @@ -99,10 +98,18 @@ namespace Umbraco.Cms.Core.Models.PublishedContent private void Initialize() { - if (_initialized) return; + if (_initialized) + { + return; + } + lock (_locker) { - if (_initialized) return; + if (_initialized) + { + return; + } + InitializeLocked(); _initialized = true; } @@ -113,10 +120,12 @@ namespace Umbraco.Cms.Core.Models.PublishedContent _converter = null; var isdefault = false; - foreach (var converter in _propertyValueConverters) + foreach (IPropertyValueConverter converter in _propertyValueConverters) { if (!converter.IsConverter(this)) + { continue; + } if (_converter == null) { @@ -142,11 +151,14 @@ namespace Umbraco.Cms.Core.Models.PublishedContent else { // no shadow - bad - throw new InvalidOperationException(string.Format("Type '{2}' cannot be an IPropertyValueConverter" - + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" - + " for that property, and only one converter can exist for a property.", - ContentType?.Alias, Alias, - converter.GetType().FullName, _converter.GetType().FullName)); + throw new InvalidOperationException(string.Format( + "Type '{2}' cannot be an IPropertyValueConverter" + + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" + + " for that property, and only one converter can exist for a property.", + ContentType?.Alias, + Alias, + converter.GetType().FullName, + _converter.GetType().FullName)); } } else @@ -165,11 +177,14 @@ namespace Umbraco.Cms.Core.Models.PublishedContent else { // previous was non-default, and got another non-default - bad - throw new InvalidOperationException(string.Format("Type '{2}' cannot be an IPropertyValueConverter" - + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" - + " for that property, and only one converter can exist for a property.", - ContentType?.Alias, Alias, - converter.GetType().FullName, _converter.GetType().FullName)); + throw new InvalidOperationException(string.Format( + "Type '{2}' cannot be an IPropertyValueConverter" + + " for property '{1}' of content type '{0}' because type '{3}' has already been detected as a converter" + + " for that property, and only one converter can exist for a property.", + ContentType?.Alias, + Alias, + converter.GetType().FullName, + _converter.GetType().FullName)); } } } @@ -181,11 +196,16 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public bool? IsValue(object? value, PropertyValueLevel level) { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } // if we have a converter, use the converter if (_converter != null) + { return _converter.IsValue(value, level); + } // otherwise use the old magic null & string comparisons return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); @@ -196,7 +216,11 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { get { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } + return _cacheLevel; } } @@ -204,7 +228,10 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public object? ConvertSourceToInter(IPublishedElement owner, object? source, bool preview) { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } // use the converter if any, else just return the source value return _converter != null @@ -215,7 +242,10 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public object? ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } // use the converter if any, else just return the inter value return _converter != null @@ -226,16 +256,28 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public object? ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } // use the converter if any if (_converter != null) + { return _converter.ConvertIntermediateToXPath(owner, this, referenceCacheLevel, inter, preview); + } // else just return the inter value as a string or an XPathNavigator - if (inter == null) return null; + if (inter == null) + { + return null; + } + if (inter is XElement xElement) + { return xElement.CreateNavigator(); + } + return inter.ToString()?.Trim(); } @@ -244,7 +286,11 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { get { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } + return _modelClrType!; } } @@ -254,7 +300,11 @@ namespace Umbraco.Cms.Core.Models.PublishedContent { get { - if (!_initialized) Initialize(); + if (!_initialized) + { + Initialize(); + } + return _clrType ?? (_modelClrType is not null ? _clrType = _publishedModelFactory.MapModelType(_modelClrType) : null); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs index edc6cd9150..f0c2626f90 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs @@ -1,17 +1,17 @@ -using System.Diagnostics; +using System.Diagnostics; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +[DebuggerDisplay("{Content?.Name} ({Score})")] +public class PublishedSearchResult { - [DebuggerDisplay("{Content?.Name} ({Score})")] - public class PublishedSearchResult + public PublishedSearchResult(IPublishedContent content, float score) { - public PublishedSearchResult(IPublishedContent content, float score) - { - Content = content; - Score = score; - } - - public IPublishedContent Content { get; } - public float Score { get; } + Content = content; + Score = score; } + + public IPublishedContent Content { get; } + + public float Score { get; } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs index ed8acf2736..64f0160383 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs @@ -1,296 +1,350 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a default implementation for . +/// +public class PublishedValueFallback : IPublishedValueFallback { + private readonly ILocalizationService? _localizationService; + private readonly IVariationContextAccessor _variationContextAccessor; + /// - /// Provides a default implementation for . + /// Initializes a new instance of the class. /// - public class PublishedValueFallback : IPublishedValueFallback + public PublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor) { - private readonly ILocalizationService? _localizationService; - private readonly IVariationContextAccessor _variationContextAccessor; + _localizationService = serviceContext.LocalizationService; + _variationContextAccessor = variationContextAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public PublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor) + /// + public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) => + TryGetValue(property, culture, segment, fallback, defaultValue, out value); + + /// + public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + { + _variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment); + + foreach (var f in fallback) { - _localizationService = serviceContext.LocalizationService; - _variationContextAccessor = variationContextAccessor; - } - - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) - { - return TryGetValue(property, culture, segment, fallback, defaultValue, out value); - } - - /// - public bool TryGetValue(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) - { - _variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment); - - foreach (var f in fallback) + switch (f) { - switch (f) - { - case Fallback.None: - continue; - case Fallback.DefaultValue: - value = defaultValue; + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (TryGetValueWithLanguageFallback(property, culture, segment, out value)) + { return true; - case Fallback.Language: - if (TryGetValueWithLanguageFallback(property, culture, segment, out value)) - return true; - break; - default: - throw NotSupportedFallbackMethod(f, "property"); - } - } + } + break; + default: + throw NotSupportedFallbackMethod(f, "property"); + } + } + + value = default; + return false; + } + + /// + public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) => + TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value); + + /// + public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + { + IPublishedPropertyType? propertyType = content.ContentType.GetPropertyType(alias); + if (propertyType == null) + { value = default; return false; } - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value) + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); + + foreach (var f in fallback) { - return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value); + switch (f) + { + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) + { + return true; + } + + break; + default: + throw NotSupportedFallbackMethod(f, "element"); + } } - /// - public bool TryGetValue(IPublishedElement content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value) + value = default; + return false; + } + + /// + public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) => + TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value, out noValueProperty); + + /// + public virtual bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value, out IPublishedProperty? noValueProperty) + { + noValueProperty = default; + + IPublishedPropertyType? propertyType = content.ContentType.GetPropertyType(alias); + if (propertyType != null) { - var propertyType = content.ContentType.GetPropertyType(alias); - if (propertyType == null) + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); + noValueProperty = content.GetProperty(alias); + } + + // note: we don't support "recurse & language" which would walk up the tree, + // looking at languages at each level - should someone need it... they'll have + // to implement it. + foreach (var f in fallback) + { + switch (f) + { + case Fallback.None: + continue; + case Fallback.DefaultValue: + value = defaultValue; + return true; + case Fallback.Language: + if (propertyType == null) + { + continue; + } + + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) + { + return true; + } + + break; + case Fallback.Ancestors: + if (TryGetValueWithAncestorsFallback(content, alias, culture, segment, out value, ref noValueProperty)) + { + return true; + } + + break; + default: + throw NotSupportedFallbackMethod(f, "content"); + } + } + + value = default; + return false; + } + + private NotSupportedException NotSupportedFallbackMethod(int fallback, string level) => + new NotSupportedException( + $"Fallback {GetType().Name} does not support fallback code '{fallback}' at {level} level."); + + // tries to get a value, recursing the tree + // because we recurse, content may not even have the a property with the specified alias (but only some ancestor) + // in case no value was found, noValueProperty contains the first property that was found (which does not have a value) + private bool TryGetValueWithAncestorsFallback(IPublishedContent? content, string alias, string? culture, string? segment, out T? value, ref IPublishedProperty? noValueProperty) + { + IPublishedProperty? property; // if we are here, content's property has no value + do + { + content = content?.Parent; + + IPublishedPropertyType? propertyType = content?.ContentType.GetPropertyType(alias); + + if (propertyType != null && content is not null) + { + culture = null; + segment = null; + _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); + } + + property = content?.GetProperty(alias); + if (property != null && noValueProperty == null) + { + noValueProperty = property; + } + } + while (content != null && (property == null || property.HasValue(culture, segment) == false)); + + // if we found a content with the property having a value, return that property value + if (property != null && property.HasValue(culture, segment)) + { + value = property.Value(this, culture, segment); + return true; + } + + value = default; + return false; + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) + { + value = default; + + if (culture.IsNullOrWhiteSpace()) + { + return false; + } + + var visited = new HashSet(); + + ILanguage? language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; + if (language == null) + { + return false; + } + + while (true) + { + if (language.FallbackLanguageId == null) { - value = default; return false; } - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment); - - foreach (var f in fallback) + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) { - switch (f) - { - case Fallback.None: - continue; - case Fallback.DefaultValue: - value = defaultValue; - return true; - case Fallback.Language: - if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) - return true; - break; - default: - throw NotSupportedFallbackMethod(f, "element"); - } + return false; } - value = default; - return false; - } + visited.Add(language2Id); - /// - public bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, object? defaultValue, out object? value, out IPublishedProperty? noValueProperty) - { - return TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value, out noValueProperty); - } - - /// - public virtual bool TryGetValue(IPublishedContent content, string alias, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value, out IPublishedProperty? noValueProperty) - { - noValueProperty = default; - - var propertyType = content.ContentType.GetPropertyType(alias); - if (propertyType != null) + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) { - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); - noValueProperty = content.GetProperty(alias); + return false; } - // note: we don't support "recurse & language" which would walk up the tree, - // looking at languages at each level - should someone need it... they'll have - // to implement it. + var culture2 = language2.IsoCode; - foreach (var f in fallback) + if (property.HasValue(culture2, segment)) { - switch (f) - { - case Fallback.None: - continue; - case Fallback.DefaultValue: - value = defaultValue; - return true; - case Fallback.Language: - if (propertyType == null) - continue; - if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) - return true; - break; - case Fallback.Ancestors: - if (TryGetValueWithAncestorsFallback(content, alias, culture, segment, out value, ref noValueProperty)) - return true; - break; - default: - throw NotSupportedFallbackMethod(f, "content"); - } - } - - value = default; - return false; - } - - private NotSupportedException NotSupportedFallbackMethod(int fallback, string level) - { - return new NotSupportedException($"Fallback {GetType().Name} does not support fallback code '{fallback}' at {level} level."); - } - - // tries to get a value, recursing the tree - // because we recurse, content may not even have the a property with the specified alias (but only some ancestor) - // in case no value was found, noValueProperty contains the first property that was found (which does not have a value) - private bool TryGetValueWithAncestorsFallback(IPublishedContent? content, string alias, string? culture, string? segment, out T? value, ref IPublishedProperty? noValueProperty) - { - IPublishedProperty? property; // if we are here, content's property has no value - do - { - content = content?.Parent; - - var propertyType = content?.ContentType.GetPropertyType(alias); - - if (propertyType != null && content is not null) - { - culture = null; - segment = null; - _variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment); - } - - property = content?.GetProperty(alias); - if (property != null && noValueProperty == null) - { - noValueProperty = property; - } - } - while (content != null && (property == null || property.HasValue(culture, segment) == false)); - - // if we found a content with the property having a value, return that property value - if (property != null && property.HasValue(culture, segment)) - { - value = property.Value(this, culture, segment); + value = property.Value(this, culture2, segment); return true; } - value = default; + language = language2; + } + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string? culture, string? segment, out T? value) + { + value = default; + + if (culture.IsNullOrWhiteSpace()) + { return false; } - // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) + var visited = new HashSet(); + + ILanguage? language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; + if (language == null) { - value = default; - - if (culture.IsNullOrWhiteSpace()) return false; - - var visited = new HashSet(); - - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; - - while (true) - { - if (language.FallbackLanguageId == null) return false; - - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); - - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; - - if (property.HasValue(culture2, segment)) - { - value = property.Value(this, culture2, segment); - return true; - } - - language = language2; - } + return false; } - // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string? culture, string? segment, out T? value) + while (true) { - value = default; - - if (culture.IsNullOrWhiteSpace()) return false; - - var visited = new HashSet(); - - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; - - while (true) + if (language.FallbackLanguageId == null) { - if (language.FallbackLanguageId == null) return false; - - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); - - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; - - if (content.HasValue(alias, culture2, segment)) - { - value = content.Value(this, alias, culture2, segment); - return true; - } - - language = language2; + return false; } + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) + { + return false; + } + + visited.Add(language2Id); + + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) + { + return false; + } + + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(this, alias, culture2, segment); + return true; + } + + language = language2; + } + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string? culture, string? segment, out T? value) + { + value = default; + + if (culture.IsNullOrWhiteSpace()) + { + return false; } - // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string? culture, string? segment, out T? value) + var visited = new HashSet(); + + // TODO: _localizationService.GetXxx() is expensive, it deep clones objects + // we want _localizationService.GetReadOnlyXxx() returning IReadOnlyLanguage which cannot be saved back = no need to clone + ILanguage? language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; + if (language == null) { - value = default; + return false; + } - if (culture.IsNullOrWhiteSpace()) return false; - - var visited = new HashSet(); - - // TODO: _localizationService.GetXxx() is expensive, it deep clones objects - // we want _localizationService.GetReadOnlyXxx() returning IReadOnlyLanguage which cannot be saved back = no need to clone - - var language = culture is not null ? _localizationService?.GetLanguageByIsoCode(culture) : null; - if (language == null) return false; - - while (true) + while (true) + { + if (language.FallbackLanguageId == null) { - if (language.FallbackLanguageId == null) return false; - - var language2Id = language.FallbackLanguageId.Value; - if (visited.Contains(language2Id)) return false; - visited.Add(language2Id); - - var language2 = _localizationService?.GetLanguageById(language2Id); - if (language2 == null) return false; - var culture2 = language2.IsoCode; - - if (content.HasValue(alias, culture2, segment)) - { - value = content.Value(this, alias, culture2, segment); - return true; - } - - language = language2; + return false; } + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) + { + return false; + } + + visited.Add(language2Id); + + ILanguage? language2 = _localizationService?.GetLanguageById(language2Id); + if (language2 == null) + { + return false; + } + + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(this, alias, culture2, segment); + return true; + } + + language = language2; } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index 2ae0ce6c1d..763006f8f1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -1,54 +1,60 @@ -using System; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// +/// Represents a published property that has a unique invariant-neutral value +/// and caches conversion results locally. +/// +/// +/// +/// Conversions results are stored within the property and will not +/// be refreshed, so this class is not suitable for cached properties. +/// +/// +/// Does not support variations: the ctor throws if the property type +/// supports variations. +/// +/// +public class RawValueProperty : PublishedPropertyBase { - /// - /// - /// Represents a published property that has a unique invariant-neutral value - /// and caches conversion results locally. - /// - /// - /// Conversions results are stored within the property and will not - /// be refreshed, so this class is not suitable for cached properties. - /// Does not support variations: the ctor throws if the property type - /// supports variations. - /// - public class RawValueProperty : PublishedPropertyBase + private readonly Lazy _objectValue; + private readonly object _sourceValue; // the value in the db + private readonly Lazy _xpathValue; + + public RawValueProperty(IPublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) + : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { - private readonly object _sourceValue; //the value in the db - private readonly Lazy _objectValue; - private readonly Lazy _xpathValue; - - // RawValueProperty does not (yet?) support variants, - // only manages the current "default" value - - public override object? GetSourceValue(string? culture = null, string? segment = null) - => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _sourceValue : null; - - public override bool HasValue(string? culture = null, string? segment = null) + if (propertyType.Variations != ContentVariation.Nothing) { - var sourceValue = GetSourceValue(culture, segment); - return sourceValue is string s ? !string.IsNullOrWhiteSpace(s) : sourceValue != null; + throw new ArgumentException("Property types with variations are not supported here.", nameof(propertyType)); } - public override object? GetValue(string? culture = null, string? segment = null) - => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _objectValue.Value : null; + _sourceValue = sourceValue; - public override object? GetXPathValue(string? culture = null, string? segment = null) - => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _xpathValue.Value : null; - - public RawValueProperty(IPublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) - : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored - { - if (propertyType.Variations != ContentVariation.Nothing) - throw new ArgumentException("Property types with variations are not supported here.", nameof(propertyType)); - - _sourceValue = sourceValue; - - var interValue = new Lazy(() => PropertyType.ConvertSourceToInter(content, _sourceValue, isPreviewing)); - _objectValue = new Lazy(() => PropertyType.ConvertInterToObject(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); - _xpathValue = new Lazy(() => PropertyType.ConvertInterToXPath(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); - } + var interValue = + new Lazy(() => PropertyType.ConvertSourceToInter(content, _sourceValue, isPreviewing)); + _objectValue = new Lazy(() => + PropertyType.ConvertInterToObject(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); + _xpathValue = new Lazy(() => + PropertyType.ConvertInterToXPath(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); } + + // RawValueProperty does not (yet?) support variants, + // only manages the current "default" value + public override object? GetSourceValue(string? culture = null, string? segment = null) + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _sourceValue : null; + + public override bool HasValue(string? culture = null, string? segment = null) + { + var sourceValue = GetSourceValue(culture, segment); + return sourceValue is string s ? !string.IsNullOrWhiteSpace(s) : sourceValue != null; + } + + public override object? GetValue(string? culture = null, string? segment = null) + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _objectValue.Value : null; + + public override object? GetXPathValue(string? culture = null, string? segment = null) + => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _xpathValue.Value : null; } diff --git a/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs b/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs index a9d06e521f..5919370792 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ThreadCultureVariationContextAccessor.cs @@ -1,23 +1,20 @@ -using System; using System.Collections.Concurrent; -using System.Threading; -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Provides a CurrentUICulture-based implementation of . +/// +/// +/// This accessor does not support segments. There is no need to set the current context. +/// +public class ThreadCultureVariationContextAccessor : IVariationContextAccessor { - /// - /// Provides a CurrentUICulture-based implementation of . - /// - /// - /// This accessor does not support segments. There is no need to set the current context. - /// - public class ThreadCultureVariationContextAccessor : IVariationContextAccessor - { - private readonly ConcurrentDictionary _contexts = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _contexts = new(); - public VariationContext? VariationContext - { - get => _contexts.GetOrAdd(Thread.CurrentThread.CurrentUICulture.Name, culture => new VariationContext(culture)); - set => throw new NotSupportedException(); - } + public VariationContext? VariationContext + { + get => _contexts.GetOrAdd(Thread.CurrentThread.CurrentUICulture.Name, culture => new VariationContext(culture)); + set => throw new NotSupportedException(); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs index 8e24f25332..ff13964fb3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs +++ b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Specifies the type of URLs that the URL provider should produce, Auto is the default. +/// +public enum UrlMode { /// - /// Specifies the type of URLs that the URL provider should produce, Auto is the default. + /// Indicates that the URL provider should do what it has been configured to do. /// - public enum UrlMode - { - /// - /// Indicates that the URL provider should do what it has been configured to do. - /// - Default = 0, + Default = 0, - /// - /// Indicates that the URL provider should produce relative URLs exclusively. - /// - Relative, + /// + /// Indicates that the URL provider should produce relative URLs exclusively. + /// + Relative, - /// - /// Indicates that the URL provider should produce absolute URLs exclusively. - /// - Absolute, + /// + /// Indicates that the URL provider should produce absolute URLs exclusively. + /// + Absolute, - /// - /// Indicates that the URL provider should determine automatically whether to return relative or absolute URLs. - /// - Auto - } + /// + /// Indicates that the URL provider should determine automatically whether to return relative or absolute URLs. + /// + Auto, } diff --git a/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs b/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs index 9b8ae30245..92326ae359 100644 --- a/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs +++ b/src/Umbraco.Core/Models/PublishedContent/VariationContext.cs @@ -1,34 +1,33 @@ -namespace Umbraco.Cms.Core.Models.PublishedContent +namespace Umbraco.Cms.Core.Models.PublishedContent; + +/// +/// Represents the variation context. +/// +public class VariationContext { /// - /// Represents the variation context. + /// Initializes a new instance of the class. /// - public class VariationContext + public VariationContext(string? culture = null, string? segment = null) { - /// - /// Initializes a new instance of the class. - /// - public VariationContext(string? culture = null, string? segment = null) - { - Culture = culture ?? ""; // cannot be null, default to invariant - Segment = segment ?? ""; // cannot be null, default to neutral - } - - /// - /// Gets the culture. - /// - public string Culture { get; } - - /// - /// Gets the segment. - /// - public string Segment { get; } - - /// - /// Gets the segment for the content item - /// - /// - /// - public virtual string GetSegment(int contentId) => Segment; + Culture = culture ?? string.Empty; // cannot be null, default to invariant + Segment = segment ?? string.Empty; // cannot be null, default to neutral } + + /// + /// Gets the culture. + /// + public string Culture { get; } + + /// + /// Gets the segment. + /// + public string Segment { get; } + + /// + /// Gets the segment for the content item + /// + /// + /// + public virtual string GetSegment(int contentId) => Segment; } diff --git a/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs b/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs index 4a986597bd..e8f6e3bdc1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs +++ b/src/Umbraco.Core/Models/PublishedContent/VariationContextAccessorExtensions.cs @@ -1,42 +1,58 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class VariationContextAccessorExtensions { - public static class VariationContextAccessorExtensions + public static void ContextualizeVariation( + this IVariationContextAccessor variationContextAccessor, + ContentVariation variations, + ref string? culture, + ref string? segment) + => variationContextAccessor.ContextualizeVariation(variations, null, ref culture, ref segment); + + public static void ContextualizeVariation( + this IVariationContextAccessor variationContextAccessor, + ContentVariation variations, + int contentId, + ref string? culture, + ref string? segment) + => variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, ref culture, ref segment); + + private static void ContextualizeVariation( + this IVariationContextAccessor variationContextAccessor, + ContentVariation variations, + int? contentId, + ref string? culture, + ref string? segment) { - public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, ref string? culture, ref string? segment) - => variationContextAccessor.ContextualizeVariation(variations, null, ref culture, ref segment); - - public static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, int contentId, ref string? culture, ref string? segment) - => variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, ref culture, ref segment); - - private static void ContextualizeVariation(this IVariationContextAccessor variationContextAccessor, ContentVariation variations, int? contentId, ref string? culture, ref string? segment) + if (culture != null && segment != null) { - if (culture != null && segment != null) return; + return; + } - // use context values - var publishedVariationContext = variationContextAccessor?.VariationContext; - if (culture == null) + // use context values + VariationContext? publishedVariationContext = variationContextAccessor?.VariationContext; + if (culture == null) + { + culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : string.Empty; + } + + if (segment == null) + { + if (variations.VariesBySegment()) { - culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : ""; + segment = contentId == null + ? publishedVariationContext?.Segment + : publishedVariationContext?.GetSegment(contentId.Value); } - - if (segment == null) + else { - if (variations.VariesBySegment()) - { - segment = contentId == null - ? publishedVariationContext?.Segment - : publishedVariationContext?.GetSegment(contentId.Value); - } - else - { - segment = ""; - } + segment = string.Empty; } } } diff --git a/src/Umbraco.Core/Models/PublishedState.cs b/src/Umbraco.Core/Models/PublishedState.cs index 87c106e11e..39d68ea273 100644 --- a/src/Umbraco.Core/Models/PublishedState.cs +++ b/src/Umbraco.Core/Models/PublishedState.cs @@ -1,60 +1,71 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// The states of a content item. +/// +public enum PublishedState { + // versions management in repo: + // + // - published = the content is published + // repo: saving draft values + // update current version (draft) values + // + // - unpublished = the content is not published + // repo: saving draft values + // update current version (draft) values + // + // - publishing = the content is being published (transitory) + // if currently published: + // delete all draft values from current version, not current anymore + // create new version with published+draft values + // + // - unpublishing = the content is being unpublished (transitory) + // if currently published (just in case): + // delete all draft values from current version, not current anymore + // create new version with published+draft values (should be managed by service) + + // when a content item is loaded, its state is one of those two: /// - /// The states of a content item. + /// The content item is published. /// - public enum PublishedState - { - // versions management in repo: - // - // - published = the content is published - // repo: saving draft values - // update current version (draft) values - // - // - unpublished = the content is not published - // repo: saving draft values - // update current version (draft) values - // - // - publishing = the content is being published (transitory) - // if currently published: - // delete all draft values from current version, not current anymore - // create new version with published+draft values - // - // - unpublishing = the content is being unpublished (transitory) - // if currently published (just in case): - // delete all draft values from current version, not current anymore - // create new version with published+draft values (should be managed by service) + Published, - // when a content item is loaded, its state is one of those two: + // also: handled over to repo to save draft values for a published content - /// - /// The content item is published. - /// - Published, - // also: handled over to repo to save draft values for a published content + /// + /// The content item is not published. + /// + Unpublished, - /// - /// The content item is not published. - /// - Unpublished, - // also: handled over to repo to save draft values for an unpublished content + // also: handled over to repo to save draft values for an unpublished content - // when it is handled over to the repository, its state can also be one of those: + // when it is handled over to the repository, its state can also be one of those: - /// - /// The version is being saved, in order to publish the content. - /// - /// The Publishing state is transitional. Once the version - /// is saved, its state changes to Published. - Publishing, + /// + /// The version is being saved, in order to publish the content. + /// + /// + /// The + /// Publishing + /// state is transitional. Once the version + /// is saved, its state changes to + /// Published + /// . + /// + Publishing, - /// - /// The version is being saved, in order to unpublish the content. - /// - /// The Unpublishing state is transitional. Once the version - /// is saved, its state changes to Unpublished. - Unpublishing - - } + /// + /// The version is being saved, in order to unpublish the content. + /// + /// + /// The + /// Unpublishing + /// state is transitional. Once the version + /// is saved, its state changes to + /// Unpublished + /// . + /// + Unpublishing, } diff --git a/src/Umbraco.Core/Models/Range.cs b/src/Umbraco.Core/Models/Range.cs index 9c5da2087e..78d49ad851 100644 --- a/src/Umbraco.Core/Models/Range.cs +++ b/src/Umbraco.Core/Models/Range.cs @@ -1,130 +1,144 @@ -using System; using System.Globalization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a range with a minimum and maximum value. +/// +/// The type of the minimum and maximum values. +/// +public class Range : IEquatable> + where T : IComparable { /// - /// Represents a range with a minimum and maximum value. + /// Gets or sets the minimum value. /// - /// The type of the minimum and maximum values. - /// - public class Range : IEquatable> - where T : IComparable - { - /// - /// Gets or sets the minimum value. - /// - /// - /// The minimum value. - /// - public T? Minimum { get; set; } + /// + /// The minimum value. + /// + public T? Minimum { get; set; } - /// - /// Gets or sets the maximum value. - /// - /// - /// The maximum value. - /// - public T? Maximum { get; set; } + /// + /// Gets or sets the maximum value. + /// + /// + /// The maximum value. + /// + public T? Maximum { get; set; } - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() => this.ToString("{0},{1}", CultureInfo.InvariantCulture); + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// + /// if the current object is equal to the parameter; otherwise, + /// . + /// + public bool Equals(Range? other) => other != null && Equals(other.Minimum, other.Maximum); - /// - /// Returns a that represents this instance. - /// - /// A composite format string for a single value (minimum and maximum are equal). Use {0} for the minimum and {1} for the maximum value. - /// A composite format string for the range values. Use {0} for the minimum and {1} for the maximum value. - /// An object that supplies culture-specific formatting information. - /// - /// A that represents this instance. - /// - public string ToString(string format, string formatRange, IFormatProvider? provider = null) => this.ToString(this.Minimum?.CompareTo(this.Maximum) == 0 ? format : formatRange, provider); + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => ToString("{0},{1}", CultureInfo.InvariantCulture); - /// - /// Returns a that represents this instance. - /// - /// A composite format string for the range values. Use {0} for the minimum and {1} for the maximum value. - /// An object that supplies culture-specific formatting information. - /// - /// A that represents this instance. - /// - public string ToString(string format, IFormatProvider? provider = null) => string.Format(provider, format, this.Minimum, this.Maximum); + /// + /// Returns a that represents this instance. + /// + /// + /// A composite format string for a single value (minimum and maximum are equal). Use {0} for the + /// minimum and {1} for the maximum value. + /// + /// + /// A composite format string for the range values. Use {0} for the minimum and {1} for the + /// maximum value. + /// + /// An object that supplies culture-specific formatting information. + /// + /// A that represents this instance. + /// + public string ToString(string format, string formatRange, IFormatProvider? provider = null) => + ToString(Minimum?.CompareTo(Maximum) == 0 ? format : formatRange, provider); - /// - /// Determines whether this range is valid (the minimum value is lower than or equal to the maximum value). - /// - /// - /// true if this range is valid; otherwise, false. - /// - public bool IsValid() => this.Minimum?.CompareTo(this.Maximum) <= 0; + /// + /// Returns a that represents this instance. + /// + /// + /// A composite format string for the range values. Use {0} for the minimum and {1} for the maximum + /// value. + /// + /// An object that supplies culture-specific formatting information. + /// + /// A that represents this instance. + /// + public string ToString(string format, IFormatProvider? provider = null) => + string.Format(provider, format, Minimum, Maximum); - /// - /// Determines whether this range contains the specified value. - /// - /// The value. - /// - /// true if this range contains the specified value; otherwise, false. - /// - public bool ContainsValue(T? value) => this.Minimum?.CompareTo(value) <= 0 && value?.CompareTo(this.Maximum) <= 0; + /// + /// Determines whether this range is valid (the minimum value is lower than or equal to the maximum value). + /// + /// + /// true if this range is valid; otherwise, false. + /// + public bool IsValid() => Minimum?.CompareTo(Maximum) <= 0; - /// - /// Determines whether this range is inside the specified range. - /// - /// The range. - /// - /// true if this range is inside the specified range; otherwise, false. - /// - public bool IsInsideRange(Range range) => this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum); + /// + /// Determines whether this range contains the specified value. + /// + /// The value. + /// + /// true if this range contains the specified value; otherwise, false. + /// + public bool ContainsValue(T? value) => Minimum?.CompareTo(value) <= 0 && value?.CompareTo(Maximum) <= 0; - /// - /// Determines whether this range contains the specified range. - /// - /// The range. - /// - /// true if this range contains the specified range; otherwise, false. - /// - public bool ContainsRange(Range range) => this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum); + /// + /// Determines whether this range is inside the specified range. + /// + /// The range. + /// + /// true if this range is inside the specified range; otherwise, false. + /// + public bool IsInsideRange(Range range) => + IsValid() && range.IsValid() && range.ContainsValue(Minimum) && range.ContainsValue(Maximum); - /// - /// Determines whether the specified , is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object? obj) => obj is Range other && this.Equals(other); + /// + /// Determines whether this range contains the specified range. + /// + /// The range. + /// + /// true if this range contains the specified range; otherwise, false. + /// + public bool ContainsRange(Range range) => + IsValid() && range.IsValid() && ContainsValue(range.Minimum) && ContainsValue(range.Maximum); - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// An object to compare with this object. - /// - /// if the current object is equal to the parameter; otherwise, . - /// - public bool Equals(Range? other) => other != null && this.Equals(other.Minimum, other.Maximum); + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object? obj) => obj is Range other && Equals(other); - /// - /// Determines whether the specified and values are equal to this instance values. - /// - /// The minimum value. - /// The maximum value. - /// - /// true if the specified and values are equal to this instance values; otherwise, false. - /// - public bool Equals(T? minimum, T? maximum) => this.Minimum?.CompareTo(minimum) == 0 && this.Maximum?.CompareTo(maximum) == 0; + /// + /// Determines whether the specified and values are equal to + /// this instance values. + /// + /// The minimum value. + /// The maximum value. + /// + /// true if the specified and values are equal to this + /// instance values; otherwise, false. + /// + public bool Equals(T? minimum, T? maximum) => Minimum?.CompareTo(minimum) == 0 && Maximum?.CompareTo(maximum) == 0; - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() => (this.Minimum, this.Maximum).GetHashCode(); - } + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() => (Minimum, Maximum).GetHashCode(); } diff --git a/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs index cbb1e51a3e..77b6253178 100644 --- a/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs +++ b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs @@ -1,42 +1,37 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public struct ReadOnlyContentBaseAdapter : IReadOnlyContentBase { - public struct ReadOnlyContentBaseAdapter : IReadOnlyContentBase - { - private readonly IContentBase _content; + private readonly IContentBase _content; - private ReadOnlyContentBaseAdapter(IContentBase content) - { - _content = content ?? throw new ArgumentNullException(nameof(content)); - } + private ReadOnlyContentBaseAdapter(IContentBase content) => + _content = content ?? throw new ArgumentNullException(nameof(content)); - public static ReadOnlyContentBaseAdapter Create(IContentBase content) => new ReadOnlyContentBaseAdapter(content); + public int Id => _content.Id; - public int Id => _content.Id; + public static ReadOnlyContentBaseAdapter Create(IContentBase content) => new(content); - public Guid Key => _content.Key; + public Guid Key => _content.Key; - public DateTime CreateDate => _content.CreateDate; + public DateTime CreateDate => _content.CreateDate; - public DateTime UpdateDate => _content.UpdateDate; + public DateTime UpdateDate => _content.UpdateDate; - public string? Name => _content.Name; + public string? Name => _content.Name; - public int CreatorId => _content.CreatorId; + public int CreatorId => _content.CreatorId; - public int ParentId => _content.ParentId; + public int ParentId => _content.ParentId; - public int Level => _content.Level; + public int Level => _content.Level; - public string? Path => _content.Path; + public string? Path => _content.Path; - public int SortOrder => _content.SortOrder; + public int SortOrder => _content.SortOrder; - public int ContentTypeId => _content.ContentTypeId; + public int ContentTypeId => _content.ContentTypeId; - public int WriterId => _content.WriterId; + public int WriterId => _content.WriterId; - public int VersionId => _content.VersionId; - } + public int VersionId => _content.VersionId; } diff --git a/src/Umbraco.Core/Models/ReadOnlyRelation.cs b/src/Umbraco.Core/Models/ReadOnlyRelation.cs index a57a5ba7e1..4388499e98 100644 --- a/src/Umbraco.Core/Models/ReadOnlyRelation.cs +++ b/src/Umbraco.Core/Models/ReadOnlyRelation.cs @@ -1,35 +1,37 @@ -using System; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// A read only relation. Can be used to bulk save witch performs better than the normal save operation, +/// but do not populate Ids back to the model +/// +public class ReadOnlyRelation { - /// - /// A read only relation. Can be used to bulk save witch performs better than the normal save operation, - /// but do not populate Ids back to the model - /// - public class ReadOnlyRelation + public ReadOnlyRelation(int id, int parentId, int childId, int relationTypeId, DateTime createDate, string comment) { - public ReadOnlyRelation(int id, int parentId, int childId, int relationTypeId, DateTime createDate, string comment) - { - Id = id; - ParentId = parentId; - ChildId = childId; - RelationTypeId = relationTypeId; - CreateDate = createDate; - Comment = comment; - } - - public ReadOnlyRelation(int parentId, int childId, int relationTypeId): this(0, parentId, childId, relationTypeId, DateTime.Now, string.Empty) - { - - } - - public int Id { get; } - public int ParentId { get; } - public int ChildId { get; } - public int RelationTypeId { get; } - public DateTime CreateDate { get; } - public string Comment { get; } - - public bool HasIdentity => Id != 0; + Id = id; + ParentId = parentId; + ChildId = childId; + RelationTypeId = relationTypeId; + CreateDate = createDate; + Comment = comment; } + + public ReadOnlyRelation(int parentId, int childId, int relationTypeId) + : this(0, parentId, childId, relationTypeId, DateTime.Now, string.Empty) + { + } + + public int Id { get; } + + public int ParentId { get; } + + public int ChildId { get; } + + public int RelationTypeId { get; } + + public DateTime CreateDate { get; } + + public string Comment { get; } + + public bool HasIdentity => Id != 0; } diff --git a/src/Umbraco.Core/Models/RedirectUrl.cs b/src/Umbraco.Core/Models/RedirectUrl.cs index d4acc0b66d..ed0cde70bd 100644 --- a/src/Umbraco.Core/Models/RedirectUrl.cs +++ b/src/Umbraco.Core/Models/RedirectUrl.cs @@ -1,64 +1,62 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Implements . +/// +[Serializable] +[DataContract(IsReference = true)] +public class RedirectUrl : EntityBase, IRedirectUrl { + private int _contentId; + private Guid _contentKey; + private DateTime _createDateUtc; + private string? _culture; + private string _url; + /// - /// Implements . + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class RedirectUrl : EntityBase, IRedirectUrl + public RedirectUrl() { - /// - /// Initializes a new instance of the class. - /// - public RedirectUrl() - { - CreateDateUtc = DateTime.UtcNow; - _url = string.Empty; - } + CreateDateUtc = DateTime.UtcNow; + _url = string.Empty; + } - private int _contentId; - private Guid _contentKey; - private DateTime _createDateUtc; - private string? _culture; - private string _url; + /// + public int ContentId + { + get => _contentId; + set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(ContentId)); + } - /// - public int ContentId - { - get => _contentId; - set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(ContentId)); - } + /// + public Guid ContentKey + { + get => _contentKey; + set => SetPropertyValueAndDetectChanges(value, ref _contentKey, nameof(ContentKey)); + } - /// - public Guid ContentKey - { - get => _contentKey; - set => SetPropertyValueAndDetectChanges(value, ref _contentKey, nameof(ContentKey)); - } + /// + public DateTime CreateDateUtc + { + get => _createDateUtc; + set => SetPropertyValueAndDetectChanges(value, ref _createDateUtc, nameof(CreateDateUtc)); + } - /// - public DateTime CreateDateUtc - { - get => _createDateUtc; - set => SetPropertyValueAndDetectChanges(value, ref _createDateUtc, nameof(CreateDateUtc)); - } + /// + public string? Culture + { + get => _culture; + set => SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); + } - /// - public string? Culture - { - get => _culture; - set => SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); - } - - /// - public string Url - { - get => _url; - set => SetPropertyValueAndDetectChanges(value, ref _url!, nameof(Url)); - } + /// + public string Url + { + get => _url; + set => SetPropertyValueAndDetectChanges(value, ref _url!, nameof(Url)); } } diff --git a/src/Umbraco.Core/Models/Relation.cs b/src/Umbraco.Core/Models/Relation.cs index 54227db910..c495ed39fb 100644 --- a/src/Umbraco.Core/Models/Relation.cs +++ b/src/Umbraco.Core/Models/Relation.cs @@ -1,103 +1,102 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Relation between two items +/// +[Serializable] +[DataContract(IsReference = true)] +public class Relation : EntityBase, IRelation { + private int _childId; + + private string? _comment; + + // NOTE: The datetime column from umbracoRelation is set on CreateDate on the Entity + private int _parentId; + private IRelationType _relationType; + /// - /// Represents a Relation between two items + /// Constructor for constructing the entity to be created /// - [Serializable] - [DataContract(IsReference = true)] - public class Relation : EntityBase, IRelation + /// + /// + /// + public Relation(int parentId, int childId, IRelationType relationType) { - //NOTE: The datetime column from umbracoRelation is set on CreateDate on the Entity - private int _parentId; - private int _childId; - private IRelationType _relationType; - private string? _comment; - - /// - /// Constructor for constructing the entity to be created - /// - /// - /// - /// - public Relation(int parentId, int childId, IRelationType relationType) - { - _parentId = parentId; - _childId = childId; - _relationType = relationType; - } - - /// - /// Constructor for reconstructing the entity from the data source - /// - /// - /// - /// - /// - /// - public Relation(int parentId, int childId, Guid parentObjectType, Guid childObjectType, IRelationType relationType) - { - _parentId = parentId; - _childId = childId; - _relationType = relationType; - ParentObjectType = parentObjectType; - ChildObjectType = childObjectType; - } - - - /// - /// Gets or sets the Parent Id of the Relation (Source) - /// - [DataMember] - public int ParentId - { - get => _parentId; - set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); - } - - [DataMember] - public Guid ParentObjectType { get; set; } - - /// - /// Gets or sets the Child Id of the Relation (Destination) - /// - [DataMember] - public int ChildId - { - get => _childId; - set => SetPropertyValueAndDetectChanges(value, ref _childId, nameof(ChildId)); - } - - [DataMember] - public Guid ChildObjectType { get; set; } - - /// - /// Gets or sets the for the Relation - /// - [DataMember] - public IRelationType RelationType - { - get => _relationType; - set => SetPropertyValueAndDetectChanges(value, ref _relationType!, nameof(RelationType)); - } - - /// - /// Gets or sets a comment for the Relation - /// - [DataMember] - public string? Comment - { - get => _comment; - set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); - } - - /// - /// Gets the Id of the that this Relation is based on. - /// - [IgnoreDataMember] - public int RelationTypeId => _relationType.Id; + _parentId = parentId; + _childId = childId; + _relationType = relationType; } + + /// + /// Constructor for reconstructing the entity from the data source + /// + /// + /// + /// + /// + /// + public Relation(int parentId, int childId, Guid parentObjectType, Guid childObjectType, IRelationType relationType) + { + _parentId = parentId; + _childId = childId; + _relationType = relationType; + ParentObjectType = parentObjectType; + ChildObjectType = childObjectType; + } + + /// + /// Gets or sets the Parent Id of the Relation (Source) + /// + [DataMember] + public int ParentId + { + get => _parentId; + set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); + } + + [DataMember] + public Guid ParentObjectType { get; set; } + + /// + /// Gets or sets the Child Id of the Relation (Destination) + /// + [DataMember] + public int ChildId + { + get => _childId; + set => SetPropertyValueAndDetectChanges(value, ref _childId, nameof(ChildId)); + } + + [DataMember] + public Guid ChildObjectType { get; set; } + + /// + /// Gets or sets the for the Relation + /// + [DataMember] + public IRelationType RelationType + { + get => _relationType; + set => SetPropertyValueAndDetectChanges(value, ref _relationType!, nameof(RelationType)); + } + + /// + /// Gets or sets a comment for the Relation + /// + [DataMember] + public string? Comment + { + get => _comment; + set => SetPropertyValueAndDetectChanges(value, ref _comment, nameof(Comment)); + } + + /// + /// Gets the Id of the that this Relation is based on. + /// + [IgnoreDataMember] + public int RelationTypeId => _relationType.Id; } diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs index 75344914f0..409776b7e3 100644 --- a/src/Umbraco.Core/Models/RelationItem.cs +++ b/src/Umbraco.Core/Models/RelationItem.cs @@ -1,44 +1,40 @@ -using System; using System.Runtime.Serialization; -using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "relationItem", Namespace = "")] +public class RelationItem { - [DataContract(Name = "relationItem", Namespace = "")] - public class RelationItem - { - [DataMember(Name = "id")] - public int NodeId { get; set; } + [DataMember(Name = "id")] + public int NodeId { get; set; } - [DataMember(Name = "key")] - public Guid NodeKey { get; set; } + [DataMember(Name = "key")] + public Guid NodeKey { get; set; } - [DataMember(Name = "name")] - public string? NodeName { get; set; } + [DataMember(Name = "name")] + public string? NodeName { get; set; } - [DataMember(Name = "type")] - public string? NodeType { get; set; } + [DataMember(Name = "type")] + public string? NodeType { get; set; } - [DataMember(Name = "udi")] - public Udi NodeUdi => Udi.Create(NodeType, NodeKey); + [DataMember(Name = "udi")] + public Udi NodeUdi => Udi.Create(NodeType, NodeKey); - [DataMember(Name = "icon")] - public string? ContentTypeIcon { get; set; } + [DataMember(Name = "icon")] + public string? ContentTypeIcon { get; set; } - [DataMember(Name = "alias")] - public string? ContentTypeAlias { get; set; } + [DataMember(Name = "alias")] + public string? ContentTypeAlias { get; set; } - [DataMember(Name = "contentTypeName")] - public string? ContentTypeName { get; set; } + [DataMember(Name = "contentTypeName")] + public string? ContentTypeName { get; set; } - [DataMember(Name = "relationTypeName")] - public string? RelationTypeName { get; set; } + [DataMember(Name = "relationTypeName")] + public string? RelationTypeName { get; set; } - [DataMember(Name = "relationTypeIsBidirectional")] - public bool RelationTypeIsBidirectional { get; set; } + [DataMember(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } - [DataMember(Name = "relationTypeIsDependency")] - public bool RelationTypeIsDependency { get; set; } - - } + [DataMember(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } } diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 4c4c69c5f1..d48e802c6e 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -1,107 +1,122 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a RelationType +/// +[Serializable] +[DataContract(IsReference = true)] +public class RelationType : EntityBase, IRelationTypeWithIsDependency { - /// - /// Represents a RelationType - /// - [Serializable] - [DataContract(IsReference = true)] - public class RelationType : EntityBase, IRelationType, IRelationTypeWithIsDependency + private string _alias; + private Guid? _childObjectType; + private bool _isBidirectional; + private bool _isDependency; + private string _name; + private Guid? _parentObjectType; + + public RelationType(string alias, string name) + : this(name, alias, false, null, null, false) { - private string _name; - private string _alias; - private bool _isBidirectional; - private bool _isDependency; - private Guid? _parentObjectType; - private Guid? _childObjectType; + } - public RelationType(string alias, string name) - : this(name: name, alias: alias, false, null, null, false) + [Obsolete("Use ctor with isDependency parameter")] + public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) + : this(name, alias, isBidrectional, parentObjectType, childObjectType, false) + { + } + + public RelationType(string? name, string? alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency) + { + if (name == null) { + throw new ArgumentNullException(nameof(name)); } - [Obsolete("Use ctor with isDependency parameter")] - public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) - :this(name,alias,isBidrectional, parentObjectType, childObjectType, false) + if (string.IsNullOrWhiteSpace(name)) { - + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - public RelationType(string? name, string? alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency) + if (alias == null) { - 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)); - - _name = name; - _alias = alias; - _isBidirectional = isBidrectional; - _isDependency = isDependency; - _parentObjectType = parentObjectType; - _childObjectType = childObjectType; + throw new ArgumentNullException(nameof(alias)); } - /// - /// Gets or sets the Name of the RelationType - /// - [DataMember] - public string? Name + if (string.IsNullOrWhiteSpace(alias)) { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(alias)); } - /// - /// Gets or sets the Alias of the RelationType - /// - [DataMember] - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); - } + _name = name; + _alias = alias; + _isBidirectional = isBidrectional; + _isDependency = isDependency; + _parentObjectType = parentObjectType; + _childObjectType = childObjectType; + } - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember] - public bool IsBidirectional - { - get => _isBidirectional; - set => SetPropertyValueAndDetectChanges(value, ref _isBidirectional, nameof(IsBidirectional)); - } + /// + /// Gets or sets the Name of the RelationType + /// + [DataMember] + public string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); + } - /// - /// Gets or sets the Parents object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember] - public Guid? ParentObjectType - { - get => _parentObjectType; - set => SetPropertyValueAndDetectChanges(value, ref _parentObjectType, nameof(ParentObjectType)); - } + /// + /// Gets or sets the Alias of the RelationType + /// + [DataMember] + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + } - /// - /// Gets or sets the Childs object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember] - public Guid? ChildObjectType - { - get => _childObjectType; - set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType)); - } + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember] + public bool IsBidirectional + { + get => _isBidirectional; + set => SetPropertyValueAndDetectChanges(value, ref _isBidirectional, nameof(IsBidirectional)); + } + /// + /// Gets or sets the Parents object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember] + public Guid? ParentObjectType + { + get => _parentObjectType; + set => SetPropertyValueAndDetectChanges(value, ref _parentObjectType, nameof(ParentObjectType)); + } - public bool IsDependency - { - get => _isDependency; - set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency)); - } + /// + /// Gets or sets the Childs object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember] + public Guid? ChildObjectType + { + get => _childObjectType; + set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType)); + } + + public bool IsDependency + { + get => _isDependency; + set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency)); } } diff --git a/src/Umbraco.Core/Models/RelationTypeExtensions.cs b/src/Umbraco.Core/Models/RelationTypeExtensions.cs index 1e7282b66b..b5803d3fb3 100644 --- a/src/Umbraco.Core/Models/RelationTypeExtensions.cs +++ b/src/Umbraco.Core/Models/RelationTypeExtensions.cs @@ -1,18 +1,17 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class RelationTypeExtensions { - public static class RelationTypeExtensions - { - public static bool IsSystemRelationType(this IRelationType relationType) => - relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias - || relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias - || relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias - || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias - || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - } + public static bool IsSystemRelationType(this IRelationType relationType) => + relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; } diff --git a/src/Umbraco.Core/Models/RequestPasswordResetModel.cs b/src/Umbraco.Core/Models/RequestPasswordResetModel.cs index 438e97fb30..9b4932f88a 100644 --- a/src/Umbraco.Core/Models/RequestPasswordResetModel.cs +++ b/src/Umbraco.Core/Models/RequestPasswordResetModel.cs @@ -1,14 +1,12 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models -{ +namespace Umbraco.Cms.Core.Models; - [DataContract(Name = "requestPasswordReset", Namespace = "")] - public class RequestPasswordResetModel - { - [Required] - [DataMember(Name = "email", IsRequired = true)] - public string Email { get; set; } = null!; - } +[DataContract(Name = "requestPasswordReset", Namespace = "")] +public class RequestPasswordResetModel +{ + [Required] + [DataMember(Name = "email", IsRequired = true)] + public string Email { get; set; } = null!; } diff --git a/src/Umbraco.Core/Models/Script.cs b/src/Umbraco.Core/Models/Script.cs index 0d121368f8..03888bd27a 100644 --- a/src/Umbraco.Core/Models/Script.cs +++ b/src/Umbraco.Core/Models/Script.cs @@ -1,29 +1,29 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Script file +/// +[Serializable] +[DataContract(IsReference = true)] +public class Script : File, IScript { - /// - /// Represents a Script file - /// - [Serializable] - [DataContract(IsReference = true)] - public class Script : File, IScript + public Script(string path) + : this(path, null) { - public Script(string path) - : this(path, (Func?) null) - { } - - public Script(string path, Func? getFileContent) - : base(path, getFileContent) - { } - - /// - /// Indicates whether the current entity has an identity, which in this case is a path/name. - /// - /// - /// Overrides the default Entity identity check. - /// - public override bool HasIdentity => string.IsNullOrEmpty(Path) == false; } + + public Script(string path, Func? getFileContent) + : base(path, getFileContent) + { + } + + /// + /// Indicates whether the current entity has an identity, which in this case is a path/name. + /// + /// + /// Overrides the default Entity identity check. + /// + public override bool HasIdentity => string.IsNullOrEmpty(Path) == false; } diff --git a/src/Umbraco.Core/Models/SendCodeViewModel.cs b/src/Umbraco.Core/Models/SendCodeViewModel.cs index 783bcdeec2..c73fd73eb3 100644 --- a/src/Umbraco.Core/Models/SendCodeViewModel.cs +++ b/src/Umbraco.Core/Models/SendCodeViewModel.cs @@ -1,32 +1,32 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Used for 2FA verification +/// +[DataContract(Name = "code", Namespace = "")] +public class Verify2FACodeModel { + [Required] + [DataMember(Name = "code", IsRequired = true)] + public string? Code { get; set; } + + [Required] + [DataMember(Name = "provider", IsRequired = true)] + public string? Provider { get; set; } + /// - /// Used for 2FA verification + /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// - [DataContract(Name = "code", Namespace = "")] - public class Verify2FACodeModel - { - [Required] - [DataMember(Name = "code", IsRequired = true)] - public string? Code { get; set; } + [DataMember(Name = "isPersistent", IsRequired = true)] + public bool IsPersistent { get; set; } - [Required] - [DataMember(Name = "provider", IsRequired = true)] - public string? Provider { get; set; } - - /// - /// Flag indicating whether the sign-in cookie should persist after the browser is closed. - /// - [DataMember(Name = "isPersistent", IsRequired = true)] - public bool IsPersistent { get; set; } - - /// - /// Flag indicating whether the current browser should be remember, suppressing all further two factor authentication prompts. - /// - [DataMember(Name = "rememberClient", IsRequired = true)] - public bool RememberClient { get; set; } - } + /// + /// Flag indicating whether the current browser should be remember, suppressing all further two factor authentication + /// prompts. + /// + [DataMember(Name = "rememberClient", IsRequired = true)] + public bool RememberClient { get; set; } } diff --git a/src/Umbraco.Core/Models/ServerRegistration.cs b/src/Umbraco.Core/Models/ServerRegistration.cs index 553460eb5b..6507d5d64c 100644 --- a/src/Umbraco.Core/Models/ServerRegistration.cs +++ b/src/Umbraco.Core/Models/ServerRegistration.cs @@ -1,124 +1,120 @@ -using System; using System.Globalization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a registered server in a multiple-servers environment. +/// +public class ServerRegistration : EntityBase, IServerRegistration { + private bool _isActive; + private bool _isSchedulingPublisher; + private string? _serverAddress; + private string? _serverIdentity; + /// - /// Represents a registered server in a multiple-servers environment. + /// Initializes a new instance of the class. /// - public class ServerRegistration : EntityBase, IServerRegistration + public ServerRegistration() { - private string? _serverAddress; - private string? _serverIdentity; - private bool _isActive; - private bool _isSchedulingPublisher; - - /// - /// Initializes a new instance of the class. - /// - public ServerRegistration() - { } - - /// - /// Initializes a new instance of the class. - /// - /// The unique id of the server registration. - /// The server URL. - /// The unique server identity. - /// The date and time the registration was created. - /// The date and time the registration was last accessed. - /// A value indicating whether the registration is active. - /// A value indicating whether the registration is master. - public ServerRegistration(int id, string? serverAddress, string? serverIdentity, DateTime registered, DateTime accessed, bool isActive, bool isSchedulingPublisher) - { - UpdateDate = accessed; - CreateDate = registered; - Key = id.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); - Id = id; - ServerAddress = serverAddress; - ServerIdentity = serverIdentity; - IsActive = isActive; - IsSchedulingPublisher = isSchedulingPublisher; - } - - /// - /// Initializes a new instance of the class. - /// - /// The server URL. - /// The unique server identity. - /// The date and time the registration was created. - public ServerRegistration(string serverAddress, string serverIdentity, DateTime registered) - { - CreateDate = registered; - UpdateDate = registered; - Key = 0.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); - ServerAddress = serverAddress; - ServerIdentity = serverIdentity; - } - - /// - /// Gets or sets the server URL. - /// - public string? ServerAddress - { - get => _serverAddress; - set => SetPropertyValueAndDetectChanges(value, ref _serverAddress, nameof(ServerAddress)); - } - - /// - /// Gets or sets the server unique identity. - /// - public string? ServerIdentity - { - get => _serverIdentity; - set => SetPropertyValueAndDetectChanges(value, ref _serverIdentity, nameof(ServerIdentity)); - } - - /// - /// Gets or sets a value indicating whether the server is active. - /// - public bool IsActive - { - get => _isActive; - set => SetPropertyValueAndDetectChanges(value, ref _isActive, nameof(IsActive)); - } - - /// - /// Gets or sets a value indicating whether the server has the SchedulingPublisher role - /// - public bool IsSchedulingPublisher - { - get => _isSchedulingPublisher; - set => SetPropertyValueAndDetectChanges(value, ref _isSchedulingPublisher, nameof(IsSchedulingPublisher)); - } - - /// - /// Gets the date and time the registration was created. - /// - public DateTime Registered - { - get => CreateDate; - set => CreateDate = value; - } - - /// - /// Gets the date and time the registration was last accessed. - /// - public DateTime Accessed - { - get => UpdateDate; - set => UpdateDate = value; - } - - /// - /// Converts the value of this instance to its equivalent string representation. - /// - /// - public override string ToString() - { - return string.Format("{{\"{0}\", \"{1}\", {2}active, {3}master}}", ServerAddress, ServerIdentity, IsActive ? "" : "!", IsSchedulingPublisher ? "" : "!"); - } } + + /// + /// Initializes a new instance of the class. + /// + /// The unique id of the server registration. + /// The server URL. + /// The unique server identity. + /// The date and time the registration was created. + /// The date and time the registration was last accessed. + /// A value indicating whether the registration is active. + /// A value indicating whether the registration is scheduling publisher. + public ServerRegistration(int id, string? serverAddress, string? serverIdentity, DateTime registered, DateTime accessed, bool isActive, bool isSchedulingPublisher) + { + UpdateDate = accessed; + CreateDate = registered; + Key = id.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); + Id = id; + ServerAddress = serverAddress; + ServerIdentity = serverIdentity; + IsActive = isActive; + IsSchedulingPublisher = isSchedulingPublisher; + } + + /// + /// Initializes a new instance of the class. + /// + /// The server URL. + /// The unique server identity. + /// The date and time the registration was created. + public ServerRegistration(string serverAddress, string serverIdentity, DateTime registered) + { + CreateDate = registered; + UpdateDate = registered; + Key = 0.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); + ServerAddress = serverAddress; + ServerIdentity = serverIdentity; + } + + /// + /// Gets or sets the server URL. + /// + public string? ServerAddress + { + get => _serverAddress; + set => SetPropertyValueAndDetectChanges(value, ref _serverAddress, nameof(ServerAddress)); + } + + /// + /// Gets or sets the server unique identity. + /// + public string? ServerIdentity + { + get => _serverIdentity; + set => SetPropertyValueAndDetectChanges(value, ref _serverIdentity, nameof(ServerIdentity)); + } + + /// + /// Gets or sets a value indicating whether the server is active. + /// + public bool IsActive + { + get => _isActive; + set => SetPropertyValueAndDetectChanges(value, ref _isActive, nameof(IsActive)); + } + + /// + /// Gets or sets a value indicating whether the server has the SchedulingPublisher role + /// + public bool IsSchedulingPublisher + { + get => _isSchedulingPublisher; + set => SetPropertyValueAndDetectChanges(value, ref _isSchedulingPublisher, nameof(IsSchedulingPublisher)); + } + + /// + /// Gets the date and time the registration was created. + /// + public DateTime Registered + { + get => CreateDate; + set => CreateDate = value; + } + + /// + /// Gets the date and time the registration was last accessed. + /// + public DateTime Accessed + { + get => UpdateDate; + set => UpdateDate = value; + } + + /// + /// Converts the value of this instance to its equivalent string representation. + /// + /// + public override string ToString() => string.Format("{{\"{0}\", \"{1}\", {2}active, {3}master}}", ServerAddress, ServerIdentity, IsActive ? string.Empty : "!", IsSchedulingPublisher ? string.Empty : "!"); } diff --git a/src/Umbraco.Core/Models/SetPasswordModel.cs b/src/Umbraco.Core/Models/SetPasswordModel.cs index c904f98694..57d1abc38f 100644 --- a/src/Umbraco.Core/Models/SetPasswordModel.cs +++ b/src/Umbraco.Core/Models/SetPasswordModel.cs @@ -1,21 +1,20 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "setPassword", Namespace = "")] +public class SetPasswordModel { - [DataContract(Name = "setPassword", Namespace = "")] - public class SetPasswordModel - { - [Required] - [DataMember(Name = "userId", IsRequired = true)] - public int UserId { get; set; } + [Required] + [DataMember(Name = "userId", IsRequired = true)] + public int UserId { get; set; } - [Required] - [DataMember(Name = "password", IsRequired = true)] - public string? Password { get; set; } + [Required] + [DataMember(Name = "password", IsRequired = true)] + public string? Password { get; set; } - [Required] - [DataMember(Name = "resetCode", IsRequired = true)] - public string? ResetCode { get; set; } - } + [Required] + [DataMember(Name = "resetCode", IsRequired = true)] + public string? ResetCode { get; set; } } diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs index 31e061362c..7fe88a8a8a 100644 --- a/src/Umbraco.Core/Models/SimpleContentType.cs +++ b/src/Umbraco.Core/Models/SimpleContentType.cs @@ -1,99 +1,108 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Implements . +/// +public class SimpleContentType : ISimpleContentType { /// - /// Implements . + /// Initializes a new instance of the class. /// - public class SimpleContentType : ISimpleContentType + public SimpleContentType(IContentType contentType) + : this((IContentTypeBase)contentType) => + DefaultTemplate = contentType.DefaultTemplate; + + /// + /// Initializes a new instance of the class. + /// + public SimpleContentType(IMediaType mediaType) + : this((IContentTypeBase)mediaType) { - /// - /// Initializes a new instance of the class. - /// - public SimpleContentType(IContentType contentType) - : this((IContentTypeBase)contentType) + } + + /// + /// Initializes a new instance of the class. + /// + public SimpleContentType(IMemberType memberType) + : this((IContentTypeBase)memberType) + { + } + + private SimpleContentType(IContentTypeBase contentType) + { + if (contentType == null) { - DefaultTemplate = contentType.DefaultTemplate; + throw new ArgumentNullException(nameof(contentType)); } - /// - /// Initializes a new instance of the class. - /// - public SimpleContentType(IMediaType mediaType) - : this((IContentTypeBase)mediaType) - { } + Id = contentType.Id; + Key = contentType.Key; + Alias = contentType.Alias; + Variations = contentType.Variations; + Icon = contentType.Icon; + IsContainer = contentType.IsContainer; + Name = contentType.Name; + AllowedAsRoot = contentType.AllowedAsRoot; + IsElement = contentType.IsElement; + } - /// - /// Initializes a new instance of the class. - /// - public SimpleContentType(IMemberType memberType) - : this((IContentTypeBase)memberType) - { } + public string Alias { get; } - private SimpleContentType(IContentTypeBase contentType) + public int Id { get; } + + public Guid Key { get; } + + /// + public ITemplate? DefaultTemplate { get; } + + public ContentVariation Variations { get; } + + public string? Icon { get; } + + public bool IsContainer { get; } + + public string? Name { get; } + + public bool AllowedAsRoot { get; } + + public bool IsElement { get; } + + public bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false) => + + // non-exact validation: can accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + Variations.ValidateVariation(culture, segment, false, wildcards, false); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (contentType == null) throw new ArgumentNullException(nameof(contentType)); - - Id = contentType.Id; - Key = contentType.Key; - Alias = contentType.Alias; - Variations = contentType.Variations; - Icon = contentType.Icon; - IsContainer = contentType.IsContainer; - Name = contentType.Name; - AllowedAsRoot = contentType.AllowedAsRoot; - IsElement = contentType.IsElement; + return false; } - public string Alias { get; } - - public int Id { get; } - - public Guid Key { get; } - - /// - public ITemplate? DefaultTemplate { get; } - - public ContentVariation Variations { get; } - - public string? Icon { get; } - - public bool IsContainer { get; } - - public string? Name { get; } - - public bool AllowedAsRoot { get; } - - public bool IsElement { get; } - - public bool SupportsPropertyVariation(string? culture, string segment, bool wildcards = false) + if (ReferenceEquals(this, obj)) { - // non-exact validation: can accept a 'null' culture if the property type varies - // by culture, and likewise for segment - // wildcard validation: can accept a '*' culture or segment - return Variations.ValidateVariation(culture, segment, false, wildcards, false); + return true; } - protected bool Equals(SimpleContentType other) + if (obj.GetType() != GetType()) { - return string.Equals(Alias, other.Alias) && Id == other.Id; + return false; } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((SimpleContentType) obj); - } + return Equals((SimpleContentType)obj); + } - public override int GetHashCode() + protected bool Equals(SimpleContentType other) => string.Equals(Alias, other.Alias) && Id == other.Id; + + public override int GetHashCode() + { + unchecked { - unchecked - { - return ((Alias != null ? Alias.GetHashCode() : 0) * 397) ^ Id; - } + return ((Alias != null ? Alias.GetHashCode() : 0) * 397) ^ Id; } - } + } } diff --git a/src/Umbraco.Core/Models/SimpleValidationModel.cs b/src/Umbraco.Core/Models/SimpleValidationModel.cs index 30efec7dfe..390fe5a31c 100644 --- a/src/Umbraco.Core/Models/SimpleValidationModel.cs +++ b/src/Umbraco.Core/Models/SimpleValidationModel.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +public class SimpleValidationModel { - public class SimpleValidationModel + public SimpleValidationModel(IDictionary modelState, string message = "The request is invalid.") { - public SimpleValidationModel(IDictionary modelState, string message = "The request is invalid.") - { - Message = message; - ModelState = modelState; - } - - public string Message { get; } - public IDictionary ModelState { get; } + Message = message; + ModelState = modelState; } + + public string Message { get; } + + public IDictionary ModelState { get; } } diff --git a/src/Umbraco.Core/Models/Stylesheet.cs b/src/Umbraco.Core/Models/Stylesheet.cs index 7b1d971434..07f35c88e3 100644 --- a/src/Umbraco.Core/Models/Stylesheet.cs +++ b/src/Umbraco.Core/Models/Stylesheet.cs @@ -1,178 +1,166 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Data; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings.Css; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Stylesheet file +/// +[Serializable] +[DataContract(IsReference = true)] +public class Stylesheet : File, IStylesheet { - /// - /// Represents a Stylesheet file - /// - [Serializable] - [DataContract(IsReference = true)] - public class Stylesheet : File, IStylesheet + private Lazy>? _properties; + + public Stylesheet(string path) + : this(path, null) { - public Stylesheet(string path) - : this(path, null) - { } + } - public Stylesheet(string path, Func? getFileContent) - : base(string.IsNullOrEmpty(path) ? path : path.EnsureEndsWith(".css"), getFileContent) + public Stylesheet(string path, Func? getFileContent) + : base(string.IsNullOrEmpty(path) ? path : path.EnsureEndsWith(".css"), getFileContent) => + InitializeProperties(); + + /// + /// Gets or sets the Content of a File + /// + public override string? Content + { + get => base.Content; + set { + base.Content = value; + + // re-set the properties so they are re-read from the content InitializeProperties(); } + } - private Lazy>? _properties; + /// + /// Returns a list of umbraco back office enabled stylesheet properties + /// + /// + /// An umbraco back office enabled stylesheet property has a special prefix, for example: + /// /** umb_name: MyPropertyName */ p { font-size: 1em; } + /// + [IgnoreDataMember] + public IEnumerable? Properties => _properties?.Value; - private void InitializeProperties() + /// + /// Indicates whether the current entity has an identity, which in this case is a path/name. + /// + /// + /// Overrides the default Entity identity check. + /// + public override bool HasIdentity => string.IsNullOrEmpty(Path) == false; + + /// + /// Adds an Umbraco stylesheet property for use in the back office + /// + /// + public void AddProperty(IStylesheetProperty property) + { + if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(property.Name))) { - //if the value is already created, we need to be created and update the collection according to - //what is now in the content - if (_properties != null && _properties.IsValueCreated) - { - //re-parse it so we can check what properties are different and adjust the event handlers - var parsed = StylesheetHelper.ParseRules(Content).ToArray(); - var names = parsed.Select(x => x.Name).ToArray(); - var existing = _properties.Value.Where(x => names.InvariantContains(x.Name)).ToArray(); - //update existing - foreach (var stylesheetProperty in existing) - { - var updateFrom = parsed.Single(x => x.Name.InvariantEquals(stylesheetProperty.Name)); - //remove current event handler while we update, we'll reset it after - stylesheetProperty.PropertyChanged -= Property_PropertyChanged; - stylesheetProperty.Alias = updateFrom.Selector; - stylesheetProperty.Value = updateFrom.Styles; - //re-add - stylesheetProperty.PropertyChanged += Property_PropertyChanged; - } - //remove no longer existing - var nonExisting = _properties.Value.Where(x => names.InvariantContains(x.Name) == false).ToArray(); - foreach (var stylesheetProperty in nonExisting) - { - stylesheetProperty.PropertyChanged -= Property_PropertyChanged; - _properties.Value.Remove(stylesheetProperty); - } - //add new ones - var newItems = parsed.Where(x => _properties.Value.Select(p => p.Name).InvariantContains(x.Name) == false); - foreach (var stylesheetRule in newItems) - { - var prop = new StylesheetProperty(stylesheetRule.Name, stylesheetRule.Selector, stylesheetRule.Styles); - prop.PropertyChanged += Property_PropertyChanged; - _properties.Value.Add(prop); - } - } - - //we haven't read the properties yet so create the lazy delegate - _properties = new Lazy>(() => - { - var parsed = StylesheetHelper.ParseRules(Content); - return parsed.Select(statement => - { - var property = new StylesheetProperty(statement.Name, statement.Selector, statement.Styles); - property.PropertyChanged += Property_PropertyChanged; - return property; - - }).ToList(); - }); + throw new DuplicateNameException("The property with the name " + property.Name + + " already exists in the collection"); } - /// - /// If the property has changed then we need to update the content - /// - /// - /// - void Property_PropertyChanged(object? sender, PropertyChangedEventArgs e) - { - var prop = (StylesheetProperty?) sender; + // now we need to serialize out the new property collection over-top of the string Content. + Content = StylesheetHelper.AppendRule( + Content, + new StylesheetRule { Name = property.Name, Selector = property.Alias, Styles = property.Value }); - if (prop is not null) + // re-set lazy collection + InitializeProperties(); + } + + /// + /// Removes an Umbraco stylesheet property + /// + /// + public void RemoveProperty(string name) + { + if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(name))) + { + Content = StylesheetHelper.ReplaceRule(Content, name, null); + } + } + + private void InitializeProperties() + { + // if the value is already created, we need to be created and update the collection according to + // what is now in the content + if (_properties != null && _properties.IsValueCreated) + { + // re-parse it so we can check what properties are different and adjust the event handlers + StylesheetRule[] parsed = StylesheetHelper.ParseRules(Content).ToArray(); + var names = parsed.Select(x => x.Name).ToArray(); + StylesheetProperty[] existing = _properties.Value.Where(x => names.InvariantContains(x.Name)).ToArray(); + + // update existing + foreach (StylesheetProperty stylesheetProperty in existing) { - //Ensure we are setting base.Content here so that the properties don't get reset and thus any event handlers would get reset too - base.Content = StylesheetHelper.ReplaceRule(Content, prop.Name, new StylesheetRule - { - Name = prop.Name, - Selector = prop.Alias, - Styles = prop.Value - }); + StylesheetRule updateFrom = parsed.Single(x => x.Name.InvariantEquals(stylesheetProperty.Name)); + + // remove current event handler while we update, we'll reset it after + stylesheetProperty.PropertyChanged -= Property_PropertyChanged; + stylesheetProperty.Alias = updateFrom.Selector; + stylesheetProperty.Value = updateFrom.Styles; + + // re-add + stylesheetProperty.PropertyChanged += Property_PropertyChanged; + } + + // remove no longer existing + StylesheetProperty[] nonExisting = + _properties.Value.Where(x => names.InvariantContains(x.Name) == false).ToArray(); + foreach (StylesheetProperty stylesheetProperty in nonExisting) + { + stylesheetProperty.PropertyChanged -= Property_PropertyChanged; + _properties.Value.Remove(stylesheetProperty); + } + + // add new ones + IEnumerable newItems = parsed.Where(x => + _properties.Value.Select(p => p.Name).InvariantContains(x.Name) == false); + foreach (StylesheetRule stylesheetRule in newItems) + { + var prop = new StylesheetProperty(stylesheetRule.Name, stylesheetRule.Selector, stylesheetRule.Styles); + prop.PropertyChanged += Property_PropertyChanged; + _properties.Value.Add(prop); } } - /// - /// Gets or sets the Content of a File - /// - public override string? Content + // we haven't read the properties yet so create the lazy delegate + _properties = new Lazy>(() => { - get { return base.Content; } - set + IEnumerable parsed = StylesheetHelper.ParseRules(Content); + return parsed.Select(statement => { - base.Content = value; - //re-set the properties so they are re-read from the content - InitializeProperties(); - } - } + var property = new StylesheetProperty(statement.Name, statement.Selector, statement.Styles); + property.PropertyChanged += Property_PropertyChanged; + return property; + }).ToList(); + }); + } - /// - /// Returns a list of umbraco back office enabled stylesheet properties - /// - /// - /// An umbraco back office enabled stylesheet property has a special prefix, for example: - /// - /// /** umb_name: MyPropertyName */ p { font-size: 1em; } - /// - [IgnoreDataMember] - public IEnumerable? Properties + /// + /// If the property has changed then we need to update the content + /// + /// + /// + private void Property_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + var prop = (StylesheetProperty?)sender; + + if (prop is not null) { - get { return _properties?.Value; } - } - - /// - /// Adds an Umbraco stylesheet property for use in the back office - /// - /// - public void AddProperty(IStylesheetProperty property) - { - if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(property.Name))) - { - throw new DuplicateNameException("The property with the name " + property.Name + " already exists in the collection"); - } - - //now we need to serialize out the new property collection over-top of the string Content. - Content = StylesheetHelper.AppendRule(Content, new StylesheetRule - { - Name = property.Name, - Selector = property.Alias, - Styles = property.Value - }); - - //re-set lazy collection - InitializeProperties(); - } - - /// - /// Removes an Umbraco stylesheet property - /// - /// - public void RemoveProperty(string name) - { - if (Properties is not null && Properties.Any(x => x.Name.InvariantEquals(name))) - { - Content = StylesheetHelper.ReplaceRule(Content, name, null); - } - } - - /// - /// Indicates whether the current entity has an identity, which in this case is a path/name. - /// - /// - /// Overrides the default Entity identity check. - /// - public override bool HasIdentity - { - get { return string.IsNullOrEmpty(Path) == false; } + // Ensure we are setting base.Content here so that the properties don't get reset and thus any event handlers would get reset too + base.Content = StylesheetHelper.ReplaceRule(Content, prop.Name, new StylesheetRule { Name = prop.Name, Selector = prop.Alias, Styles = prop.Value }); } } } diff --git a/src/Umbraco.Core/Models/StylesheetProperty.cs b/src/Umbraco.Core/Models/StylesheetProperty.cs index af6f347a63..730ff8ff3e 100644 --- a/src/Umbraco.Core/Models/StylesheetProperty.cs +++ b/src/Umbraco.Core/Models/StylesheetProperty.cs @@ -1,51 +1,48 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Stylesheet Property +/// +/// +/// Properties are always formatted to have a single selector, so it can be used in the backoffice +/// +[Serializable] +[DataContract(IsReference = true)] +public class StylesheetProperty : BeingDirtyBase, IValueObject, IStylesheetProperty { - /// - /// Represents a Stylesheet Property - /// - /// - /// Properties are always formatted to have a single selector, so it can be used in the backoffice - /// - [Serializable] - [DataContract(IsReference = true)] - public class StylesheetProperty : BeingDirtyBase, IValueObject, IStylesheetProperty + private string _alias; + private string _value; + + public StylesheetProperty(string name, string alias, string value) { - private string _alias; - private string _value; + Name = name; + _alias = alias; + _value = value; + } - public StylesheetProperty(string name, string @alias, string value) - { - Name = name; - _alias = alias; - _value = value; - } + /// + /// The CSS rule name that can be used by Umbraco in the back office + /// + public string Name { get; private set; } - /// - /// The CSS rule name that can be used by Umbraco in the back office - /// - public string Name { get; private set; } - - /// - /// This is the CSS Selector - /// - public string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); - } - - /// - /// The CSS value for the selector - /// - public string Value - { - get => _value; - set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); - } + /// + /// This is the CSS Selector + /// + public string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges(value, ref _alias!, nameof(Alias)); + } + /// + /// The CSS value for the selector + /// + public string Value + { + get => _value; + set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); } } diff --git a/src/Umbraco.Core/Models/Tag.cs b/src/Umbraco.Core/Models/Tag.cs index 92436d068b..1c4bf4b88c 100644 --- a/src/Umbraco.Core/Models/Tag.cs +++ b/src/Umbraco.Core/Models/Tag.cs @@ -1,59 +1,58 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a tag entity. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Tag : EntityBase, ITag { + private string _group = string.Empty; + private int? _languageId; + private string _text = string.Empty; + /// - /// Represents a tag entity. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class Tag : EntityBase, ITag + public Tag() { - private string _group = string.Empty; - private string _text = string.Empty; - private int? _languageId; - - /// - /// Initializes a new instance of the class. - /// - public Tag() - { } - - /// - /// Initializes a new instance of the class. - /// - public Tag(int id, string group, string text, int? languageId = null) - { - Id = id; - Text = text; - Group = group; - LanguageId = languageId; - } - - /// - public string Group - { - get => _group; - set => SetPropertyValueAndDetectChanges(value, ref _group!, nameof(Group)); - } - - /// - public string Text - { - get => _text; - set => SetPropertyValueAndDetectChanges(value, ref _text!, nameof(Text)); - } - - /// - public int? LanguageId - { - get => _languageId; - set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); - } - - /// - public int NodeCount { get; set; } } + + /// + /// Initializes a new instance of the class. + /// + public Tag(int id, string group, string text, int? languageId = null) + { + Id = id; + Text = text; + Group = group; + LanguageId = languageId; + } + + /// + public string Group + { + get => _group; + set => SetPropertyValueAndDetectChanges(value, ref _group!, nameof(Group)); + } + + /// + public string Text + { + get => _text; + set => SetPropertyValueAndDetectChanges(value, ref _text!, nameof(Text)); + } + + /// + public int? LanguageId + { + get => _languageId; + set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); + } + + /// + public int NodeCount { get; set; } } diff --git a/src/Umbraco.Core/Models/TagModel.cs b/src/Umbraco.Core/Models/TagModel.cs index 6a0430a492..2646b216e3 100644 --- a/src/Umbraco.Core/Models/TagModel.cs +++ b/src/Umbraco.Core/Models/TagModel.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "tag", Namespace = "")] +public class TagModel { - [DataContract(Name = "tag", Namespace = "")] - public class TagModel - { - [DataMember(Name = "id", IsRequired = true)] - public int Id { get; set; } + [DataMember(Name = "id", IsRequired = true)] + public int Id { get; set; } - [DataMember(Name = "text", IsRequired = true)] - public string? Text { get; set; } + [DataMember(Name = "text", IsRequired = true)] + public string? Text { get; set; } - [DataMember(Name = "group")] - public string? Group { get; set; } + [DataMember(Name = "group")] + public string? Group { get; set; } - [DataMember(Name = "nodeCount")] - public int NodeCount { get; set; } - } + [DataMember(Name = "nodeCount")] + public int NodeCount { get; set; } } diff --git a/src/Umbraco.Core/Models/TaggableObjectTypes.cs b/src/Umbraco.Core/Models/TaggableObjectTypes.cs index 8a9384ec74..03be2273a2 100644 --- a/src/Umbraco.Core/Models/TaggableObjectTypes.cs +++ b/src/Umbraco.Core/Models/TaggableObjectTypes.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Enum representing the taggable object types +/// +public enum TaggableObjectTypes { - /// - /// Enum representing the taggable object types - /// - public enum TaggableObjectTypes - { - All, - Content, - Media, - Member - } + All, + Content, + Media, + Member, } diff --git a/src/Umbraco.Core/Models/TaggedEntity.cs b/src/Umbraco.Core/Models/TaggedEntity.cs index 9bc05eae15..821f592343 100644 --- a/src/Umbraco.Core/Models/TaggedEntity.cs +++ b/src/Umbraco.Core/Models/TaggedEntity.cs @@ -1,31 +1,30 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a tagged entity. +/// +/// +/// Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged, +/// which is why this class is composed of a list of tagged properties and the identifier the actual entity. +/// +public class TaggedEntity { /// - /// Represents a tagged entity. + /// Initializes a new instance of the class. /// - /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that are tagged, - /// which is why this class is composed of a list of tagged properties and the identifier the actual entity. - public class TaggedEntity + public TaggedEntity(int entityId, IEnumerable taggedProperties) { - /// - /// Initializes a new instance of the class. - /// - public TaggedEntity(int entityId, IEnumerable taggedProperties) - { - EntityId = entityId; - TaggedProperties = taggedProperties; - } - - /// - /// Gets the identifier of the entity. - /// - public int EntityId { get; } - - /// - /// Gets the tagged properties. - /// - public IEnumerable TaggedProperties { get; } + EntityId = entityId; + TaggedProperties = taggedProperties; } + + /// + /// Gets the identifier of the entity. + /// + public int EntityId { get; } + + /// + /// Gets the tagged properties. + /// + public IEnumerable TaggedProperties { get; } } diff --git a/src/Umbraco.Core/Models/TaggedProperty.cs b/src/Umbraco.Core/Models/TaggedProperty.cs index 24ef9ccc45..90257a1a3e 100644 --- a/src/Umbraco.Core/Models/TaggedProperty.cs +++ b/src/Umbraco.Core/Models/TaggedProperty.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a tagged property on an entity. +/// +public class TaggedProperty { /// - /// Represents a tagged property on an entity. + /// Initializes a new instance of the class. /// - public class TaggedProperty + public TaggedProperty(int propertyTypeId, string? propertyTypeAlias, IEnumerable tags) { - /// - /// Initializes a new instance of the class. - /// - public TaggedProperty(int propertyTypeId, string? propertyTypeAlias, IEnumerable tags) - { - PropertyTypeId = propertyTypeId; - PropertyTypeAlias = propertyTypeAlias; - Tags = tags; - } - - /// - /// Gets the identifier of the property type. - /// - public int PropertyTypeId { get; } - - /// - /// Gets the alias of the property type. - /// - public string? PropertyTypeAlias { get; } - - /// - /// Gets the tags. - /// - public IEnumerable Tags { get; } + PropertyTypeId = propertyTypeId; + PropertyTypeAlias = propertyTypeAlias; + Tags = tags; } + + /// + /// Gets the identifier of the property type. + /// + public int PropertyTypeId { get; } + + /// + /// Gets the alias of the property type. + /// + public string? PropertyTypeAlias { get; } + + /// + /// Gets the tags. + /// + public IEnumerable Tags { get; } } diff --git a/src/Umbraco.Core/Models/TagsStorageType.cs b/src/Umbraco.Core/Models/TagsStorageType.cs index 7bd8ea7937..ccff41bb72 100644 --- a/src/Umbraco.Core/Models/TagsStorageType.cs +++ b/src/Umbraco.Core/Models/TagsStorageType.cs @@ -1,20 +1,21 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Defines how tags are stored. +/// +/// +/// Tags are always stored as a string, but the string can +/// either be a delimited string, or a serialized Json array. +/// +public enum TagsStorageType { /// - /// Defines how tags are stored. + /// Store tags as a delimited string. /// - /// Tags are always stored as a string, but the string can - /// either be a delimited string, or a serialized Json array. - public enum TagsStorageType - { - /// - /// Store tags as a delimited string. - /// - Csv, + Csv, - /// - /// Store tags as serialized Json. - /// - Json - } + /// + /// Store tags as serialized Json. + /// + Json, } diff --git a/src/Umbraco.Core/Models/TelemetryLevel.cs b/src/Umbraco.Core/Models/TelemetryLevel.cs index 26a714b385..cdf1d24e90 100644 --- a/src/Umbraco.Core/Models/TelemetryLevel.cs +++ b/src/Umbraco.Core/Models/TelemetryLevel.cs @@ -1,12 +1,11 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public enum TelemetryLevel { - [DataContract] - public enum TelemetryLevel - { - Minimal, - Basic, - Detailed, - } + Minimal, + Basic, + Detailed, } diff --git a/src/Umbraco.Core/Models/TelemetryResource.cs b/src/Umbraco.Core/Models/TelemetryResource.cs index 401e07848f..1c62842381 100644 --- a/src/Umbraco.Core/Models/TelemetryResource.cs +++ b/src/Umbraco.Core/Models/TelemetryResource.cs @@ -1,11 +1,10 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public class TelemetryResource { - [DataContract] - public class TelemetryResource - { - [DataMember] - public TelemetryLevel TelemetryLevel { get; set; } - } + [DataMember] + public TelemetryLevel TelemetryLevel { get; set; } } diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index 7efccf1e7d..1900233aa9 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -1,86 +1,85 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Template file. +/// +[Serializable] +[DataContract(IsReference = true)] +public class Template : File, ITemplate { - /// - /// Represents a Template file. - /// - [Serializable] - [DataContract(IsReference = true)] - public class Template : File, ITemplate + private readonly IShortStringHelper _shortStringHelper; + private string _alias; + private string? _masterTemplateAlias; + private Lazy? _masterTemplateId; + private string? _name; + + public Template(IShortStringHelper shortStringHelper, string? name, string? alias) + : this(shortStringHelper, name, alias, null) { - private string _alias; - private readonly IShortStringHelper _shortStringHelper; - private string? _name; - private string? _masterTemplateAlias; - private Lazy? _masterTemplateId; + } - public Template(IShortStringHelper shortStringHelper, string? name, string? alias) - : this(shortStringHelper, name, alias, null) - { } + public Template(IShortStringHelper shortStringHelper, string? name, string? alias, Func? getFileContent) + : base(string.Empty, getFileContent) + { + _shortStringHelper = shortStringHelper; + _name = name; + _alias = alias?.ToCleanString(shortStringHelper, CleanStringType.UnderscoreAlias) ?? string.Empty; + _masterTemplateId = new Lazy(() => -1); + } - public Template(IShortStringHelper shortStringHelper, string? name, string? alias, Func? getFileContent) - : base(string.Empty, getFileContent) + [DataMember] + public Lazy? MasterTemplateId + { + get => _masterTemplateId; + set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateId, nameof(MasterTemplateId)); + } + + public string? MasterTemplateAlias + { + get => _masterTemplateAlias; + set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateAlias, nameof(MasterTemplateAlias)); + } + + [DataMember] + public new string? Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + [DataMember] + public new string Alias + { + get => _alias; + set => SetPropertyValueAndDetectChanges( + value.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias), ref _alias!, nameof(Alias)); + } + + /// + /// Returns true if the template is used as a layout for other templates (i.e. it has 'children') + /// + public bool IsMasterTemplate { get; set; } + + public void SetMasterTemplate(ITemplate? masterTemplate) + { + if (masterTemplate == null) { - _shortStringHelper = shortStringHelper; - _name = name; - _alias = alias?.ToCleanString(shortStringHelper, CleanStringType.UnderscoreAlias) ?? string.Empty; - _masterTemplateId = new Lazy(() => -1); + MasterTemplateId = new Lazy(() => -1); + MasterTemplateAlias = null; } - - [DataMember] - public Lazy? MasterTemplateId + else { - get => _masterTemplateId; - set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateId, nameof(MasterTemplateId)); - } - - public string? MasterTemplateAlias - { - get => _masterTemplateAlias; - set => SetPropertyValueAndDetectChanges(value, ref _masterTemplateAlias, nameof(MasterTemplateAlias)); - } - - [DataMember] - public new string? Name - { - get => _name; - set => SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - [DataMember] - public new string Alias - { - get => _alias; - set => SetPropertyValueAndDetectChanges(value.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias), ref _alias!, nameof(Alias)); - } - - /// - /// Returns true if the template is used as a layout for other templates (i.e. it has 'children') - /// - public bool IsMasterTemplate { get; set; } - - public void SetMasterTemplate(ITemplate? masterTemplate) - { - if (masterTemplate == null) - { - MasterTemplateId = new Lazy(() => -1); - MasterTemplateAlias = null; - } - else - { - MasterTemplateId = new Lazy(() => masterTemplate.Id); - MasterTemplateAlias = masterTemplate.Alias; - } - - } - - protected override void DeepCloneNameAndAlias(File clone) - { - // do nothing - prevents File from doing its stuff + MasterTemplateId = new Lazy(() => masterTemplate.Id); + MasterTemplateAlias = masterTemplate.Alias; } } + + protected override void DeepCloneNameAndAlias(File clone) + { + // do nothing - prevents File from doing its stuff + } } diff --git a/src/Umbraco.Core/Models/TemplateNode.cs b/src/Umbraco.Core/Models/TemplateNode.cs index 339f4efee3..f02988e6d2 100644 --- a/src/Umbraco.Core/Models/TemplateNode.cs +++ b/src/Umbraco.Core/Models/TemplateNode.cs @@ -1,34 +1,31 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Models +/// +/// Represents a template in a template tree +/// +public class TemplateNode { - /// - /// Represents a template in a template tree - /// - public class TemplateNode + public TemplateNode(ITemplate template) { - public TemplateNode(ITemplate template) - { - Template = template; - Children = new List(); - } - - /// - /// The current template - /// - public ITemplate Template { get; set; } - - /// - /// The children of the current template - /// - public IEnumerable Children { get; set; } - - /// - /// The parent template to the current template - /// - /// - /// Will be null if there is no parent - /// - public TemplateNode? Parent { get; set; } + Template = template; + Children = new List(); } + + /// + /// The current template + /// + public ITemplate Template { get; set; } + + /// + /// The children of the current template + /// + public IEnumerable Children { get; set; } + + /// + /// The parent template to the current template + /// + /// + /// Will be null if there is no parent + /// + public TemplateNode? Parent { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateOnDisk.cs b/src/Umbraco.Core/Models/TemplateOnDisk.cs index 61c10ba456..04fffb7c10 100644 --- a/src/Umbraco.Core/Models/TemplateOnDisk.cs +++ b/src/Umbraco.Core/Models/TemplateOnDisk.cs @@ -1,52 +1,50 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a Template file that can have its content on disk. +/// +[Serializable] +[DataContract(IsReference = true)] +public class TemplateOnDisk : Template { /// - /// Represents a Template file that can have its content on disk. + /// Initializes a new instance of the class. /// - [Serializable] - [DataContract(IsReference = true)] - public class TemplateOnDisk : Template + /// The name of the template. + /// The alias of the template. + /// The short string helper + public TemplateOnDisk(IShortStringHelper shortStringHelper, string name, string alias) + : base(shortStringHelper, name, alias) => + IsOnDisk = true; + + /// + /// Gets or sets a value indicating whether the content is on disk already. + /// + public bool IsOnDisk { get; set; } + + /// + /// Gets or sets the content. + /// + /// + /// + /// Getting the content while the template is "on disk" throws, + /// the template must be saved before its content can be retrieved. + /// + /// + /// Setting the content means it is not "on disk" anymore, and the + /// template becomes (and behaves like) a normal template. + /// + /// + public override string? Content { - /// - /// Initializes a new instance of the class. - /// - /// The name of the template. - /// The alias of the template. - public TemplateOnDisk(IShortStringHelper shortStringHelper, string name, string alias) - : base(shortStringHelper, name, alias) + get => IsOnDisk ? string.Empty : base.Content; + set { - IsOnDisk = true; - } - - /// - /// Gets or sets a value indicating whether the content is on disk already. - /// - public bool IsOnDisk { get; set; } - - /// - /// Gets or sets the content. - /// - /// - /// Getting the content while the template is "on disk" throws, - /// the template must be saved before its content can be retrieved. - /// Setting the content means it is not "on disk" anymore, and the - /// template becomes (and behaves like) a normal template. - /// - public override string? Content - { - get - { - return IsOnDisk ? string.Empty : base.Content; - } - set - { - base.Content = value; - IsOnDisk = false; - } + base.Content = value; + IsOnDisk = false; } } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/ContentTypeModel.cs b/src/Umbraco.Core/Models/TemplateQuery/ContentTypeModel.cs index f4f3e7bc59..c94cd67b8a 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/ContentTypeModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/ContentTypeModel.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery -{ - public class ContentTypeModel - { - public string? Alias { get; set; } +namespace Umbraco.Cms.Core.Models.TemplateQuery; - public string? Name { get; set; } - } +public class ContentTypeModel +{ + public string? Alias { get; set; } + + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/Operator.cs b/src/Umbraco.Core/Models/TemplateQuery/Operator.cs index eb3fe4be29..c76202fb68 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/Operator.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/Operator.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public enum Operator { - public enum Operator - { - Equals = 1, - NotEquals = 2, - Contains = 3, - NotContains = 4, - LessThan = 5, - LessThanEqualTo = 6, - GreaterThan = 7, - GreaterThanEqualTo = 8 - } + Equals = 1, + NotEquals = 2, + Contains = 3, + NotContains = 4, + LessThan = 5, + LessThanEqualTo = 6, + GreaterThan = 7, + GreaterThanEqualTo = 8, } diff --git a/src/Umbraco.Core/Models/TemplateQuery/OperatorFactory.cs b/src/Umbraco.Core/Models/TemplateQuery/OperatorFactory.cs index a8e3b40fef..fc23ebdb3d 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/OperatorFactory.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/OperatorFactory.cs @@ -1,32 +1,34 @@ -using System; +namespace Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Cms.Core.Models.TemplateQuery +public static class OperatorFactory { - public static class OperatorFactory + public static Operator FromString(string stringOperator) { - public static Operator FromString(string stringOperator) + if (stringOperator == null) { - if (stringOperator == null) throw new ArgumentNullException(nameof(stringOperator)); + throw new ArgumentNullException(nameof(stringOperator)); + } - switch (stringOperator) - { - case "=": - case "==": - return Operator.Equals; - case "!=": - case "<>": - return Operator.NotEquals; - case "<": - return Operator.LessThan; - case "<=": - return Operator.LessThanEqualTo; - case ">": - return Operator.GreaterThan; - case ">=": - return Operator.GreaterThanEqualTo; - default: - throw new ArgumentException($"A operator cannot be created from the specified string '{stringOperator}'", nameof(stringOperator)); - } + switch (stringOperator) + { + case "=": + case "==": + return Operator.Equals; + case "!=": + case "<>": + return Operator.NotEquals; + case "<": + return Operator.LessThan; + case "<=": + return Operator.LessThanEqualTo; + case ">": + return Operator.GreaterThan; + case ">=": + return Operator.GreaterThanEqualTo; + default: + throw new ArgumentException( + $"A operator cannot be created from the specified string '{stringOperator}'", + nameof(stringOperator)); } } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/OperatorTerm.cs b/src/Umbraco.Core/Models/TemplateQuery/OperatorTerm.cs index ce66965c68..d2a8c8e0db 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/OperatorTerm.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/OperatorTerm.cs @@ -1,25 +1,24 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Cms.Core.Models.TemplateQuery +public class OperatorTerm { - public class OperatorTerm + public OperatorTerm() { - public OperatorTerm() - { - Name = "is"; - Operator = Operator.Equals; - AppliesTo = new [] { "string" }; - } - - public OperatorTerm(string name, Operator @operator, IEnumerable appliesTo) - { - Name = name; - Operator = @operator; - AppliesTo = appliesTo; - } - - public string Name { get; set; } - public Operator Operator { get; set; } - public IEnumerable AppliesTo { get; set; } + Name = "is"; + Operator = Operator.Equals; + AppliesTo = new[] { "string" }; } + + public OperatorTerm(string name, Operator @operator, IEnumerable appliesTo) + { + Name = name; + Operator = @operator; + AppliesTo = appliesTo; + } + + public string Name { get; set; } + + public Operator Operator { get; set; } + + public IEnumerable AppliesTo { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/PropertyModel.cs b/src/Umbraco.Core/Models/TemplateQuery/PropertyModel.cs index 3ea4059b7e..39ea100e7d 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/PropertyModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/PropertyModel.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class PropertyModel { - public class PropertyModel - { - public string? Name { get; set; } + public string? Name { get; set; } - public string Alias { get; set; } = string.Empty; + public string Alias { get; set; } = string.Empty; - public string? Type { get; set; } - } + public string? Type { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryCondition.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryCondition.cs index b6305f16a8..2c64f13876 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/QueryCondition.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/QueryCondition.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class QueryCondition { - public class QueryCondition - { - public PropertyModel Property { get; set; } = new PropertyModel(); - public OperatorTerm Term { get; set; } = new OperatorTerm(); - public string ConstraintValue { get; set; } = string.Empty; - } + public PropertyModel Property { get; set; } = new(); + + public OperatorTerm Term { get; set; } = new(); + + public string ConstraintValue { get; set; } = string.Empty; } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryConditionExtensions.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryConditionExtensions.cs index 962cf92558..0722422aae 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/QueryConditionExtensions.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/QueryConditionExtensions.cs @@ -1,75 +1,74 @@ -using System; using System.Linq.Expressions; using System.Reflection; using Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class QueryConditionExtensions { - public static class QueryConditionExtensions + private static Lazy StringContainsMethodInfo => + new(() => typeof(string).GetMethod("Contains", new[] { typeof(string) })!); + + public static Expression> BuildCondition(this QueryCondition condition, string parameterAlias) { - private static Lazy StringContainsMethodInfo => - new Lazy(() => typeof(string).GetMethod("Contains", new[] {typeof(string)})!); - - public static Expression> BuildCondition(this QueryCondition condition, string parameterAlias) + object constraintValue; + switch (condition.Property.Type?.ToLowerInvariant()) { - object constraintValue; - switch (condition.Property.Type?.ToLowerInvariant()) - { - case "string": - constraintValue = condition.ConstraintValue; - break; - case "datetime": - constraintValue = DateTime.Parse(condition.ConstraintValue); - break; - case "boolean": - constraintValue = Boolean.Parse(condition.ConstraintValue); - break; - default: - constraintValue = Convert.ChangeType(condition.ConstraintValue, typeof(int)); - break; - } - - var parameterExpression = Expression.Parameter(typeof(T), parameterAlias); - var propertyExpression = Expression.Property(parameterExpression, condition.Property.Alias); - - var valueExpression = Expression.Constant(constraintValue); - Expression bodyExpression; - switch (condition.Term.Operator) - { - case Operator.NotEquals: - bodyExpression = Expression.NotEqual(propertyExpression, valueExpression); - break; - case Operator.GreaterThan: - bodyExpression = Expression.GreaterThan(propertyExpression, valueExpression); - break; - case Operator.GreaterThanEqualTo: - bodyExpression = Expression.GreaterThanOrEqual(propertyExpression, valueExpression); - break; - case Operator.LessThan: - bodyExpression = Expression.LessThan(propertyExpression, valueExpression); - break; - case Operator.LessThanEqualTo: - bodyExpression = Expression.LessThanOrEqual(propertyExpression, valueExpression); - break; - case Operator.Contains: - bodyExpression = Expression.Call(propertyExpression, StringContainsMethodInfo.Value, - valueExpression); - break; - case Operator.NotContains: - var tempExpression = Expression.Call(propertyExpression, StringContainsMethodInfo.Value, - valueExpression); - bodyExpression = Expression.Equal(tempExpression, Expression.Constant(false)); - break; - default: - case Operator.Equals: - bodyExpression = Expression.Equal(propertyExpression, valueExpression); - break; - } - - var predicate = - Expression.Lambda>(bodyExpression.Reduce(), parameterExpression); - - return predicate; + case "string": + constraintValue = condition.ConstraintValue; + break; + case "datetime": + constraintValue = DateTime.Parse(condition.ConstraintValue); + break; + case "boolean": + constraintValue = bool.Parse(condition.ConstraintValue); + break; + default: + constraintValue = Convert.ChangeType(condition.ConstraintValue, typeof(int)); + break; } + + ParameterExpression parameterExpression = Expression.Parameter(typeof(T), parameterAlias); + MemberExpression propertyExpression = Expression.Property(parameterExpression, condition.Property.Alias); + + ConstantExpression valueExpression = Expression.Constant(constraintValue); + Expression bodyExpression; + switch (condition.Term.Operator) + { + case Operator.NotEquals: + bodyExpression = Expression.NotEqual(propertyExpression, valueExpression); + break; + case Operator.GreaterThan: + bodyExpression = Expression.GreaterThan(propertyExpression, valueExpression); + break; + case Operator.GreaterThanEqualTo: + bodyExpression = Expression.GreaterThanOrEqual(propertyExpression, valueExpression); + break; + case Operator.LessThan: + bodyExpression = Expression.LessThan(propertyExpression, valueExpression); + break; + case Operator.LessThanEqualTo: + bodyExpression = Expression.LessThanOrEqual(propertyExpression, valueExpression); + break; + case Operator.Contains: + bodyExpression = Expression.Call(propertyExpression, StringContainsMethodInfo.Value, valueExpression); + break; + case Operator.NotContains: + MethodCallExpression tempExpression = Expression.Call( + propertyExpression, + StringContainsMethodInfo.Value, + valueExpression); + bodyExpression = Expression.Equal(tempExpression, Expression.Constant(false)); + break; + default: + case Operator.Equals: + bodyExpression = Expression.Equal(propertyExpression, valueExpression); + break; + } + + var predicate = + Expression.Lambda>(bodyExpression.Reduce(), parameterExpression); + + return predicate; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs index 48d6506143..06f5c82d19 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/QueryModel.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Cms.Core.Models.TemplateQuery +public class QueryModel { - public class QueryModel - { - public ContentTypeModel? ContentType { get; set; } - public SourceModel? Source { get; set; } - public IEnumerable? Filters { get; set; } - public SortExpression? Sort { get; set; } - public int Take { get; set; } - } + public ContentTypeModel? ContentType { get; set; } + + public SourceModel? Source { get; set; } + + public IEnumerable? Filters { get; set; } + + public SortExpression? Sort { get; set; } + + public int Take { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/QueryResultModel.cs b/src/Umbraco.Core/Models/TemplateQuery/QueryResultModel.cs index 8605f92423..61845214a5 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/QueryResultModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/QueryResultModel.cs @@ -1,15 +1,14 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Models.TemplateQuery; -namespace Umbraco.Cms.Core.Models.TemplateQuery +public class QueryResultModel { + public string? QueryExpression { get; set; } - public class QueryResultModel - { + public IEnumerable? SampleResults { get; set; } - public string? QueryExpression { get; set; } - public IEnumerable? SampleResults { get; set; } - public int ResultCount { get; set; } - public long ExecutionTime { get; set; } - public int Take { get; set; } - } + public int ResultCount { get; set; } + + public long ExecutionTime { get; set; } + + public int Take { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/SortExpression.cs b/src/Umbraco.Core/Models/TemplateQuery/SortExpression.cs index c68b366ba5..b5accd7ccd 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/SortExpression.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/SortExpression.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery -{ - public class SortExpression - { - public PropertyModel? Property { get; set; } +namespace Umbraco.Cms.Core.Models.TemplateQuery; - public string? Direction { get; set; } - } +public class SortExpression +{ + public PropertyModel? Property { get; set; } + + public string? Direction { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/SourceModel.cs b/src/Umbraco.Core/Models/TemplateQuery/SourceModel.cs index 4b67f7e73c..a36ae38a9e 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/SourceModel.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/SourceModel.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery +namespace Umbraco.Cms.Core.Models.TemplateQuery; + +public class SourceModel { - public class SourceModel - { - public int Id { get; set; } - public string? Name { get; set; } - } + public int Id { get; set; } + + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/TemplateQuery/TemplateQueryResult.cs b/src/Umbraco.Core/Models/TemplateQuery/TemplateQueryResult.cs index 95615b4d0d..4e56beb635 100644 --- a/src/Umbraco.Core/Models/TemplateQuery/TemplateQueryResult.cs +++ b/src/Umbraco.Core/Models/TemplateQuery/TemplateQueryResult.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Models.TemplateQuery -{ - public class TemplateQueryResult - { - public string? Icon { get; set; } +namespace Umbraco.Cms.Core.Models.TemplateQuery; - public string? Name { get; set; } - } +public class TemplateQueryResult +{ + public string? Icon { get; set; } + + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Models/Trees/ActionMenuItem.cs b/src/Umbraco.Core/Models/Trees/ActionMenuItem.cs index c89fb402d0..87fe72a0fb 100644 --- a/src/Umbraco.Core/Models/Trees/ActionMenuItem.cs +++ b/src/Umbraco.Core/Models/Trees/ActionMenuItem.cs @@ -1,54 +1,52 @@ -using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Trees +namespace Umbraco.Cms.Core.Models.Trees; + +/// +/// +/// A menu item that represents some JS that needs to execute when the menu item is clicked. +/// +/// +/// These types of menu items are rare but they do exist. Things like refresh node simply execute +/// JS and don't launch a dialog. +/// Each action menu item describes what angular service that it's method exists in and what the method name is. +/// An action menu item must describe the angular service name for which it's method exists. It may also define what +/// the +/// method name is that will be called in this service but if one is not specified then we will assume the method name +/// is the +/// same as the Type name of the current action menu class. +/// +public abstract class ActionMenuItem : MenuItem { - /// - /// - /// A menu item that represents some JS that needs to execute when the menu item is clicked. - /// - /// - /// These types of menu items are rare but they do exist. Things like refresh node simply execute - /// JS and don't launch a dialog. - /// Each action menu item describes what angular service that it's method exists in and what the method name is. - /// An action menu item must describe the angular service name for which it's method exists. It may also define what the - /// method name is that will be called in this service but if one is not specified then we will assume the method name is the - /// same as the Type name of the current action menu class. - /// - public abstract class ActionMenuItem : MenuItem + protected ActionMenuItem(string alias, string name) + : base(alias, name) => Initialize(); + + protected ActionMenuItem(string alias, ILocalizedTextService textService) + : base(alias, textService) => + Initialize(); + + /// + /// The angular service name containing the + /// + public abstract string AngularServiceName { get; } + + /// + /// The angular service method name to call for this menu item + /// + public virtual string? AngularServiceMethodName { get; } = null; + + private void Initialize() { - /// - /// The angular service name containing the - /// - public abstract string AngularServiceName { get; } - - /// - /// The angular service method name to call for this menu item - /// - public virtual string? AngularServiceMethodName { get; } = null; - - protected ActionMenuItem(string alias, string name) : base(alias, name) + // add the current type to the metadata + if (AngularServiceMethodName.IsNullOrWhiteSpace()) { - Initialize(); + // if no method name is supplied we will assume that the menu action is the type name of the current menu class + ExecuteJsMethod($"{AngularServiceName}.{GetType().Name}"); } - - protected ActionMenuItem(string alias, ILocalizedTextService textService) : base(alias, textService) + else { - Initialize(); - } - - private void Initialize() - { - //add the current type to the metadata - if (AngularServiceMethodName.IsNullOrWhiteSpace()) - { - //if no method name is supplied we will assume that the menu action is the type name of the current menu class - ExecuteJsMethod($"{AngularServiceName}.{this.GetType().Name}"); - } - else - { - ExecuteJsMethod($"{AngularServiceName}.{AngularServiceMethodName}"); - } + ExecuteJsMethod($"{AngularServiceName}.{AngularServiceMethodName}"); } } } diff --git a/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs b/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs index a8d945242e..41c8c6f0de 100644 --- a/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs +++ b/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs @@ -1,27 +1,27 @@ -using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Trees +namespace Umbraco.Cms.Core.Models.Trees; + +/// +/// Represents the refresh node menu item +/// +public sealed class CreateChildEntity : ActionMenuItem { - /// - /// Represents the refresh node menu item - /// - public sealed class CreateChildEntity : ActionMenuItem + public CreateChildEntity(string name, bool separatorBefore = false) + : base(ActionNew.ActionAlias, name) { - public override string AngularServiceName => "umbracoMenuActions"; - - public CreateChildEntity(string name, bool separatorBefore = false) - : base(ActionNew.ActionAlias, name) - { - Icon = "add"; Name = name; - SeparatorBefore = separatorBefore; - } - - public CreateChildEntity(ILocalizedTextService textService, bool separatorBefore = false) - : base(ActionNew.ActionAlias, textService) - { - Icon = "add"; - SeparatorBefore = separatorBefore; - } + Icon = "add"; + Name = name; + SeparatorBefore = separatorBefore; } + + public CreateChildEntity(ILocalizedTextService textService, bool separatorBefore = false) + : base(ActionNew.ActionAlias, textService) + { + Icon = "add"; + SeparatorBefore = separatorBefore; + } + + public override string AngularServiceName => "umbracoMenuActions"; } diff --git a/src/Umbraco.Core/Models/Trees/ExportMember.cs b/src/Umbraco.Core/Models/Trees/ExportMember.cs index 30f904f952..3f11ef4b05 100644 --- a/src/Umbraco.Core/Models/Trees/ExportMember.cs +++ b/src/Umbraco.Core/Models/Trees/ExportMember.cs @@ -1,17 +1,14 @@ -using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Trees +namespace Umbraco.Cms.Core.Models.Trees; + +/// +/// Represents the export member menu item +/// +public sealed class ExportMember : ActionMenuItem { - /// - /// Represents the export member menu item - /// - public sealed class ExportMember : ActionMenuItem - { - public override string AngularServiceName => "umbracoMenuActions"; + public ExportMember(ILocalizedTextService textService) + : base("export", textService) => Icon = "download-alt"; - public ExportMember(ILocalizedTextService textService) : base("export", textService) - { - Icon = "download-alt"; - } - } + public override string AngularServiceName => "umbracoMenuActions"; } diff --git a/src/Umbraco.Core/Models/Trees/MenuItem.cs b/src/Umbraco.Core/Models/Trees/MenuItem.cs index e56a2440a8..3f77ccf2b6 100644 --- a/src/Umbraco.Core/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Core/Models/Trees/MenuItem.cs @@ -1,213 +1,198 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -using System.Threading; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Trees; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Trees +namespace Umbraco.Cms.Core.Models.Trees; + +/// +/// A context menu item +/// +[DataContract(Name = "menuItem", Namespace = "")] +public class MenuItem { - /// - /// A context menu item - /// - [DataContract(Name = "menuItem", Namespace = "")] - public class MenuItem + #region Constructors + + public MenuItem() { - #region Constructors - public MenuItem() - { - AdditionalData = new Dictionary(); - Icon = "folder"; - } - - public MenuItem(string alias, string name) - : this() - { - Alias = alias; - Name = name; - } - - public MenuItem(string alias, ILocalizedTextService textService) - : this() - { - Alias = alias; - Name = textService.Localize("actions", Alias); - TextDescription = textService.Localize("visuallyHiddenTexts", alias + "_description", Thread.CurrentThread.CurrentUICulture); - } - - /// - /// Create a menu item based on an definition - /// - /// - /// - public MenuItem(IAction action, string name = "") - : this() - { - Name = name.IsNullOrWhiteSpace() ? action.Alias : name; - Alias = action.Alias; - SeparatorBefore = false; - Icon = action.Icon; - Action = action; - } - #endregion - - #region Properties - [IgnoreDataMember] - public IAction? Action { get; set; } - - /// - /// A dictionary to support any additional meta data that should be rendered for the node which is - /// useful for custom action commands such as 'create', 'copy', etc... - /// - /// - /// We will also use the meta data collection for dealing with legacy menu items (i.e. for loading custom URLs or - /// executing custom JS). - /// - [DataMember(Name = "metaData")] - public Dictionary AdditionalData { get; private set; } - - [DataMember(Name = "name", IsRequired = true)] - [Required] - public string? Name { get; set; } - - [DataMember(Name = "alias", IsRequired = true)] - [Required] - public string? Alias { get; set; } - - [DataMember(Name = "textDescription")] - public string? TextDescription { get; set; } - - /// - /// Ensures a menu separator will exist before this menu item - /// - [DataMember(Name = "separator")] - public bool SeparatorBefore { get; set; } - - [DataMember(Name = "cssclass")] - public string Icon { get; set; } - - /// - /// Used in the UI to inform the user that the menu item will open a dialog/confirmation - /// - [DataMember(Name = "opensDialog")] - public bool OpensDialog { get; set; } - - #endregion - - #region Constants - - /// - /// Used as a key for the AdditionalData to specify a specific dialog title instead of the menu title - /// - internal const string DialogTitleKey = "dialogTitle"; - - /// - /// Used to specify the URL that the dialog will launch to in an iframe - /// - internal const string ActionUrlKey = "actionUrl"; - - // TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with - // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog - // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, - // though would be v-easy, just not sure we want to ever support that? - internal const string ActionUrlMethodKey = "actionUrlMethod"; - - /// - /// Used to specify the angular view that the dialog will launch - /// - internal const string ActionViewKey = "actionView"; - - /// - /// Used to specify the js method to execute for the menu item - /// - internal const string JsActionKey = "jsAction"; - - /// - /// Used to specify an angular route to go to for the menu item - /// - internal const string ActionRouteKey = "actionRoute"; - - #endregion - - #region Methods - - /// - /// Sets the menu item to navigate to the specified angular route path - /// - /// - public void NavigateToRoute(string route) - { - AdditionalData[ActionRouteKey] = route; - } - - /// - /// Adds the required meta data to the menu item so that angular knows to attempt to call the Js method. - /// - /// - public void ExecuteJsMethod(string jsToExecute) - { - SetJsAction(jsToExecute); - } - - /// - /// Sets the menu item to display a dialog based on an angular view path - /// - /// - /// - public void LaunchDialogView(string view, string dialogTitle) - { - SetDialogTitle(dialogTitle); - SetActionView(view); - } - - /// - /// Sets the menu item to display a dialog based on a URL path in an iframe - /// - /// - /// - public void LaunchDialogUrl(string url, string dialogTitle) - { - SetDialogTitle(dialogTitle); - SetActionUrl(url); - } - - private void SetJsAction(string jsToExecute) - { - AdditionalData[JsActionKey] = jsToExecute; - } - - /// - /// Puts a dialog title into the meta data to be displayed on the dialog of the menu item (if there is one) - /// instead of the menu name - /// - /// - private void SetDialogTitle(string dialogTitle) - { - AdditionalData[DialogTitleKey] = dialogTitle; - } - - /// - /// Configures the menu item to launch a specific view - /// - /// - private void SetActionView(string view) - { - AdditionalData[ActionViewKey] = view; - } - - /// - /// Configures the menu item to launch a URL with the specified action (dialog or new window) - /// - /// - /// - private void SetActionUrl(string url, ActionUrlMethod method = ActionUrlMethod.Dialog) - { - AdditionalData[ActionUrlKey] = url; - AdditionalData[ActionUrlMethodKey] = method; - } - - #endregion + AdditionalData = new Dictionary(); + Icon = "folder"; } + + public MenuItem(string alias, string name) + : this() + { + Alias = alias; + Name = name; + } + + public MenuItem(string alias, ILocalizedTextService textService) + : this() + { + Alias = alias; + Name = textService.Localize("actions", Alias); + TextDescription = textService.Localize("visuallyHiddenTexts", alias + "_description", Thread.CurrentThread.CurrentUICulture); + } + + /// + /// Create a menu item based on an definition + /// + /// + /// + public MenuItem(IAction action, string name = "") + : this() + { + Name = name.IsNullOrWhiteSpace() ? action.Alias : name; + Alias = action.Alias; + SeparatorBefore = false; + Icon = action.Icon; + Action = action; + } + + #endregion + + #region Properties + + [IgnoreDataMember] + public IAction? Action { get; set; } + + /// + /// A dictionary to support any additional meta data that should be rendered for the node which is + /// useful for custom action commands such as 'create', 'copy', etc... + /// + /// + /// We will also use the meta data collection for dealing with legacy menu items (i.e. for loading custom URLs or + /// executing custom JS). + /// + [DataMember(Name = "metaData")] + public Dictionary AdditionalData { get; private set; } + + [DataMember(Name = "name", IsRequired = true)] + [Required] + public string? Name { get; set; } + + [DataMember(Name = "alias", IsRequired = true)] + [Required] + public string? Alias { get; set; } + + [DataMember(Name = "textDescription")] + public string? TextDescription { get; set; } + + /// + /// Ensures a menu separator will exist before this menu item + /// + [DataMember(Name = "separator")] + public bool SeparatorBefore { get; set; } + + [DataMember(Name = "cssclass")] + public string Icon { get; set; } + + /// + /// Used in the UI to inform the user that the menu item will open a dialog/confirmation + /// + [DataMember(Name = "opensDialog")] + public bool OpensDialog { get; set; } + + #endregion + + #region Constants + + /// + /// Used as a key for the AdditionalData to specify a specific dialog title instead of the menu title + /// + internal const string DialogTitleKey = "dialogTitle"; + + /// + /// Used to specify the URL that the dialog will launch to in an iframe + /// + internal const string ActionUrlKey = "actionUrl"; + + // TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with + // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog + // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, + // though would be v-easy, just not sure we want to ever support that? + internal const string ActionUrlMethodKey = "actionUrlMethod"; + + /// + /// Used to specify the angular view that the dialog will launch + /// + internal const string ActionViewKey = "actionView"; + + /// + /// Used to specify the js method to execute for the menu item + /// + internal const string JsActionKey = "jsAction"; + + /// + /// Used to specify an angular route to go to for the menu item + /// + internal const string ActionRouteKey = "actionRoute"; + + #endregion + + #region Methods + + /// + /// Sets the menu item to navigate to the specified angular route path + /// + /// + public void NavigateToRoute(string route) => AdditionalData[ActionRouteKey] = route; + + /// + /// Adds the required meta data to the menu item so that angular knows to attempt to call the Js method. + /// + /// + public void ExecuteJsMethod(string jsToExecute) => SetJsAction(jsToExecute); + + /// + /// Sets the menu item to display a dialog based on an angular view path + /// + /// + /// + public void LaunchDialogView(string view, string dialogTitle) + { + SetDialogTitle(dialogTitle); + SetActionView(view); + } + + /// + /// Sets the menu item to display a dialog based on a URL path in an iframe + /// + /// + /// + public void LaunchDialogUrl(string url, string dialogTitle) + { + SetDialogTitle(dialogTitle); + SetActionUrl(url); + } + + private void SetJsAction(string jsToExecute) => AdditionalData[JsActionKey] = jsToExecute; + + /// + /// Puts a dialog title into the meta data to be displayed on the dialog of the menu item (if there is one) + /// instead of the menu name + /// + /// + private void SetDialogTitle(string dialogTitle) => AdditionalData[DialogTitleKey] = dialogTitle; + + /// + /// Configures the menu item to launch a specific view + /// + /// + private void SetActionView(string view) => AdditionalData[ActionViewKey] = view; + + /// + /// Configures the menu item to launch a URL with the specified action (dialog or new window) + /// + /// + /// + private void SetActionUrl(string url, ActionUrlMethod method = ActionUrlMethod.Dialog) + { + AdditionalData[ActionUrlKey] = url; + AdditionalData[ActionUrlMethodKey] = method; + } + + #endregion } diff --git a/src/Umbraco.Core/Models/Trees/RefreshNode.cs b/src/Umbraco.Core/Models/Trees/RefreshNode.cs index 01eb2fa34a..befbec019e 100644 --- a/src/Umbraco.Core/Models/Trees/RefreshNode.cs +++ b/src/Umbraco.Core/Models/Trees/RefreshNode.cs @@ -1,27 +1,26 @@ -using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Models.Trees +namespace Umbraco.Cms.Core.Models.Trees; + +/// +/// +/// Represents the refresh node menu item +/// +public sealed class RefreshNode : ActionMenuItem { - /// - /// - /// Represents the refresh node menu item - /// - public sealed class RefreshNode : ActionMenuItem + public RefreshNode(string name, bool separatorBefore = false) + : base("refreshNode", name) { - public override string AngularServiceName => "umbracoMenuActions"; - - public RefreshNode(string name, bool separatorBefore = false) - : base("refreshNode", name) - { - Icon = "refresh"; - SeparatorBefore = separatorBefore; - } - - public RefreshNode(ILocalizedTextService textService, bool separatorBefore = false) - : base("refreshNode", textService) - { - Icon = "refresh"; - SeparatorBefore = separatorBefore; - } + Icon = "refresh"; + SeparatorBefore = separatorBefore; } + + public RefreshNode(ILocalizedTextService textService, bool separatorBefore = false) + : base("refreshNode", textService) + { + Icon = "refresh"; + SeparatorBefore = separatorBefore; + } + + public override string AngularServiceName => "umbracoMenuActions"; } diff --git a/src/Umbraco.Core/Models/TwoFactorLogin.cs b/src/Umbraco.Core/Models/TwoFactorLogin.cs index c38105626c..551482e3a2 100644 --- a/src/Umbraco.Core/Models/TwoFactorLogin.cs +++ b/src/Umbraco.Core/Models/TwoFactorLogin.cs @@ -1,13 +1,14 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public class TwoFactorLogin : EntityBase, ITwoFactorLogin { - public class TwoFactorLogin : EntityBase, ITwoFactorLogin - { - public string ProviderName { get; set; } = null!; - public string Secret { get; set; } = null!; - public Guid UserOrMemberKey { get; set; } - public bool Confirmed { get; set; } - } + public bool Confirmed { get; set; } + + public string ProviderName { get; set; } = null!; + + public string Secret { get; set; } = null!; + + public Guid UserOrMemberKey { get; set; } } diff --git a/src/Umbraco.Core/Models/UmbracoDomain.cs b/src/Umbraco.Core/Models/UmbracoDomain.cs index 3f2eb00f51..c883e14770 100644 --- a/src/Umbraco.Core/Models/UmbracoDomain.cs +++ b/src/Umbraco.Core/Models/UmbracoDomain.cs @@ -1,54 +1,47 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[Serializable] +[DataContract(IsReference = true)] +public class UmbracoDomain : EntityBase, IDomain { - [Serializable] - [DataContract(IsReference = true)] - public class UmbracoDomain : EntityBase, IDomain + private int? _contentId; + private string _domainName; + private int? _languageId; + + public UmbracoDomain(string domainName) => _domainName = domainName; + + public UmbracoDomain(string domainName, string languageIsoCode) + : this(domainName) => + LanguageIsoCode = languageIsoCode; + + [DataMember] + public int? LanguageId { - public UmbracoDomain(string domainName) - { - _domainName = domainName; - } - - public UmbracoDomain(string domainName, string languageIsoCode) - : this(domainName) - { - LanguageIsoCode = languageIsoCode; - } - - private int? _contentId; - private int? _languageId; - private string _domainName; - - [DataMember] - public int? LanguageId - { - get => _languageId; - set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); - } - - [DataMember] - public string DomainName - { - get => _domainName; - set => SetPropertyValueAndDetectChanges(value, ref _domainName!, nameof(DomainName)); - } - - [DataMember] - public int? RootContentId - { - get => _contentId; - set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(RootContentId)); - } - - public bool IsWildcard => string.IsNullOrWhiteSpace(DomainName) || DomainName.StartsWith("*"); - - /// - /// Readonly value of the language ISO code for the domain - /// - public string? LanguageIsoCode { get; set; } + get => _languageId; + set => SetPropertyValueAndDetectChanges(value, ref _languageId, nameof(LanguageId)); } + + [DataMember] + public string DomainName + { + get => _domainName; + set => SetPropertyValueAndDetectChanges(value, ref _domainName!, nameof(DomainName)); + } + + [DataMember] + public int? RootContentId + { + get => _contentId; + set => SetPropertyValueAndDetectChanges(value, ref _contentId, nameof(RootContentId)); + } + + public bool IsWildcard => string.IsNullOrWhiteSpace(DomainName) || DomainName.StartsWith("*"); + + /// + /// Readonly value of the language ISO code for the domain + /// + public string? LanguageIsoCode { get; set; } } diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 00dbd490f8..600927db84 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -1,178 +1,175 @@ -using Umbraco.Cms.Core.CodeAnnotations; +using Umbraco.Cms.Core.CodeAnnotations; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Enum used to represent the Umbraco Object Types and their associated GUIDs +/// +public enum UmbracoObjectTypes { /// - /// Enum used to represent the Umbraco Object Types and their associated GUIDs + /// Default value /// - public enum UmbracoObjectTypes - { - /// - /// Default value - /// - Unknown, + Unknown, + /// + /// Root + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.SystemRoot)] + [FriendlyName("Root")] + ROOT, - /// - /// Root - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.SystemRoot)] - [FriendlyName("Root")] - ROOT, + /// + /// Document + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Document, typeof(IContent))] + [FriendlyName("Document")] + [UmbracoUdiType(Constants.UdiEntityType.Document)] + Document, - /// - /// Document - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Document, typeof(IContent))] - [FriendlyName("Document")] - [UmbracoUdiType(Constants.UdiEntityType.Document)] - Document, + /// + /// Media + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Media, typeof(IMedia))] + [FriendlyName("Media")] + [UmbracoUdiType(Constants.UdiEntityType.Media)] + Media, - /// - /// Media - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Media, typeof(IMedia))] - [FriendlyName("Media")] - [UmbracoUdiType(Constants.UdiEntityType.Media)] - Media, + /// + /// Member Type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberType, typeof(IMemberType))] + [FriendlyName("Member Type")] + [UmbracoUdiType(Constants.UdiEntityType.MemberType)] + MemberType, - /// - /// Member Type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberType, typeof(IMemberType))] - [FriendlyName("Member Type")] - [UmbracoUdiType(Constants.UdiEntityType.MemberType)] - MemberType, + /// + /// Template + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Template, typeof(ITemplate))] + [FriendlyName("Template")] + [UmbracoUdiType(Constants.UdiEntityType.Template)] + Template, - /// - /// Template - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Template, typeof(ITemplate))] - [FriendlyName("Template")] - [UmbracoUdiType(Constants.UdiEntityType.Template)] - Template, + /// + /// Member Group + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup)] + [FriendlyName("Member Group")] + [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] + MemberGroup, - /// - /// Member Group - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup)] - [FriendlyName("Member Group")] - [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] - MemberGroup, + /// + /// "Media Type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.MediaType, typeof(IMediaType))] + [FriendlyName("Media Type")] + [UmbracoUdiType(Constants.UdiEntityType.MediaType)] + MediaType, - /// - /// "Media Type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MediaType, typeof(IMediaType))] - [FriendlyName("Media Type")] - [UmbracoUdiType(Constants.UdiEntityType.MediaType)] - MediaType, + /// + /// Document Type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentType, typeof(IContentType))] + [FriendlyName("Document Type")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentType)] + DocumentType, - /// - /// Document Type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentType, typeof(IContentType))] - [FriendlyName("Document Type")] - [UmbracoUdiType(Constants.UdiEntityType.DocumentType)] - DocumentType, + /// + /// Recycle Bin + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.ContentRecycleBin)] + [FriendlyName("Recycle Bin")] + RecycleBin, - /// - /// Recycle Bin - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.ContentRecycleBin)] - [FriendlyName("Recycle Bin")] - RecycleBin, + /// + /// Member + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Member, typeof(IMember))] + [FriendlyName("Member")] + [UmbracoUdiType(Constants.UdiEntityType.Member)] + Member, - /// - /// Member - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Member, typeof(IMember))] - [FriendlyName("Member")] - [UmbracoUdiType(Constants.UdiEntityType.Member)] - Member, + /// + /// Data Type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DataType, typeof(IDataType))] + [FriendlyName("Data Type")] + [UmbracoUdiType(Constants.UdiEntityType.DataType)] + DataType, - /// - /// Data Type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DataType, typeof(IDataType))] - [FriendlyName("Data Type")] - [UmbracoUdiType(Constants.UdiEntityType.DataType)] - DataType, + /// + /// Document type container + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentTypeContainer)] + [FriendlyName("Document Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentTypeContainer)] + DocumentTypeContainer, - /// - /// Document type container - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentTypeContainer)] - [FriendlyName("Document Type Container")] - [UmbracoUdiType(Constants.UdiEntityType.DocumentTypeContainer)] - DocumentTypeContainer, + /// + /// Media type container + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.MediaTypeContainer)] + [FriendlyName("Media Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.MediaTypeContainer)] + MediaTypeContainer, - /// - /// Media type container - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MediaTypeContainer)] - [FriendlyName("Media Type Container")] - [UmbracoUdiType(Constants.UdiEntityType.MediaTypeContainer)] - MediaTypeContainer, + /// + /// Media type container + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DataTypeContainer)] + [FriendlyName("Data Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.DataTypeContainer)] + DataTypeContainer, - /// - /// Media type container - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DataTypeContainer)] - [FriendlyName("Data Type Container")] - [UmbracoUdiType(Constants.UdiEntityType.DataTypeContainer)] - DataTypeContainer, + /// + /// Relation type + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.RelationType)] + [FriendlyName("Relation Type")] + [UmbracoUdiType(Constants.UdiEntityType.RelationType)] + RelationType, - /// - /// Relation type - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.RelationType)] - [FriendlyName("Relation Type")] - [UmbracoUdiType(Constants.UdiEntityType.RelationType)] - RelationType, + /// + /// Forms Form + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsForm)] + [FriendlyName("Form")] + FormsForm, - /// - /// Forms Form - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsForm)] - [FriendlyName("Form")] - FormsForm, + /// + /// Forms PreValue + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsPreValue)] + [FriendlyName("PreValue")] + FormsPreValue, - /// - /// Forms PreValue - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsPreValue)] - [FriendlyName("PreValue")] - FormsPreValue, + /// + /// Forms DataSource + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsDataSource)] + [FriendlyName("DataSource")] + FormsDataSource, - /// - /// Forms DataSource - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.FormsDataSource)] - [FriendlyName("DataSource")] - FormsDataSource, + /// + /// Language + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.Language)] + [FriendlyName("Language")] + Language, - /// - /// Language - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.Language)] - [FriendlyName("Language")] - Language, + /// + /// Document Blueprint + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentBlueprint, typeof(IContent))] + [FriendlyName("DocumentBlueprint")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentBlueprint)] + DocumentBlueprint, - /// - /// Document Blueprint - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentBlueprint, typeof(IContent))] - [FriendlyName("DocumentBlueprint")] - [UmbracoUdiType(Constants.UdiEntityType.DocumentBlueprint)] - DocumentBlueprint, - - /// - /// Reserved Identifier - /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.IdReservation)] - [FriendlyName("Identifier Reservation")] - IdReservation - - } + /// + /// Reserved Identifier + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.IdReservation)] + [FriendlyName("Identifier Reservation")] + IdReservation, } diff --git a/src/Umbraco.Core/Models/UmbracoUserExtensions.cs b/src/Umbraco.Core/Models/UmbracoUserExtensions.cs index 71612f3531..d708704fac 100644 --- a/src/Umbraco.Core/Models/UmbracoUserExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoUserExtensions.cs @@ -1,79 +1,90 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoUserExtensions { - public static class UmbracoUserExtensions + public static IEnumerable GetPermissions(this IUser user, string path, IUserService userService) => + userService.GetPermissionsForPath(user, path).GetAllPermissions(); + + public static bool HasSectionAccess(this IUser user, string app) { - public static IEnumerable GetPermissions(this IUser user, string path, IUserService userService) + IEnumerable apps = user.AllowedSections; + return apps.Any(uApp => uApp.InvariantEquals(app)); + } + + /// + /// Determines whether this user is the 'super' user. + /// + public static bool IsSuper(this IUser user) + { + if (user == null) { - return userService.GetPermissionsForPath(user, path).GetAllPermissions(); + throw new ArgumentNullException(nameof(user)); } - public static bool HasSectionAccess(this IUser user, string app) + return user.Id == Constants.Security.SuperUserId; + } + + /// + /// Determines whether this user belongs to the administrators group. + /// + /// The 'super' user does not automatically belongs to the administrators group. + public static bool IsAdmin(this IUser user) + { + if (user == null) { - var apps = user.AllowedSections; - return apps.Any(uApp => uApp.InvariantEquals(app)); + throw new ArgumentNullException(nameof(user)); } - /// - /// Determines whether this user is the 'super' user. - /// - public static bool IsSuper(this IUser user) + return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.AdminGroupAlias); + } + + /// + /// Returns the culture info associated with this user, based on the language they're assigned to in the back office + /// + /// + /// + /// + /// + public static CultureInfo GetUserCulture(this IUser user, ILocalizedTextService textService, GlobalSettings globalSettings) + { + if (user == null) { - if (user == null) throw new ArgumentNullException(nameof(user)); - return user.Id == Constants.Security.SuperUserId; + throw new ArgumentNullException(nameof(user)); } - /// - /// Determines whether this user belongs to the administrators group. - /// - /// The 'super' user does not automatically belongs to the administrators group. - public static bool IsAdmin(this IUser user) + if (textService == null) { - if (user == null) throw new ArgumentNullException(nameof(user)); - return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.AdminGroupAlias); + throw new ArgumentNullException(nameof(textService)); } - /// - /// Returns the culture info associated with this user, based on the language they're assigned to in the back office - /// - /// - /// - /// - /// - public static CultureInfo GetUserCulture(this IUser user, ILocalizedTextService textService, GlobalSettings globalSettings) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (textService == null) throw new ArgumentNullException(nameof(textService)); - return GetUserCulture(user.Language, textService, globalSettings); - } + return GetUserCulture(user.Language, textService, globalSettings); + } - public static CultureInfo GetUserCulture(string? userLanguage, ILocalizedTextService textService, GlobalSettings globalSettings) + public static CultureInfo GetUserCulture(string? userLanguage, ILocalizedTextService textService, GlobalSettings globalSettings) + { + try { - try - { - var culture = CultureInfo.GetCultureInfo(userLanguage!.Replace("_", "-")); - // TODO: This is a hack because we store the user language as 2 chars instead of the full culture - // which is actually stored in the language files (which are also named with 2 chars!) so we need to attempt - // to convert to a supported full culture - var result = textService.ConvertToSupportedCultureWithRegionCode(culture); - return result; - } - catch (CultureNotFoundException) - { - //return the default one - return CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage); - } + var culture = CultureInfo.GetCultureInfo(userLanguage!.Replace("_", "-")); + + // TODO: This is a hack because we store the user language as 2 chars instead of the full culture + // which is actually stored in the language files (which are also named with 2 chars!) so we need to attempt + // to convert to a supported full culture + CultureInfo result = textService.ConvertToSupportedCultureWithRegionCode(culture); + return result; + } + catch (CultureNotFoundException) + { + // return the default one + return CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage); } } } diff --git a/src/Umbraco.Core/Models/UnLinkLoginModel.cs b/src/Umbraco.Core/Models/UnLinkLoginModel.cs index d8c9920c5e..c121230810 100644 --- a/src/Umbraco.Core/Models/UnLinkLoginModel.cs +++ b/src/Umbraco.Core/Models/UnLinkLoginModel.cs @@ -1,16 +1,15 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models -{ - public class UnLinkLoginModel - { - [Required] - [DataMember(Name = "loginProvider", IsRequired = true)] - public string? LoginProvider { get; set; } +namespace Umbraco.Cms.Core.Models; - [Required] - [DataMember(Name = "providerKey", IsRequired = true)] - public string? ProviderKey { get; set; } - } +public class UnLinkLoginModel +{ + [Required] + [DataMember(Name = "loginProvider", IsRequired = true)] + public string? LoginProvider { get; set; } + + [Required] + [DataMember(Name = "providerKey", IsRequired = true)] + public string? ProviderKey { get; set; } } diff --git a/src/Umbraco.Core/Models/UpgradeCheckResponse.cs b/src/Umbraco.Core/Models/UpgradeCheckResponse.cs index 3238720541..b639616524 100644 --- a/src/Umbraco.Core/Models/UpgradeCheckResponse.cs +++ b/src/Umbraco.Core/Models/UpgradeCheckResponse.cs @@ -2,26 +2,28 @@ using System.Net; using System.Runtime.Serialization; using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract(Name = "upgrade", Namespace = "")] +public class UpgradeCheckResponse { - [DataContract(Name = "upgrade", Namespace = "")] - public class UpgradeCheckResponse + public UpgradeCheckResponse() { - [DataMember(Name = "type")] - public string? Type { get; set; } - - [DataMember(Name = "comment")] - public string? Comment { get; set; } - - [DataMember(Name = "url")] - public string? Url { get; set; } - - public UpgradeCheckResponse() { } - public UpgradeCheckResponse(string upgradeType, string upgradeComment, string upgradeUrl, IUmbracoVersion umbracoVersion) - { - Type = upgradeType; - Comment = upgradeComment; - Url = upgradeUrl + "?version=" + WebUtility.UrlEncode(umbracoVersion.Version?.ToString(3)); - } } + + public UpgradeCheckResponse(string upgradeType, string upgradeComment, string upgradeUrl, IUmbracoVersion umbracoVersion) + { + Type = upgradeType; + Comment = upgradeComment; + Url = upgradeUrl + "?version=" + WebUtility.UrlEncode(umbracoVersion.Version?.ToString(3)); + } + + [DataMember(Name = "type")] + public string? Type { get; set; } + + [DataMember(Name = "comment")] + public string? Comment { get; set; } + + [DataMember(Name = "url")] + public string? Url { get; set; } } diff --git a/src/Umbraco.Core/Models/UsageInformation.cs b/src/Umbraco.Core/Models/UsageInformation.cs index e2bedd6f0f..3de3a1201a 100644 --- a/src/Umbraco.Core/Models/UsageInformation.cs +++ b/src/Umbraco.Core/Models/UsageInformation.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public class UsageInformation { - [DataContract] - public class UsageInformation + public UsageInformation(string name, object data) { - [DataMember(Name = "name")] - public string Name { get; } - - [DataMember(Name = "data")] - public object Data { get; } - - public UsageInformation(string name, object data) - { - Name = name; - Data = data; - } + Name = name; + Data = data; } + + [DataMember(Name = "name")] + public string Name { get; } + + [DataMember(Name = "data")] + public object Data { get; } } diff --git a/src/Umbraco.Core/Models/UserData.cs b/src/Umbraco.Core/Models/UserData.cs index 07b45b3c54..144871c3f7 100644 --- a/src/Umbraco.Core/Models/UserData.cs +++ b/src/Umbraco.Core/Models/UserData.cs @@ -1,19 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public class UserData { - [DataContract] - public class UserData + public UserData(string name, string data) { - [DataMember(Name = "name")] - public string Name { get; } - [DataMember(Name = "data")] - public string Data { get; } - - public UserData(string name, string data) - { - Name = name; - Data = data; - } + Name = name; + Data = data; } + + [DataMember(Name = "name")] + public string Name { get; } + + [DataMember(Name = "data")] + public string Data { get; } } diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 924b11bcc4..87f91978e0 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Security.Cryptography; using Umbraco.Cms.Core.Cache; @@ -12,285 +9,367 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +public static class UserExtensions { - public static class UserExtensions + /// + /// Tries to lookup the user's Gravatar to see if the endpoint can be reached, if so it returns the valid URL + /// + /// + /// + /// + /// + /// + /// A list of 5 different sized avatar URLs + /// + public static string[] GetUserAvatarUrls(this IUser user, IAppCache cache, MediaFileManager mediaFileManager, IImageUrlGenerator imageUrlGenerator) { - /// - /// Tries to lookup the user's Gravatar to see if the endpoint can be reached, if so it returns the valid URL - /// - /// - /// - /// - /// - /// A list of 5 different sized avatar URLs - /// - public static string[] GetUserAvatarUrls(this IUser user, IAppCache cache, MediaFileManager mediaFileManager, IImageUrlGenerator imageUrlGenerator) + // If FIPS is required, never check the Gravatar service as it only supports MD5 hashing. + // Unfortunately, if the FIPS setting is enabled on Windows, using MD5 will throw an exception + // and the website will not run. + // Also, check if the user has explicitly removed all avatars including a Gravatar, this will be possible and the value will be "none" + if (user.Avatar == "none" || CryptoConfig.AllowOnlyFipsAlgorithms) { - // If FIPS is required, never check the Gravatar service as it only supports MD5 hashing. - // Unfortunately, if the FIPS setting is enabled on Windows, using MD5 will throw an exception - // and the website will not run. - // Also, check if the user has explicitly removed all avatars including a Gravatar, this will be possible and the value will be "none" - if (user.Avatar == "none" || CryptoConfig.AllowOnlyFipsAlgorithms) - { - return new string[0]; - } + return new string[0]; + } - if (user.Avatar.IsNullOrWhiteSpace()) - { - var gravatarHash = user.Email?.GenerateHash(); - var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; + if (user.Avatar.IsNullOrWhiteSpace()) + { + var gravatarHash = user.Email?.GenerateHash(); + var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; - //try Gravatar - var gravatarAccess = cache.GetCacheItem("UserAvatar" + user.Id, () => + // try Gravatar + var gravatarAccess = cache.GetCacheItem("UserAvatar" + user.Id, () => + { + // Test if we can reach this URL, will fail when there's network or firewall errors + var request = (HttpWebRequest)WebRequest.Create(gravatarUrl); + + // Require response within 10 seconds + request.Timeout = 10000; + try { - // Test if we can reach this URL, will fail when there's network or firewall errors - var request = (HttpWebRequest)WebRequest.Create(gravatarUrl); - // Require response within 10 seconds - request.Timeout = 10000; - try + using ((HttpWebResponse)request.GetResponse()) { - using ((HttpWebResponse)request.GetResponse()) { } } - catch (Exception) - { - // There was an HTTP or other error, return an null instead - return false; - } - return true; - }); - - if (gravatarAccess) + } + catch (Exception) { - return new[] - { - gravatarUrl + "&s=30", - gravatarUrl + "&s=60", - gravatarUrl + "&s=90", - gravatarUrl + "&s=150", - gravatarUrl + "&s=300" - }; + // There was an HTTP or other error, return an null instead + return false; } - return new string[0]; + return true; + }); + + if (gravatarAccess) + { + return new[] + { + gravatarUrl + "&s=30", gravatarUrl + "&s=60", gravatarUrl + "&s=90", gravatarUrl + "&s=150", + gravatarUrl + "&s=300", + }; } - //use the custom avatar - var avatarUrl = mediaFileManager.FileSystem.GetUrl(user.Avatar); - return new[] + return new string[0]; + } + + // use the custom avatar + var avatarUrl = mediaFileManager.FileSystem.GetUrl(user.Avatar); + return new[] + { + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 30, Height = 30 }), - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 60, Height = 60 }), - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 90, Height = 90 }), - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 150, Height = 150 }), - imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 300, Height = 300 }), - }.WhereNotNull().ToArray(); - - } - - - - internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } - - internal static bool HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } - - internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } - - internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) - { - return ContentPermissions.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } - - public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches) - { - if (content == null) throw new ArgumentNullException(nameof(content)); - return ContentPermissions.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } - - public static bool HasPathAccess(this IUser user, IMedia? media, IEntityService entityService, AppCaches appCaches) - { - if (media == null) throw new ArgumentNullException(nameof(media)); - return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } - - public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissions.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - } - - public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - } - - /// - /// Determines whether this user has access to view sensitive data - /// - /// - public static bool HasAccessToSensitiveData(this IUser user) - { - if (user == null) throw new ArgumentNullException("user"); - return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias); - } - - /// - /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin - /// - public static int[]? CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) - { - var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Id; - var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); - var result = runtimeCache.GetCacheItem(cacheKey, () => + ImageCropMode = ImageCropMode.Crop, Width = 30, Height = 30, + }), + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { - // This returns a nullable array even though we're checking if items have value and there cannot be null - // We use Cast to recast into non-nullable array - var gsn = user.Groups.Where(x => x.StartContentId is not null).Select(x => x.StartContentId).Distinct().Cast().ToArray(); - var usn = user.StartContentIds; - if (usn is not null) - { - var vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService); - return vals; - } + ImageCropMode = ImageCropMode.Crop, Width = 60, Height = 60, + }), + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) + { + ImageCropMode = ImageCropMode.Crop, Width = 90, Height = 90, + }), + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) + { + ImageCropMode = ImageCropMode.Crop, Width = 150, Height = 150, + }), + imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) + { + ImageCropMode = ImageCropMode.Crop, Width = 300, Height = 300, + }), + }.WhereNotNull().ToArray(); + } - return null; - }, TimeSpan.FromMinutes(2), true); - - return result; + public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); } - /// - /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin - /// - /// - /// - /// - /// - public static int[]? CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + return ContentPermissions.HasPathAccess( + content.Path, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + } + + internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RootString, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + + internal static bool HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RecycleBinContentString, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + + internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RootString, + user.CalculateMediaStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinMedia); + + internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RecycleBinMediaString, + user.CalculateMediaStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinMedia); + + public static bool HasPathAccess(this IUser user, IMedia? media, IEntityService entityService, AppCaches appCaches) + { + if (media == null) { - var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id; - var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); - var result = runtimeCache.GetCacheItem(cacheKey, () => - { - var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId!.Value).Distinct().ToArray(); - var usn = user.StartMediaIds; - if (usn is not null) - { - var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); - return vals; - } - - return null; - }, TimeSpan.FromMinutes(2), true); - - return result; + throw new ArgumentNullException(nameof(media)); } - public static string[]? GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + } + + public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) + { + if (entity == null) { - var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; - var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); - var result = runtimeCache.GetCacheItem(cacheKey, () => + throw new ArgumentNullException(nameof(entity)); + } + + return ContentPermissions.HasPathAccess( + entity.Path, + user.CalculateContentStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinContent); + } + + public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + } + + /// + /// Determines whether this user has access to view sensitive data + /// + /// + public static bool HasAccessToSensitiveData(this IUser user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias); + } + + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + public static int[]? CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Id; + IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => + { + // This returns a nullable array even though we're checking if items have value and there cannot be null + // We use Cast to recast into non-nullable array + var gsn = user.Groups.Where(x => x.StartContentId is not null).Select(x => x.StartContentId).Distinct() + .Cast().ToArray(); + var usn = user.StartContentIds; + if (usn is not null) { - var startNodeIds = user.CalculateMediaStartNodeIds(entityService, appCaches); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + var vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService); return vals; - }, TimeSpan.FromMinutes(2), true); + } - return result; - } + return null; + }, + TimeSpan.FromMinutes(2), + true); - public static string[]? GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + return result; + } + + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + /// + /// + /// + /// + public static int[]? CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id; + IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => { - var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; - var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); - var result = runtimeCache.GetCacheItem(cacheKey, () => + var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId!.Value).Distinct() + .ToArray(); + var usn = user.StartMediaIds; + if (usn is not null) { - var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); + var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); return vals; - }, TimeSpan.FromMinutes(2), true); + } - return result; - } + return null; + }, + TimeSpan.FromMinutes(2), + true); - private static bool StartsWithPath(string test, string path) + return result; + } + + public static string[]? GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; + IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => { - return test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; - } + var startNodeIds = user.CalculateMediaStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, + TimeSpan.FromMinutes(2), + true); - private static string GetBinPath(UmbracoObjectTypes objectType) + return result; + } + + public static string[]? GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; + IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => { - var binPath = Constants.System.RootString + ","; - switch (objectType) - { - case UmbracoObjectTypes.Document: - binPath += Constants.System.RecycleBinContentString; - break; - case UmbracoObjectTypes.Media: - binPath += Constants.System.RecycleBinMediaString; - break; - default: - throw new ArgumentOutOfRangeException(nameof(objectType)); - } - return binPath; - } + var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path) + .ToArray(); + return vals; + }, + TimeSpan.FromMinutes(2), + true); - internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) + return result; + } + + internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) + { + // assume groupSn and userSn each don't contain duplicates + var asn = groupSn.Concat(userSn).Distinct().ToArray(); + Dictionary paths = asn.Length > 0 + ? entityService.GetAllPaths(objectType, asn).ToDictionary(x => x.Id, x => x.Path) + : new Dictionary(); + + paths[Constants.System.Root] = Constants.System.RootString; // entityService does not get that one + + var binPath = GetBinPath(objectType); + + var lsn = new List(); + foreach (var sn in groupSn) { - // assume groupSn and userSn each don't contain duplicates - - var asn = groupSn.Concat(userSn).Distinct().ToArray(); - var paths = asn.Length > 0 - ? entityService.GetAllPaths(objectType, asn).ToDictionary(x => x.Id, x => x.Path) - : new Dictionary(); - - paths[Constants.System.Root] = Constants.System.RootString; // entityService does not get that one - - var binPath = GetBinPath(objectType); - - var lsn = new List(); - foreach (var sn in groupSn) + if (paths.TryGetValue(sn, out var snp) == false) { - if (paths.TryGetValue(sn, out var snp) == false) continue; // ignore rogue node (no path) - - if (StartsWithPath(snp, binPath)) continue; // ignore bin - - if (lsn.Any(x => StartsWithPath(snp, paths[x]))) continue; // skip if something above this sn - lsn.RemoveAll(x => StartsWithPath(paths[x], snp)); // remove anything below this sn - lsn.Add(sn); + continue; // ignore rogue node (no path) } - var usn = new List(); - foreach (var sn in userSn) + if (StartsWithPath(snp, binPath)) { - if (paths.TryGetValue(sn, out var snp) == false) continue; // ignore rogue node (no path) - - if (StartsWithPath(snp, binPath)) continue; // ignore bin - - if (usn.Any(x => StartsWithPath(paths[x], snp))) continue; // skip if something below this sn - usn.RemoveAll(x => StartsWithPath(snp, paths[x])); // remove anything above this sn - usn.Add(sn); + continue; // ignore bin } - foreach (var sn in usn) + if (lsn.Any(x => StartsWithPath(snp, paths[x]))) { - var snp = paths[sn]; // has to be here now - lsn.RemoveAll(x => StartsWithPath(snp, paths[x]) || StartsWithPath(paths[x], snp)); // remove anything above or below this sn - lsn.Add(sn); + continue; // skip if something above this sn } - return lsn.ToArray(); + lsn.RemoveAll(x => StartsWithPath(paths[x], snp)); // remove anything below this sn + lsn.Add(sn); } + + var usn = new List(); + foreach (var sn in userSn) + { + if (paths.TryGetValue(sn, out var snp) == false) + { + continue; // ignore rogue node (no path) + } + + if (StartsWithPath(snp, binPath)) + { + continue; // ignore bin + } + + if (usn.Any(x => StartsWithPath(paths[x], snp))) + { + continue; // skip if something below this sn + } + + usn.RemoveAll(x => StartsWithPath(snp, paths[x])); // remove anything above this sn + usn.Add(sn); + } + + foreach (var sn in usn) + { + var snp = paths[sn]; // has to be here now + lsn.RemoveAll(x => + StartsWithPath(snp, paths[x]) || + StartsWithPath(paths[x], snp)); // remove anything above or below this sn + lsn.Add(sn); + } + + return lsn.ToArray(); + } + + private static bool StartsWithPath(string test, string path) => + test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; + + private static string GetBinPath(UmbracoObjectTypes objectType) + { + var binPath = Constants.System.RootString + ","; + switch (objectType) + { + case UmbracoObjectTypes.Document: + binPath += Constants.System.RecycleBinContentString; + break; + case UmbracoObjectTypes.Media: + binPath += Constants.System.RecycleBinMediaString; + break; + default: + throw new ArgumentOutOfRangeException(nameof(objectType)); + } + + return binPath; } } diff --git a/src/Umbraco.Core/Models/UserTourStatus.cs b/src/Umbraco.Core/Models/UserTourStatus.cs index 72e0a81cba..a954a0b864 100644 --- a/src/Umbraco.Core/Models/UserTourStatus.cs +++ b/src/Umbraco.Core/Models/UserTourStatus.cs @@ -1,60 +1,69 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// A model representing the tours a user has taken/completed +/// +[DataContract(Name = "userTourStatus", Namespace = "")] +public class UserTourStatus : IEquatable { /// - /// A model representing the tours a user has taken/completed + /// The tour alias /// - [DataContract(Name = "userTourStatus", Namespace = "")] - public class UserTourStatus : IEquatable + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; + + /// + /// If the tour is completed + /// + [DataMember(Name = "completed")] + public bool Completed { get; set; } + + /// + /// If the tour is disabled + /// + [DataMember(Name = "disabled")] + public bool Disabled { get; set; } + + public static bool operator ==(UserTourStatus? left, UserTourStatus? right) => Equals(left, right); + + public bool Equals(UserTourStatus? other) { - /// - /// The tour alias - /// - [DataMember(Name = "alias")] - public string Alias { get; set; } = string.Empty; - - /// - /// If the tour is completed - /// - [DataMember(Name = "completed")] - public bool Completed { get; set; } - - /// - /// If the tour is disabled - /// - [DataMember(Name = "disabled")] - public bool Disabled { get; set; } - - public bool Equals(UserTourStatus? other) + if (ReferenceEquals(null, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(Alias, other.Alias); + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((UserTourStatus) obj); + return true; } - public override int GetHashCode() - { - return Alias.GetHashCode(); - } - - public static bool operator ==(UserTourStatus? left, UserTourStatus? right) - { - return Equals(left, right); - } - - public static bool operator !=(UserTourStatus? left, UserTourStatus? right) - { - return !Equals(left, right); - } + return string.Equals(Alias, other.Alias); } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((UserTourStatus)obj); + } + + public override int GetHashCode() => Alias.GetHashCode(); + + public static bool operator !=(UserTourStatus? left, UserTourStatus? right) => !Equals(left, right); } diff --git a/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs b/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs index 095d4f50a9..acdaed7dd9 100644 --- a/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs +++ b/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +[DataContract] +public class UserTwoFactorProviderModel { - [DataContract] - public class UserTwoFactorProviderModel + public UserTwoFactorProviderModel(string providerName, bool isEnabledOnUser) { - public UserTwoFactorProviderModel(string providerName, bool isEnabledOnUser) - { - ProviderName = providerName; - IsEnabledOnUser = isEnabledOnUser; - } - - [DataMember(Name = "providerName")] - public string ProviderName { get; } - - [DataMember(Name = "isEnabledOnUser")] - public bool IsEnabledOnUser { get; } + ProviderName = providerName; + IsEnabledOnUser = isEnabledOnUser; } + + [DataMember(Name = "providerName")] + public string ProviderName { get; } + + [DataMember(Name = "isEnabledOnUser")] + public bool IsEnabledOnUser { get; } } diff --git a/src/Umbraco.Core/Models/ValidatePasswordResetCodeModel.cs b/src/Umbraco.Core/Models/ValidatePasswordResetCodeModel.cs index d104383b38..b4ebb89e47 100644 --- a/src/Umbraco.Core/Models/ValidatePasswordResetCodeModel.cs +++ b/src/Umbraco.Core/Models/ValidatePasswordResetCodeModel.cs @@ -1,19 +1,17 @@ -using System; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models -{ - [Serializable] - [DataContract(Name = "validatePasswordReset", Namespace = "")] - public class ValidatePasswordResetCodeModel - { - [Required] - [DataMember(Name = "userId", IsRequired = true)] - public int UserId { get; set; } +namespace Umbraco.Cms.Core.Models; - [Required] - [DataMember(Name = "resetCode", IsRequired = true)] - public string? ResetCode { get; set; } - } +[Serializable] +[DataContract(Name = "validatePasswordReset", Namespace = "")] +public class ValidatePasswordResetCodeModel +{ + [Required] + [DataMember(Name = "userId", IsRequired = true)] + public int UserId { get; set; } + + [Required] + [DataMember(Name = "resetCode", IsRequired = true)] + public string? ResetCode { get; set; } } diff --git a/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs b/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs index 10133a7f36..bffd551815 100644 --- a/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs +++ b/src/Umbraco.Core/Models/Validation/RequiredForPersistenceAttribute.cs @@ -1,31 +1,32 @@ -using System.ComponentModel.DataAnnotations; -using System.Linq; +using System.ComponentModel.DataAnnotations; using System.Reflection; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Models.Validation +namespace Umbraco.Cms.Core.Models.Validation; + +/// +/// Specifies that a data field value is required in order to persist an object. +/// +/// +/// +/// There are two levels of validation in Umbraco. (1) value validation is performed by +/// +/// instances; it can prevent a content item from being published, but not from being saved. (2) required +/// validation +/// of properties marked with ; it does prevent an object from being +/// saved +/// and is used for properties that are absolutely mandatory, such as the name of a content item. +/// +/// +public class RequiredForPersistenceAttribute : RequiredAttribute { /// - /// Specifies that a data field value is required in order to persist an object. + /// Determines whether an object has all required values for persistence. /// - /// - /// There are two levels of validation in Umbraco. (1) value validation is performed by - /// instances; it can prevent a content item from being published, but not from being saved. (2) required validation - /// of properties marked with ; it does prevent an object from being saved - /// and is used for properties that are absolutely mandatory, such as the name of a content item. - /// - public class RequiredForPersistenceAttribute : RequiredAttribute - { - /// - /// Determines whether an object has all required values for persistence. - /// - public static bool HasRequiredValuesForPersistence(object model) + public static bool HasRequiredValuesForPersistence(object model) => + model.GetType().GetProperties().All(x => { - return model.GetType().GetProperties().All(x => - { - var a = x.GetCustomAttribute(); - return a == null || a.IsValid(x.GetValue(model)); - }); - } - } + RequiredForPersistenceAttribute? a = x.GetCustomAttribute(); + return a == null || a.IsValid(x.GetValue(model)); + }); } diff --git a/src/Umbraco.Core/Models/ValueStorageType.cs b/src/Umbraco.Core/Models/ValueStorageType.cs index cca84b72b7..975369f993 100644 --- a/src/Umbraco.Core/Models/ValueStorageType.cs +++ b/src/Umbraco.Core/Models/ValueStorageType.cs @@ -1,47 +1,45 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents the supported database types for storing a value. +/// +[Serializable] +[DataContract] +public enum ValueStorageType { + // note: these values are written out in the database in some places, + // and then parsed back in a case-sensitive way - think about it before + // changing the casing of values. + /// - /// Represents the supported database types for storing a value. + /// Store property value as NText. /// - [Serializable] - [DataContract] - public enum ValueStorageType - { - // note: these values are written out in the database in some places, - // and then parsed back in a case-sensitive way - think about it before - // changing the casing of values. + [EnumMember] + Ntext, - /// - /// Store property value as NText. - /// - [EnumMember] - Ntext, + /// + /// Store property value as NVarChar. + /// + [EnumMember] + Nvarchar, - /// - /// Store property value as NVarChar. - /// - [EnumMember] - Nvarchar, + /// + /// Store property value as Integer. + /// + [EnumMember] + Integer, - /// - /// Store property value as Integer. - /// - [EnumMember] - Integer, + /// + /// Store property value as Date. + /// + [EnumMember] + Date, - /// - /// Store property value as Date. - /// - [EnumMember] - Date, - - /// - /// Store property value as Decimal. - /// - [EnumMember] - Decimal - } + /// + /// Store property value as Decimal. + /// + [EnumMember] + Decimal, } diff --git a/src/Umbraco.Core/MonitorLock.cs b/src/Umbraco.Core/MonitorLock.cs index 11651aaa6c..45dbdbbd10 100644 --- a/src/Umbraco.Core/MonitorLock.cs +++ b/src/Umbraco.Core/MonitorLock.cs @@ -1,32 +1,31 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Provides an equivalent to the c# lock statement, to be used in a using block. +/// +/// Ie replace lock (o) {...} by using (new MonitorLock(o)) { ... } +public class MonitorLock : IDisposable { + private readonly bool _entered; + private readonly object _locker; + /// - /// Provides an equivalent to the c# lock statement, to be used in a using block. + /// Initializes a new instance of the class with an object to lock. /// - /// Ie replace lock (o) {...} by using (new MonitorLock(o)) { ... } - public class MonitorLock : IDisposable + /// The object to lock. + /// Should always be used within a using block. + public MonitorLock(object locker) { - private readonly object _locker; - private readonly bool _entered; + _locker = locker; + _entered = false; + Monitor.Enter(_locker, ref _entered); + } - /// - /// Initializes a new instance of the class with an object to lock. - /// - /// The object to lock. - /// Should always be used within a using block. - public MonitorLock(object locker) + void IDisposable.Dispose() + { + if (_entered) { - _locker = locker; - _entered = false; - System.Threading.Monitor.Enter(_locker, ref _entered); - } - - void IDisposable.Dispose() - { - if (_entered) - System.Threading.Monitor.Exit(_locker); + Monitor.Exit(_locker); } } } diff --git a/src/Umbraco.Core/NamedUdiRange.cs b/src/Umbraco.Core/NamedUdiRange.cs index 5855f27926..e0d52df9f4 100644 --- a/src/Umbraco.Core/NamedUdiRange.cs +++ b/src/Umbraco.Core/NamedUdiRange.cs @@ -1,34 +1,34 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a complemented with a name. +/// +public class NamedUdiRange : UdiRange { /// - /// Represents a complemented with a name. + /// Initializes a new instance of the class with a and an optional + /// selector. /// - public class NamedUdiRange : UdiRange + /// A . + /// An optional selector. + public NamedUdiRange(Udi udi, string selector = Constants.DeploySelector.This) + : base(udi, selector) { - /// - /// Initializes a new instance of the class with a and an optional selector. - /// - /// A . - /// An optional selector. - public NamedUdiRange(Udi udi, string selector = Constants.DeploySelector.This) - : base(udi, selector) - { } - - /// - /// Initializes a new instance of the class with a , a name, and an optional selector. - /// - /// A . - /// A name. - /// An optional selector. - public NamedUdiRange(Udi udi, string name, string selector = Constants.DeploySelector.This) - : base(udi, selector) - { - Name = name; - } - - /// - /// Gets or sets the name of the range. - /// - public string? Name { get; set; } } + + /// + /// Initializes a new instance of the class with a , a name, and an + /// optional selector. + /// + /// A . + /// A name. + /// An optional selector. + public NamedUdiRange(Udi udi, string name, string selector = Constants.DeploySelector.This) + : base(udi, selector) => + Name = name; + + /// + /// Gets or sets the name of the range. + /// + public string? Name { get; set; } } diff --git a/src/Umbraco.Core/Net/IIpResolver.cs b/src/Umbraco.Core/Net/IIpResolver.cs index 6c7ab72dec..edc9c6428c 100644 --- a/src/Umbraco.Core/Net/IIpResolver.cs +++ b/src/Umbraco.Core/Net/IIpResolver.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Net +namespace Umbraco.Cms.Core.Net; + +public interface IIpResolver { - public interface IIpResolver - { - string GetCurrentRequestIpAddress(); - } + string GetCurrentRequestIpAddress(); } diff --git a/src/Umbraco.Core/Net/ISessionIdResolver.cs b/src/Umbraco.Core/Net/ISessionIdResolver.cs index f5d6b4de29..4ec6248b39 100644 --- a/src/Umbraco.Core/Net/ISessionIdResolver.cs +++ b/src/Umbraco.Core/Net/ISessionIdResolver.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Net +namespace Umbraco.Cms.Core.Net; + +public interface ISessionIdResolver { - public interface ISessionIdResolver - { - string? SessionId { get; } - } + string? SessionId { get; } } diff --git a/src/Umbraco.Core/Net/IUserAgentProvider.cs b/src/Umbraco.Core/Net/IUserAgentProvider.cs index ba4f61b897..6916ee8d37 100644 --- a/src/Umbraco.Core/Net/IUserAgentProvider.cs +++ b/src/Umbraco.Core/Net/IUserAgentProvider.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Net +namespace Umbraco.Cms.Core.Net; + +public interface IUserAgentProvider { - public interface IUserAgentProvider - { - string? GetUserAgent(); - } + string? GetUserAgent(); } diff --git a/src/Umbraco.Core/Net/NullSessionIdResolver.cs b/src/Umbraco.Core/Net/NullSessionIdResolver.cs index 207a9c6048..c76c6c8632 100644 --- a/src/Umbraco.Core/Net/NullSessionIdResolver.cs +++ b/src/Umbraco.Core/Net/NullSessionIdResolver.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Net +namespace Umbraco.Cms.Core.Net; + +public class NullSessionIdResolver : ISessionIdResolver { - public class NullSessionIdResolver : ISessionIdResolver - { - public string? SessionId => null; - } + public string? SessionId => null; } diff --git a/src/Umbraco.Core/Notifications/ApplicationCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/ApplicationCacheRefresherNotification.cs index eb596a3a0b..cd0b1326f4 100644 --- a/src/Umbraco.Core/Notifications/ApplicationCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/ApplicationCacheRefresherNotification.cs @@ -1,11 +1,13 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ApplicationCacheRefresherNotification : CacheRefresherNotification { - public class ApplicationCacheRefresherNotification : CacheRefresherNotification + public ApplicationCacheRefresherNotification(object messageObject, MessageType messageType) + : base( + messageObject, + messageType) { - public ApplicationCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs b/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs index adcf14d636..23438827fd 100644 --- a/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs +++ b/src/Umbraco.Core/Notifications/AssignedMemberRolesNotification.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications -{ - public class AssignedMemberRolesNotification : MemberRolesNotification - { - public AssignedMemberRolesNotification(int[] memberIds, string[] roles) : base(memberIds, roles) - { +namespace Umbraco.Cms.Core.Notifications; - } +public class AssignedMemberRolesNotification : MemberRolesNotification +{ + public AssignedMemberRolesNotification(int[] memberIds, string[] roles) + : base(memberIds, roles) + { } } diff --git a/src/Umbraco.Core/Notifications/AssignedUserGroupPermissionsNotification.cs b/src/Umbraco.Core/Notifications/AssignedUserGroupPermissionsNotification.cs index 18425f2393..347f1934bc 100644 --- a/src/Umbraco.Core/Notifications/AssignedUserGroupPermissionsNotification.cs +++ b/src/Umbraco.Core/Notifications/AssignedUserGroupPermissionsNotification.cs @@ -1,15 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public class AssignedUserGroupPermissionsNotification : EnumerableObjectNotification - { - public AssignedUserGroupPermissionsNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IEnumerable EntityPermissions => Target; +public class AssignedUserGroupPermissionsNotification : EnumerableObjectNotification +{ + public AssignedUserGroupPermissionsNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } + + public IEnumerable EntityPermissions => Target; } diff --git a/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs index bd110ad878..637c05dfb0 100644 --- a/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/CacheRefresherNotification.cs @@ -1,20 +1,19 @@ -using System; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications -{ - /// - /// Base class for cache refresher notifications - /// - public abstract class CacheRefresherNotification : INotification - { - public CacheRefresherNotification(object messageObject, MessageType messageType) - { - MessageObject = messageObject ?? throw new ArgumentNullException(nameof(messageObject)); - MessageType = messageType; - } +namespace Umbraco.Cms.Core.Notifications; - public object MessageObject { get; } - public MessageType MessageType { get; } +/// +/// Base class for cache refresher notifications +/// +public abstract class CacheRefresherNotification : INotification +{ + public CacheRefresherNotification(object messageObject, MessageType messageType) + { + MessageObject = messageObject ?? throw new ArgumentNullException(nameof(messageObject)); + MessageType = messageType; } + + public object MessageObject { get; } + + public MessageType MessageType { get; } } diff --git a/src/Umbraco.Core/Notifications/CancelableEnumerableObjectNotification.cs b/src/Umbraco.Core/Notifications/CancelableEnumerableObjectNotification.cs index ea7476cd3f..1f51e68409 100644 --- a/src/Umbraco.Core/Notifications/CancelableEnumerableObjectNotification.cs +++ b/src/Umbraco.Core/Notifications/CancelableEnumerableObjectNotification.cs @@ -1,18 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CancelableEnumerableObjectNotification : CancelableObjectNotification> { - public abstract class CancelableEnumerableObjectNotification : CancelableObjectNotification> + protected CancelableEnumerableObjectNotification(T target, EventMessages messages) + : base(new[] { target }, messages) + { + } + + protected CancelableEnumerableObjectNotification(IEnumerable target, EventMessages messages) + : base( + target, + messages) { - protected CancelableEnumerableObjectNotification(T target, EventMessages messages) : base(new [] {target}, messages) - { - } - protected CancelableEnumerableObjectNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/CancelableNotification.cs b/src/Umbraco.Core/Notifications/CancelableNotification.cs index 13989d50da..438bc1ee99 100644 --- a/src/Umbraco.Core/Notifications/CancelableNotification.cs +++ b/src/Umbraco.Core/Notifications/CancelableNotification.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class CancelableNotification : StatefulNotification, ICancelableNotification { - public class CancelableNotification : StatefulNotification, ICancelableNotification + public CancelableNotification(EventMessages messages) => Messages = messages; + + public EventMessages Messages { get; } + + public bool Cancel { get; set; } + + public void CancelOperation(EventMessage cancellationMessage) { - public CancelableNotification(EventMessages messages) => Messages = messages; - - public EventMessages Messages { get; } - - public bool Cancel { get; set; } - - public void CancelOperation(EventMessage cancellationMessage) - { - Cancel = true; - cancellationMessage.IsDefaultEventMessage = true; - Messages.Add(cancellationMessage); - } + Cancel = true; + cancellationMessage.IsDefaultEventMessage = true; + Messages.Add(cancellationMessage); } } diff --git a/src/Umbraco.Core/Notifications/CancelableObjectNotification.cs b/src/Umbraco.Core/Notifications/CancelableObjectNotification.cs index 25f6a4474f..be15626eb0 100644 --- a/src/Umbraco.Core/Notifications/CancelableObjectNotification.cs +++ b/src/Umbraco.Core/Notifications/CancelableObjectNotification.cs @@ -3,21 +3,22 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CancelableObjectNotification : ObjectNotification, ICancelableNotification + where T : class { - public abstract class CancelableObjectNotification : ObjectNotification, ICancelableNotification where T : class + protected CancelableObjectNotification(T target, EventMessages messages) + : base(target, messages) { - protected CancelableObjectNotification(T target, EventMessages messages) : base(target, messages) - { - } + } - public bool Cancel { get; set; } + public bool Cancel { get; set; } - public void CancelOperation(EventMessage cancelationMessage) - { - Cancel = true; - cancelationMessage.IsDefaultEventMessage = true; - Messages.Add(cancelationMessage); - } + public void CancelOperation(EventMessage cancelationMessage) + { + Cancel = true; + cancelationMessage.IsDefaultEventMessage = true; + Messages.Add(cancelationMessage); } } diff --git a/src/Umbraco.Core/Notifications/ContentCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/ContentCacheRefresherNotification.cs index 35b4f472c7..67a43b5ac2 100644 --- a/src/Umbraco.Core/Notifications/ContentCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentCacheRefresherNotification.cs @@ -1,11 +1,13 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ContentCacheRefresherNotification : CacheRefresherNotification { - public class ContentCacheRefresherNotification : CacheRefresherNotification + public ContentCacheRefresherNotification(object messageObject, MessageType messageType) + : base( + messageObject, + messageType) { - public ContentCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentCopiedNotification.cs b/src/Umbraco.Core/Notifications/ContentCopiedNotification.cs index 6399fb714d..a5c6ede432 100644 --- a/src/Umbraco.Core/Notifications/ContentCopiedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentCopiedNotification.cs @@ -4,13 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentCopiedNotification : CopiedNotification { - public sealed class ContentCopiedNotification : CopiedNotification + public ContentCopiedNotification(IContent original, IContent copy, int parentId, bool relateToOriginal, EventMessages messages) + : base(original, copy, parentId, relateToOriginal, messages) { - public ContentCopiedNotification(IContent original, IContent copy, int parentId, bool relateToOriginal, EventMessages messages) - : base(original, copy, parentId, relateToOriginal, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentCopyingNotification.cs b/src/Umbraco.Core/Notifications/ContentCopyingNotification.cs index d30d49efeb..ef8eb48058 100644 --- a/src/Umbraco.Core/Notifications/ContentCopyingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentCopyingNotification.cs @@ -4,13 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentCopyingNotification : CopyingNotification { - public sealed class ContentCopyingNotification : CopyingNotification + public ContentCopyingNotification(IContent original, IContent copy, int parentId, EventMessages messages) + : base(original, copy, parentId, messages) { - public ContentCopyingNotification(IContent original, IContent copy, int parentId, EventMessages messages) - : base(original, copy, parentId, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentDeletedBlueprintNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletedBlueprintNotification.cs index 1c516a295f..884fcf493b 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletedBlueprintNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletedBlueprintNotification.cs @@ -1,22 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletedBlueprintNotification : EnumerableObjectNotification { - public sealed class ContentDeletedBlueprintNotification : EnumerableObjectNotification + public ContentDeletedBlueprintNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentDeletedBlueprintNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentDeletedBlueprintNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable DeletedBlueprints => Target; } + + public ContentDeletedBlueprintNotification(IEnumerable target, EventMessages messages) + : base( + target, + messages) + { + } + + public IEnumerable DeletedBlueprints => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentDeletedNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletedNotification.cs index 6398c4f28e..c68a07b1f0 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletedNotification : DeletedNotification { - public sealed class ContentDeletedNotification : DeletedNotification + public ContentDeletedNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentDeletedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentDeletedVersionsNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletedVersionsNotification.cs index 30f00b52bf..5e2b646008 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletedVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletedVersionsNotification.cs @@ -1,16 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletedVersionsNotification : DeletedVersionsNotification { - public sealed class ContentDeletedVersionsNotification : DeletedVersionsNotification + public ContentDeletedVersionsNotification( + int id, + EventMessages messages, + int specificVersion = default, + bool deletePriorVersions = false, + DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - public ContentDeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentDeletingNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletingNotification.cs index ee02c6f339..de4176a01b 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentDeletingNotification : DeletingNotification - { - public ContentDeletingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentDeletingNotification : DeletingNotification +{ + public ContentDeletingNotification(IContent target, EventMessages messages) + : base(target, messages) + { + } + + public ContentDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentDeletingVersionsNotification.cs b/src/Umbraco.Core/Notifications/ContentDeletingVersionsNotification.cs index 340aaaa559..5d173bcc0c 100644 --- a/src/Umbraco.Core/Notifications/ContentDeletingVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentDeletingVersionsNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentDeletingVersionsNotification : DeletingVersionsNotification { - public sealed class ContentDeletingVersionsNotification : DeletingVersionsNotification + public ContentDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - public ContentDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentEmptiedRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/ContentEmptiedRecycleBinNotification.cs index 1453553efa..9a1637dda9 100644 --- a/src/Umbraco.Core/Notifications/ContentEmptiedRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentEmptiedRecycleBinNotification.cs @@ -1,16 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentEmptiedRecycleBinNotification : EmptiedRecycleBinNotification { - public sealed class ContentEmptiedRecycleBinNotification : EmptiedRecycleBinNotification + public ContentEmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) + : base( + deletedEntities, messages) { - public ContentEmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) : base(deletedEntities, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentEmptyingRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/ContentEmptyingRecycleBinNotification.cs index 134e65d982..f55d1166ce 100644 --- a/src/Umbraco.Core/Notifications/ContentEmptyingRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentEmptyingRecycleBinNotification.cs @@ -1,16 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentEmptyingRecycleBinNotification : EmptyingRecycleBinNotification { - public sealed class ContentEmptyingRecycleBinNotification : EmptyingRecycleBinNotification + public ContentEmptyingRecycleBinNotification(IEnumerable? deletedEntities, EventMessages messages) + : base( + deletedEntities, messages) { - public ContentEmptyingRecycleBinNotification(IEnumerable? deletedEntities, EventMessages messages) : base(deletedEntities, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentMovedNotification.cs b/src/Umbraco.Core/Notifications/ContentMovedNotification.cs index 607d678049..50bd24876d 100644 --- a/src/Umbraco.Core/Notifications/ContentMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentMovedNotification.cs @@ -1,20 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentMovedNotification : MovedNotification - { - public ContentMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentMovedNotification : MovedNotification +{ + public ContentMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public ContentMovedNotification(IEnumerable> target, EventMessages messages) + : base( + target, + messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentMovedToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/ContentMovedToRecycleBinNotification.cs index 3b736b1409..bf5415d9d1 100644 --- a/src/Umbraco.Core/Notifications/ContentMovedToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentMovedToRecycleBinNotification.cs @@ -1,20 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentMovedToRecycleBinNotification : MovedToRecycleBinNotification - { - public ContentMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentMovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentMovedToRecycleBinNotification : MovedToRecycleBinNotification +{ + public ContentMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentMovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentMovingNotification.cs b/src/Umbraco.Core/Notifications/ContentMovingNotification.cs index 01c04eb226..eddc7a13f7 100644 --- a/src/Umbraco.Core/Notifications/ContentMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentMovingNotification.cs @@ -1,20 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentMovingNotification : MovingNotification - { - public ContentMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentMovingNotification : MovingNotification +{ + public ContentMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public ContentMovingNotification(IEnumerable> target, EventMessages messages) + : base( + target, + messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentMovingToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/ContentMovingToRecycleBinNotification.cs index 88aa48c7b8..5a691c6487 100644 --- a/src/Umbraco.Core/Notifications/ContentMovingToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentMovingToRecycleBinNotification.cs @@ -1,20 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentMovingToRecycleBinNotification : MovingToRecycleBinNotification - { - public ContentMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentMovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentMovingToRecycleBinNotification : MovingToRecycleBinNotification +{ + public ContentMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentMovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentNotificationExtensions.cs b/src/Umbraco.Core/Notifications/ContentNotificationExtensions.cs index c009b1cb62..a7449f24cc 100644 --- a/src/Umbraco.Core/Notifications/ContentNotificationExtensions.cs +++ b/src/Umbraco.Core/Notifications/ContentNotificationExtensions.cs @@ -3,65 +3,67 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public static class ContentNotificationExtensions { - public static class ContentNotificationExtensions - { - /// - /// Determines whether a culture is being saved, during a Saving notification - /// - public static bool IsSavingCulture(this SavingNotification notification, T content, string culture) where T : IContentBase - => (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos cultureInfo) ?? false) && cultureInfo.IsDirty(); + /// + /// Determines whether a culture is being saved, during a Saving notification + /// + public static bool IsSavingCulture(this SavingNotification notification, T content, string culture) + where T : IContentBase + => (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos cultureInfo) ?? false) && + cultureInfo.IsDirty(); - /// - /// Determines whether a culture has been saved, during a Saved notification - /// - public static bool HasSavedCulture(this SavedNotification notification, T content, string culture) where T : IContentBase - => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UpdatedCulture + culture); + /// + /// Determines whether a culture has been saved, during a Saved notification + /// + public static bool HasSavedCulture(this SavedNotification notification, T content, string culture) + where T : IContentBase + => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UpdatedCulture + culture); - /// - /// Determines whether a culture is being published, during a Publishing notification - /// - public static bool IsPublishingCulture(this ContentPublishingNotification notification, IContent content, string culture) - => IsPublishingCulture(content, culture); + /// + /// Determines whether a culture is being published, during a Publishing notification + /// + public static bool IsPublishingCulture(this ContentPublishingNotification notification, IContent content, string culture) + => IsPublishingCulture(content, culture); - /// - /// Determines whether a culture is being unpublished, during an Publishing notification - /// - public static bool IsUnpublishingCulture(this ContentPublishingNotification notification, IContent content, string culture) - => IsUnpublishingCulture(content, culture); + /// + /// Determines whether a culture is being unpublished, during an Publishing notification + /// + public static bool IsUnpublishingCulture(this ContentPublishingNotification notification, IContent content, string culture) + => IsUnpublishingCulture(content, culture); - /// - /// Determines whether a culture is being unpublished, during a Unpublishing notification - /// - public static bool IsUnpublishingCulture(this ContentUnpublishingNotification notification, IContent content, string culture) - => IsUnpublishingCulture(content, culture); + /// + /// Determines whether a culture is being unpublished, during a Unpublishing notification + /// + public static bool IsUnpublishingCulture(this ContentUnpublishingNotification notification, IContent content, string culture) => IsUnpublishingCulture(content, culture); - /// - /// Determines whether a culture has been published, during a Published notification - /// - public static bool HasPublishedCulture(this ContentPublishedNotification notification, IContent content, string culture) - => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.ChangedCulture + culture); + /// + /// Determines whether a culture has been published, during a Published notification + /// + public static bool HasPublishedCulture(this ContentPublishedNotification notification, IContent content, string culture) + => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.ChangedCulture + culture); - /// - /// Determines whether a culture has been unpublished, during a Published notification - /// - public static bool HasUnpublishedCulture(this ContentPublishedNotification notification, IContent content, string culture) - => HasUnpublishedCulture(content, culture); + /// + /// Determines whether a culture has been unpublished, during a Published notification + /// + public static bool HasUnpublishedCulture(this ContentPublishedNotification notification, IContent content, string culture) + => HasUnpublishedCulture(content, culture); - /// - /// Determines whether a culture has been unpublished, during an Unpublished notification - /// - public static bool HasUnpublishedCulture(this ContentUnpublishedNotification notification, IContent content, string culture) - => HasUnpublishedCulture(content, culture); + /// + /// Determines whether a culture has been unpublished, during an Unpublished notification + /// + public static bool HasUnpublishedCulture(this ContentUnpublishedNotification notification, IContent content, string culture) + => HasUnpublishedCulture(content, culture); - private static bool IsUnpublishingCulture(IContent content, string culture) - => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); + public static bool IsPublishingCulture(IContent content, string culture) + => (content.PublishCultureInfos?.TryGetValue(culture, out ContentCultureInfos cultureInfo) ?? false) && + cultureInfo.IsDirty(); - public static bool IsPublishingCulture(IContent content, string culture) - => (content.PublishCultureInfos?.TryGetValue(culture, out ContentCultureInfos cultureInfo) ?? false) && cultureInfo.IsDirty(); + private static bool IsUnpublishingCulture(IContent content, string culture) + => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); - public static bool HasUnpublishedCulture(IContent content, string culture) - => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); - } + public static bool HasUnpublishedCulture(IContent content, string culture) + => content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); } diff --git a/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs b/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs index 69d1751e58..0400155d3c 100644 --- a/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs @@ -1,22 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentPublishedNotification : EnumerableObjectNotification { - public sealed class ContentPublishedNotification : EnumerableObjectNotification + public ContentPublishedNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentPublishedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentPublishedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable PublishedEntities => Target; } + + public ContentPublishedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable PublishedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentPublishingNotification.cs b/src/Umbraco.Core/Notifications/ContentPublishingNotification.cs index 65a8efdadf..c9a1110089 100644 --- a/src/Umbraco.Core/Notifications/ContentPublishingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentPublishingNotification.cs @@ -1,22 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentPublishingNotification : CancelableEnumerableObjectNotification { - public sealed class ContentPublishingNotification : CancelableEnumerableObjectNotification + public ContentPublishingNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentPublishingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentPublishingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable PublishedEntities => Target; } + + public ContentPublishingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable PublishedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs b/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs index b9cda7722c..f2d18fbba1 100644 --- a/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs @@ -1,17 +1,15 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ +namespace Umbraco.Cms.Core.Notifications; - [Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class ContentRefreshNotification : EntityRefreshNotification +[Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class ContentRefreshNotification : EntityRefreshNotification +{ + public ContentRefreshNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentRefreshNotification(IContent target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentRolledBackNotification.cs b/src/Umbraco.Core/Notifications/ContentRolledBackNotification.cs index a1f370bd94..50b89e10b8 100644 --- a/src/Umbraco.Core/Notifications/ContentRolledBackNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentRolledBackNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentRolledBackNotification : RolledBackNotification { - public sealed class ContentRolledBackNotification : RolledBackNotification + public ContentRolledBackNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentRolledBackNotification(IContent target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentRollingBackNotification.cs b/src/Umbraco.Core/Notifications/ContentRollingBackNotification.cs index e12bfa1631..29b864853c 100644 --- a/src/Umbraco.Core/Notifications/ContentRollingBackNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentRollingBackNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentRollingBackNotification : RollingBackNotification { - public sealed class ContentRollingBackNotification : RollingBackNotification + public ContentRollingBackNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentRollingBackNotification(IContent target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs b/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs index 6addde88c1..d06f364ed2 100644 --- a/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs @@ -4,14 +4,14 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSavedBlueprintNotification : ObjectNotification - { - public ContentSavedBlueprintNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IContent SavedBlueprint => Target; +public sealed class ContentSavedBlueprintNotification : ObjectNotification +{ + public ContentSavedBlueprintNotification(IContent target, EventMessages messages) + : base(target, messages) + { } + + public IContent SavedBlueprint => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSavedNotification.cs b/src/Umbraco.Core/Notifications/ContentSavedNotification.cs index b58a366368..2d3253117d 100644 --- a/src/Umbraco.Core/Notifications/ContentSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSavedNotification : SavedNotification - { - public ContentSavedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentSavedNotification : SavedNotification +{ + public ContentSavedNotification(IContent target, EventMessages messages) + : base(target, messages) + { + } + + public ContentSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentSavingNotification.cs b/src/Umbraco.Core/Notifications/ContentSavingNotification.cs index afe21bf870..4a57a10f29 100644 --- a/src/Umbraco.Core/Notifications/ContentSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSavingNotification : SavingNotification - { - public ContentSavingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class ContentSavingNotification : SavingNotification +{ + public ContentSavingNotification(IContent target, EventMessages messages) + : base(target, messages) + { + } + + public ContentSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentSendingToPublishNotification.cs b/src/Umbraco.Core/Notifications/ContentSendingToPublishNotification.cs index 0a5c018883..7d5ee26130 100644 --- a/src/Umbraco.Core/Notifications/ContentSendingToPublishNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSendingToPublishNotification.cs @@ -4,14 +4,14 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSendingToPublishNotification : CancelableObjectNotification - { - public ContentSendingToPublishNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IContent Entity => Target; +public sealed class ContentSendingToPublishNotification : CancelableObjectNotification +{ + public ContentSendingToPublishNotification(IContent target, EventMessages messages) + : base(target, messages) + { } + + public IContent Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs b/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs index c5e2e5dc3b..e10b9930e3 100644 --- a/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSentToPublishNotification.cs @@ -4,14 +4,14 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class ContentSentToPublishNotification : ObjectNotification - { - public ContentSentToPublishNotification(IContent target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IContent Entity => Target; +public sealed class ContentSentToPublishNotification : ObjectNotification +{ + public ContentSentToPublishNotification(IContent target, EventMessages messages) + : base(target, messages) + { } + + public IContent Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentSortedNotification.cs b/src/Umbraco.Core/Notifications/ContentSortedNotification.cs index 0a299e3c0a..8f0d6304ff 100644 --- a/src/Umbraco.Core/Notifications/ContentSortedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSortedNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSortedNotification : SortedNotification { - public sealed class ContentSortedNotification : SortedNotification + public ContentSortedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) { - public ContentSortedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentSortingNotification.cs b/src/Umbraco.Core/Notifications/ContentSortingNotification.cs index 1d6cd31c5a..bc3e94a464 100644 --- a/src/Umbraco.Core/Notifications/ContentSortingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentSortingNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentSortingNotification : SortingNotification { - public sealed class ContentSortingNotification : SortingNotification + public ContentSortingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) { - public ContentSortingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs b/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs index b5b100038b..df5aab16c7 100644 --- a/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs @@ -1,31 +1,35 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTreeChangeNotification : TreeChangeNotification { - public class ContentTreeChangeNotification : TreeChangeNotification + public ContentTreeChangeNotification(TreeChange target, EventMessages messages) + : base(target, messages) { - public ContentTreeChangeNotification(TreeChange target, EventMessages messages) : base(target, messages) - { - } + } - public ContentTreeChangeNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } + public ContentTreeChangeNotification(IEnumerable> target, EventMessages messages) + : base( + target, messages) + { + } - public ContentTreeChangeNotification(IEnumerable target, - TreeChangeTypes changeTypes, - EventMessages messages) : base(target.Select(x => new TreeChange(x, changeTypes)), messages) - { - } + public ContentTreeChangeNotification( + IEnumerable target, + TreeChangeTypes changeTypes, + EventMessages messages) + : base(target.Select(x => new TreeChange(x, changeTypes)), messages) + { + } - public ContentTreeChangeNotification(IContent target, - TreeChangeTypes changeTypes, - EventMessages messages) : base(new TreeChange(target, changeTypes), messages) - { - } + public ContentTreeChangeNotification( + IContent target, + TreeChangeTypes changeTypes, + EventMessages messages) + : base(new TreeChange(target, changeTypes), messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeCacheRefresherNotification.cs index 8bd06a4c46..d4ced3496d 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeCacheRefresherNotification.cs @@ -1,11 +1,13 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ContentTypeCacheRefresherNotification : CacheRefresherNotification { - public class ContentTypeCacheRefresherNotification : CacheRefresherNotification + public ContentTypeCacheRefresherNotification(object messageObject, MessageType messageType) + : base( + messageObject, + messageType) { - public ContentTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeChangeNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeChangeNotification.cs index e03f381318..606a6fb34e 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeChangeNotification.cs @@ -1,20 +1,24 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class ContentTypeChangeNotification : EnumerableObjectNotification> + where T : class, IContentTypeComposition { - public abstract class ContentTypeChangeNotification : EnumerableObjectNotification> where T : class, IContentTypeComposition + protected ContentTypeChangeNotification(ContentTypeChange target, EventMessages messages) + : base( + target, + messages) { - protected ContentTypeChangeNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } - - protected ContentTypeChangeNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> Changes => Target; } + + protected ContentTypeChangeNotification(IEnumerable> target, EventMessages messages) + : base( + target, messages) + { + } + + public IEnumerable> Changes => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentTypeChangedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeChangedNotification.cs index e0aca73cd2..0456ebc9cf 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeChangedNotification.cs @@ -1,18 +1,20 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeChangedNotification : ContentTypeChangeNotification - { - public ContentTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeChangedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeChangedNotification : ContentTypeChangeNotification +{ + public ContentTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentTypeChangedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeDeletedNotification.cs index d5b2b3e28e..92092d1a57 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeDeletedNotification.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeDeletedNotification : DeletedNotification - { - public ContentTypeDeletedNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeDeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeDeletedNotification : DeletedNotification +{ + public ContentTypeDeletedNotification(IContentType target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeDeletedNotification(IEnumerable target, EventMessages messages) + : base( + target, + messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeDeletingNotification.cs index 56863b93fb..0313ffcc17 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeDeletingNotification.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeDeletingNotification : DeletingNotification - { - public ContentTypeDeletingNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeDeletingNotification : DeletingNotification +{ + public ContentTypeDeletingNotification(IContentType target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeDeletingNotification(IEnumerable target, EventMessages messages) + : base( + target, + messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeMovedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeMovedNotification.cs index d4794329cf..4fab7a67ac 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeMovedNotification.cs @@ -1,17 +1,20 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeMovedNotification : MovedNotification - { - public ContentTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeMovedNotification : MovedNotification +{ + public ContentTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentTypeMovedNotification(IEnumerable> target, EventMessages messages) + : base( + target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeMovingNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeMovingNotification.cs index a888150097..210dcf43f2 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeMovingNotification.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeMovingNotification : MovingNotification - { - public ContentTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeMovingNotification : MovingNotification +{ + public ContentTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base( + target, + messages) + { + } + + public ContentTypeMovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeRefreshNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeRefreshNotification.cs index 717225db2d..108e72aecc 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeRefreshNotification.cs @@ -1,18 +1,22 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class ContentTypeRefreshNotification : ContentTypeChangeNotification where T: class, IContentTypeComposition - { - protected ContentTypeRefreshNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - protected ContentTypeRefreshNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public abstract class ContentTypeRefreshNotification : ContentTypeChangeNotification + where T : class, IContentTypeComposition +{ + protected ContentTypeRefreshNotification(ContentTypeChange target, EventMessages messages) + : base( + target, + messages) + { + } + + protected ContentTypeRefreshNotification(IEnumerable> target, EventMessages messages) + : base( + target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeRefreshedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeRefreshedNotification.cs index 72d111bb67..b49eef2876 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeRefreshedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeRefreshedNotification.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - [Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class ContentTypeRefreshedNotification : ContentTypeRefreshNotification - { - public ContentTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeRefreshedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +[Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class ContentTypeRefreshedNotification : ContentTypeRefreshNotification +{ + public ContentTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeRefreshedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeSavedNotification.cs index 5b9a231d60..f5c45c6323 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeSavedNotification : SavedNotification - { - public ContentTypeSavedNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeSavedNotification : SavedNotification +{ + public ContentTypeSavedNotification(IContentType target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/ContentTypeSavingNotification.cs index 85deb91418..5c1bc5d611 100644 --- a/src/Umbraco.Core/Notifications/ContentTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTypeSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ContentTypeSavingNotification : SavingNotification - { - public ContentTypeSavingNotification(IContentType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ContentTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ContentTypeSavingNotification : SavingNotification +{ + public ContentTypeSavingNotification(IContentType target, EventMessages messages) + : base(target, messages) + { + } + + public ContentTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ContentUnpublishedNotification.cs b/src/Umbraco.Core/Notifications/ContentUnpublishedNotification.cs index c08d79ac59..2677ef5a08 100644 --- a/src/Umbraco.Core/Notifications/ContentUnpublishedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentUnpublishedNotification.cs @@ -1,22 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentUnpublishedNotification : EnumerableObjectNotification { - public sealed class ContentUnpublishedNotification : EnumerableObjectNotification + public ContentUnpublishedNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentUnpublishedNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentUnpublishedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable UnpublishedEntities => Target; } + + public ContentUnpublishedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable UnpublishedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/ContentUnpublishingNotification.cs b/src/Umbraco.Core/Notifications/ContentUnpublishingNotification.cs index 5fb5034515..7fc0717c04 100644 --- a/src/Umbraco.Core/Notifications/ContentUnpublishingNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentUnpublishingNotification.cs @@ -1,22 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class ContentUnpublishingNotification : CancelableEnumerableObjectNotification { - public sealed class ContentUnpublishingNotification : CancelableEnumerableObjectNotification + public ContentUnpublishingNotification(IContent target, EventMessages messages) + : base(target, messages) { - public ContentUnpublishingNotification(IContent target, EventMessages messages) : base(target, messages) - { - } - - public ContentUnpublishingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable UnpublishedEntities => Target; } + + public ContentUnpublishingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable UnpublishedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/CopiedNotification.cs b/src/Umbraco.Core/Notifications/CopiedNotification.cs index c7d6c88bcd..13b9cf25ba 100644 --- a/src/Umbraco.Core/Notifications/CopiedNotification.cs +++ b/src/Umbraco.Core/Notifications/CopiedNotification.cs @@ -3,22 +3,24 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CopiedNotification : ObjectNotification + where T : class { - public abstract class CopiedNotification : ObjectNotification where T : class + protected CopiedNotification(T original, T copy, int parentId, bool relateToOriginal, EventMessages messages) + : base(original, messages) { - protected CopiedNotification(T original, T copy, int parentId, bool relateToOriginal, EventMessages messages) : base(original, messages) - { - Copy = copy; - ParentId = parentId; - RelateToOriginal = relateToOriginal; - } - - public T Original => Target; - - public T Copy { get; } - - public int ParentId { get; } - public bool RelateToOriginal { get; } + Copy = copy; + ParentId = parentId; + RelateToOriginal = relateToOriginal; } + + public T Original => Target; + + public T Copy { get; } + + public int ParentId { get; } + + public bool RelateToOriginal { get; } } diff --git a/src/Umbraco.Core/Notifications/CopyingNotification.cs b/src/Umbraco.Core/Notifications/CopyingNotification.cs index 99f46f8b43..0992f9708b 100644 --- a/src/Umbraco.Core/Notifications/CopyingNotification.cs +++ b/src/Umbraco.Core/Notifications/CopyingNotification.cs @@ -3,20 +3,21 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class CopyingNotification : CancelableObjectNotification + where T : class { - public abstract class CopyingNotification : CancelableObjectNotification where T : class + protected CopyingNotification(T original, T copy, int parentId, EventMessages messages) + : base(original, messages) { - protected CopyingNotification(T original, T copy, int parentId, EventMessages messages) : base(original, messages) - { - Copy = copy; - ParentId = parentId; - } - - public T Original => Target; - - public T Copy { get; } - - public int ParentId { get; } + Copy = copy; + ParentId = parentId; } + + public T Original => Target; + + public T Copy { get; } + + public int ParentId { get; } } diff --git a/src/Umbraco.Core/Notifications/CreatedNotification.cs b/src/Umbraco.Core/Notifications/CreatedNotification.cs index 2108b5fb5c..8667e4bdcc 100644 --- a/src/Umbraco.Core/Notifications/CreatedNotification.cs +++ b/src/Umbraco.Core/Notifications/CreatedNotification.cs @@ -3,14 +3,15 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class CreatedNotification : ObjectNotification where T : class - { - protected CreatedNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T CreatedEntity => Target; +public abstract class CreatedNotification : ObjectNotification + where T : class +{ + protected CreatedNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T CreatedEntity => Target; } diff --git a/src/Umbraco.Core/Notifications/CreatingNotification.cs b/src/Umbraco.Core/Notifications/CreatingNotification.cs index da4fbfe742..f76a3d8839 100644 --- a/src/Umbraco.Core/Notifications/CreatingNotification.cs +++ b/src/Umbraco.Core/Notifications/CreatingNotification.cs @@ -3,14 +3,15 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class CreatingNotification : CancelableObjectNotification where T : class - { - protected CreatingNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T CreatedEntity => Target; +public abstract class CreatingNotification : CancelableObjectNotification + where T : class +{ + protected CreatingNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T CreatedEntity => Target; } diff --git a/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs b/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs index aacca17afb..2ea921ceb6 100644 --- a/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs +++ b/src/Umbraco.Core/Notifications/CreatingRequestNotification.cs @@ -1,20 +1,17 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +/// +/// Used for notifying when an Umbraco request is being created +/// +public class CreatingRequestNotification : INotification { /// - /// Used for notifying when an Umbraco request is being created + /// Initializes a new instance of the class. /// - public class CreatingRequestNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public CreatingRequestNotification(Uri url) => Url = url; + public CreatingRequestNotification(Uri url) => Url = url; - /// - /// Gets or sets the URL for the request - /// - public Uri Url { get; set; } - } + /// + /// Gets or sets the URL for the request + /// + public Uri Url { get; set; } } diff --git a/src/Umbraco.Core/Notifications/DataTypeCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/DataTypeCacheRefresherNotification.cs index f59de3ebd0..5f8b34fb22 100644 --- a/src/Umbraco.Core/Notifications/DataTypeCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeCacheRefresherNotification : CacheRefresherNotification { - public class DataTypeCacheRefresherNotification : CacheRefresherNotification + public DataTypeCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public DataTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/DataTypeDeletedNotification.cs index 405af74c1c..839fa00230 100644 --- a/src/Umbraco.Core/Notifications/DataTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeDeletedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeDeletedNotification : DeletedNotification { - public class DataTypeDeletedNotification : DeletedNotification + public DataTypeDeletedNotification(IDataType target, EventMessages messages) + : base(target, messages) { - public DataTypeDeletedNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/DataTypeDeletingNotification.cs index ab997a0def..70035a5237 100644 --- a/src/Umbraco.Core/Notifications/DataTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeDeletingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeDeletingNotification : DeletingNotification { - public class DataTypeDeletingNotification : DeletingNotification + public DataTypeDeletingNotification(IDataType target, EventMessages messages) + : base(target, messages) { - public DataTypeDeletingNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeMovedNotification.cs b/src/Umbraco.Core/Notifications/DataTypeMovedNotification.cs index 150582547b..27065b8619 100644 --- a/src/Umbraco.Core/Notifications/DataTypeMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeMovedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeMovedNotification : MovedNotification { - public class DataTypeMovedNotification : MovedNotification + public DataTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public DataTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeMovingNotification.cs b/src/Umbraco.Core/Notifications/DataTypeMovingNotification.cs index ae8fb4be6e..1a54f14622 100644 --- a/src/Umbraco.Core/Notifications/DataTypeMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeMovingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DataTypeMovingNotification : MovingNotification { - public class DataTypeMovingNotification : MovingNotification + public DataTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) { - public DataTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DataTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/DataTypeSavedNotification.cs index 6c1a806069..ca23336ce1 100644 --- a/src/Umbraco.Core/Notifications/DataTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DataTypeSavedNotification : SavedNotification - { - public DataTypeSavedNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DataTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DataTypeSavedNotification : SavedNotification +{ + public DataTypeSavedNotification(IDataType target, EventMessages messages) + : base(target, messages) + { + } + + public DataTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DataTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/DataTypeSavingNotification.cs index 3538950b12..8099431da6 100644 --- a/src/Umbraco.Core/Notifications/DataTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/DataTypeSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DataTypeSavingNotification : SavingNotification - { - public DataTypeSavingNotification(IDataType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DataTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DataTypeSavingNotification : SavingNotification +{ + public DataTypeSavingNotification(IDataType target, EventMessages messages) + : base(target, messages) + { + } + + public DataTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DeletedNotification.cs b/src/Umbraco.Core/Notifications/DeletedNotification.cs index 3b2a370388..69af0581af 100644 --- a/src/Umbraco.Core/Notifications/DeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/DeletedNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletedNotification : EnumerableObjectNotification { - public abstract class DeletedNotification : EnumerableObjectNotification + protected DeletedNotification(T target, EventMessages messages) + : base(target, messages) { - protected DeletedNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected DeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable DeletedEntities => Target; } + + protected DeletedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable DeletedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/DeletedVersionsNotification.cs b/src/Umbraco.Core/Notifications/DeletedVersionsNotification.cs index 420323afaf..03b8e150b7 100644 --- a/src/Umbraco.Core/Notifications/DeletedVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/DeletedVersionsNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletedVersionsNotification : DeletedVersionsNotificationBase + where T : class { - public abstract class DeletedVersionsNotification : DeletedVersionsNotificationBase where T : class + protected DeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - protected DeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) - : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DeletedVersionsNotificationBase.cs b/src/Umbraco.Core/Notifications/DeletedVersionsNotificationBase.cs index 352eee8cae..a68593de80 100644 --- a/src/Umbraco.Core/Notifications/DeletedVersionsNotificationBase.cs +++ b/src/Umbraco.Core/Notifications/DeletedVersionsNotificationBase.cs @@ -1,30 +1,34 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletedVersionsNotificationBase : StatefulNotification + where T : class { - public abstract class DeletedVersionsNotificationBase : StatefulNotification where T : class + protected DeletedVersionsNotificationBase( + int id, + EventMessages messages, + int specificVersion = default, + bool deletePriorVersions = false, + DateTime dateToRetain = default) { - protected DeletedVersionsNotificationBase(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) - { - Id = id; - Messages = messages; - SpecificVersion = specificVersion; - DeletePriorVersions = deletePriorVersions; - DateToRetain = dateToRetain; - } - - public int Id { get; } - - public EventMessages Messages { get; } - - public int SpecificVersion { get; } - - public bool DeletePriorVersions { get; } - - public DateTime DateToRetain { get; } + Id = id; + Messages = messages; + SpecificVersion = specificVersion; + DeletePriorVersions = deletePriorVersions; + DateToRetain = dateToRetain; } + + public int Id { get; } + + public EventMessages Messages { get; } + + public int SpecificVersion { get; } + + public bool DeletePriorVersions { get; } + + public DateTime DateToRetain { get; } } diff --git a/src/Umbraco.Core/Notifications/DeletingNotification.cs b/src/Umbraco.Core/Notifications/DeletingNotification.cs index b4090a5b83..ab630468dd 100644 --- a/src/Umbraco.Core/Notifications/DeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/DeletingNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class DeletingNotification : CancelableEnumerableObjectNotification { - public abstract class DeletingNotification : CancelableEnumerableObjectNotification + protected DeletingNotification(T target, EventMessages messages) + : base(target, messages) { - protected DeletingNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected DeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable DeletedEntities => Target; } + + protected DeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable DeletedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/DeletingVersionsNotification.cs b/src/Umbraco.Core/Notifications/DeletingVersionsNotification.cs index ca8f1f2514..6b708da28b 100644 --- a/src/Umbraco.Core/Notifications/DeletingVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/DeletingVersionsNotification.cs @@ -1,18 +1,17 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class DeletingVersionsNotification : DeletedVersionsNotificationBase, ICancelableNotification where T : class - { - protected DeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) - : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } +namespace Umbraco.Cms.Core.Notifications; - public bool Cancel { get; set; } +public abstract class DeletingVersionsNotification : DeletedVersionsNotificationBase, ICancelableNotification + where T : class +{ + protected DeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) + { } + + public bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs index b36939800e..170e8e21be 100644 --- a/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DictionaryCacheRefresherNotification : CacheRefresherNotification { - public class DictionaryCacheRefresherNotification : CacheRefresherNotification + public DictionaryCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public DictionaryCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DictionaryItemDeletedNotification.cs b/src/Umbraco.Core/Notifications/DictionaryItemDeletedNotification.cs index c151e7ec60..c62f6d3f7d 100644 --- a/src/Umbraco.Core/Notifications/DictionaryItemDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryItemDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DictionaryItemDeletedNotification : DeletedNotification { - public class DictionaryItemDeletedNotification : DeletedNotification + public DictionaryItemDeletedNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) { - public DictionaryItemDeletedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DictionaryItemDeletingNotification.cs b/src/Umbraco.Core/Notifications/DictionaryItemDeletingNotification.cs index 5be95c478b..d882bb594f 100644 --- a/src/Umbraco.Core/Notifications/DictionaryItemDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryItemDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DictionaryItemDeletingNotification : DeletingNotification - { - public DictionaryItemDeletingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DictionaryItemDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DictionaryItemDeletingNotification : DeletingNotification +{ + public DictionaryItemDeletingNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) + { + } + + public DictionaryItemDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DictionaryItemSavedNotification.cs b/src/Umbraco.Core/Notifications/DictionaryItemSavedNotification.cs index dc5194b847..386871a28b 100644 --- a/src/Umbraco.Core/Notifications/DictionaryItemSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryItemSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DictionaryItemSavedNotification : SavedNotification - { - public DictionaryItemSavedNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DictionaryItemSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DictionaryItemSavedNotification : SavedNotification +{ + public DictionaryItemSavedNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) + { + } + + public DictionaryItemSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DictionaryItemSavingNotification.cs b/src/Umbraco.Core/Notifications/DictionaryItemSavingNotification.cs index 79fef15aef..517fc772a0 100644 --- a/src/Umbraco.Core/Notifications/DictionaryItemSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/DictionaryItemSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DictionaryItemSavingNotification : SavingNotification - { - public DictionaryItemSavingNotification(IDictionaryItem target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DictionaryItemSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DictionaryItemSavingNotification : SavingNotification +{ + public DictionaryItemSavingNotification(IDictionaryItem target, EventMessages messages) + : base(target, messages) + { + } + + public DictionaryItemSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DomainCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/DomainCacheRefresherNotification.cs index 326a7d2b64..86114b5003 100644 --- a/src/Umbraco.Core/Notifications/DomainCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DomainCacheRefresherNotification : CacheRefresherNotification { - public class DomainCacheRefresherNotification : CacheRefresherNotification + public DomainCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public DomainCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DomainDeletedNotification.cs b/src/Umbraco.Core/Notifications/DomainDeletedNotification.cs index b1b3a80ba1..c569afc7b4 100644 --- a/src/Umbraco.Core/Notifications/DomainDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class DomainDeletedNotification : DeletedNotification { - public class DomainDeletedNotification : DeletedNotification + public DomainDeletedNotification(IDomain target, EventMessages messages) + : base(target, messages) { - public DomainDeletedNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/DomainDeletingNotification.cs b/src/Umbraco.Core/Notifications/DomainDeletingNotification.cs index cd678d3689..afeb3fa67c 100644 --- a/src/Umbraco.Core/Notifications/DomainDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DomainDeletingNotification : DeletingNotification - { - public DomainDeletingNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DomainDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DomainDeletingNotification : DeletingNotification +{ + public DomainDeletingNotification(IDomain target, EventMessages messages) + : base(target, messages) + { + } + + public DomainDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DomainSavedNotification.cs b/src/Umbraco.Core/Notifications/DomainSavedNotification.cs index 61bd8ba3a4..75c93e15b7 100644 --- a/src/Umbraco.Core/Notifications/DomainSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DomainSavedNotification : SavedNotification - { - public DomainSavedNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DomainSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DomainSavedNotification : SavedNotification +{ + public DomainSavedNotification(IDomain target, EventMessages messages) + : base(target, messages) + { + } + + public DomainSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/DomainSavingNotification.cs b/src/Umbraco.Core/Notifications/DomainSavingNotification.cs index 32a2d71a73..673ed92c72 100644 --- a/src/Umbraco.Core/Notifications/DomainSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/DomainSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class DomainSavingNotification : SavingNotification - { - public DomainSavingNotification(IDomain target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public DomainSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class DomainSavingNotification : SavingNotification +{ + public DomainSavingNotification(IDomain target, EventMessages messages) + : base(target, messages) + { + } + + public DomainSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/EmptiedRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/EmptiedRecycleBinNotification.cs index 2773f3140f..8e648ac14d 100644 --- a/src/Umbraco.Core/Notifications/EmptiedRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/EmptiedRecycleBinNotification.cs @@ -1,21 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class EmptiedRecycleBinNotification : StatefulNotification + where T : class { - public abstract class EmptiedRecycleBinNotification : StatefulNotification where T : class + protected EmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) { - protected EmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) - { - DeletedEntities = deletedEntities; - Messages = messages; - } - - public IEnumerable DeletedEntities { get; } - - public EventMessages Messages { get; } + DeletedEntities = deletedEntities; + Messages = messages; } + + public IEnumerable DeletedEntities { get; } + + public EventMessages Messages { get; } } diff --git a/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs index 42005fc9f4..5701819415 100644 --- a/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/EmptyingRecycleBinNotification.cs @@ -1,23 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class EmptyingRecycleBinNotification : StatefulNotification, ICancelableNotification + where T : class { - public abstract class EmptyingRecycleBinNotification : StatefulNotification, ICancelableNotification where T : class + protected EmptyingRecycleBinNotification(IEnumerable? deletedEntities, EventMessages messages) { - protected EmptyingRecycleBinNotification(IEnumerable? deletedEntities, EventMessages messages) - { - DeletedEntities = deletedEntities; - Messages = messages; - } - - public IEnumerable? DeletedEntities { get; } - - public EventMessages Messages { get; } - - public bool Cancel { get; set; } + DeletedEntities = deletedEntities; + Messages = messages; } + + public IEnumerable? DeletedEntities { get; } + + public EventMessages Messages { get; } + + public bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs index 66c55e94ad..5074aa3893 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerDeletedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerDeletedNotification : DeletedNotification { - public class EntityContainerDeletedNotification : DeletedNotification + public EntityContainerDeletedNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerDeletedNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerDeletingNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerDeletingNotification.cs index 45a7a5b6c8..4d22d7715a 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerDeletingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerDeletingNotification : DeletingNotification { - public class EntityContainerDeletingNotification : DeletingNotification + public EntityContainerDeletingNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerDeletingNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerRenamedNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerRenamedNotification.cs index e6046c9a58..11e7100b91 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerRenamedNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerRenamedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerRenamedNotification : RenamedNotification { - public class EntityContainerRenamedNotification : RenamedNotification + public EntityContainerRenamedNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerRenamedNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerRenamingNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerRenamingNotification.cs index c03d5f5ee3..9e1b795d9f 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerRenamingNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerRenamingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerRenamingNotification : RenamingNotification { - public class EntityContainerRenamingNotification : RenamingNotification + public EntityContainerRenamingNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerRenamingNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerSavedNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerSavedNotification.cs index 33cac9effd..4fa3446834 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerSavedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerSavedNotification : SavedNotification { - public class EntityContainerSavedNotification : SavedNotification + public EntityContainerSavedNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerSavedNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityContainerSavingNotification.cs b/src/Umbraco.Core/Notifications/EntityContainerSavingNotification.cs index 25cbfc9311..6c5455e762 100644 --- a/src/Umbraco.Core/Notifications/EntityContainerSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityContainerSavingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class EntityContainerSavingNotification : SavingNotification { - public class EntityContainerSavingNotification : SavingNotification + public EntityContainerSavingNotification(EntityContainer target, EventMessages messages) + : base(target, messages) { - public EntityContainerSavingNotification(EntityContainer target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/EntityRefreshNotification.cs b/src/Umbraco.Core/Notifications/EntityRefreshNotification.cs index 1afc1fa078..4a5aaa4216 100644 --- a/src/Umbraco.Core/Notifications/EntityRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/EntityRefreshNotification.cs @@ -1,14 +1,15 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class EntityRefreshNotification : ObjectNotification where T : class, IContentBase - { - public EntityRefreshNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T Entity => Target; +public class EntityRefreshNotification : ObjectNotification + where T : class, IContentBase +{ + public EntityRefreshNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs b/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs index fde93f0139..3989e34b4b 100644 --- a/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs +++ b/src/Umbraco.Core/Notifications/EnumerableObjectNotification.cs @@ -1,19 +1,19 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class EnumerableObjectNotification : ObjectNotification> - { - protected EnumerableObjectNotification(T target, EventMessages messages) : base(new [] {target}, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - protected EnumerableObjectNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public abstract class EnumerableObjectNotification : ObjectNotification> +{ + protected EnumerableObjectNotification(T target, EventMessages messages) + : base(new[] { target }, messages) + { + } + + protected EnumerableObjectNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ExportedMemberNotification.cs b/src/Umbraco.Core/Notifications/ExportedMemberNotification.cs index 9cf69032e6..29c843945c 100644 --- a/src/Umbraco.Core/Notifications/ExportedMemberNotification.cs +++ b/src/Umbraco.Core/Notifications/ExportedMemberNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ExportedMemberNotification : INotification { - public class ExportedMemberNotification : INotification + public ExportedMemberNotification(IMember member, MemberExportModel exported) { - public ExportedMemberNotification(IMember member, MemberExportModel exported) - { - Member = member; - Exported = exported; - } - - public IMember Member { get; } - - public MemberExportModel Exported { get; } + Member = member; + Exported = exported; } + + public IMember Member { get; } + + public MemberExportModel Exported { get; } } diff --git a/src/Umbraco.Core/Notifications/ICancelableNotification.cs b/src/Umbraco.Core/Notifications/ICancelableNotification.cs index c30e6613fe..e4d1b61309 100644 --- a/src/Umbraco.Core/Notifications/ICancelableNotification.cs +++ b/src/Umbraco.Core/Notifications/ICancelableNotification.cs @@ -1,10 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public interface ICancelableNotification : INotification { - public interface ICancelableNotification : INotification - { - bool Cancel { get; set; } - } + bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/INotification.cs b/src/Umbraco.Core/Notifications/INotification.cs index 2427da1454..fc73fba39b 100644 --- a/src/Umbraco.Core/Notifications/INotification.cs +++ b/src/Umbraco.Core/Notifications/INotification.cs @@ -1,12 +1,11 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// A marker interface to represent a notification. +/// +public interface INotification { - /// - /// A marker interface to represent a notification. - /// - public interface INotification - { - } } diff --git a/src/Umbraco.Core/Notifications/IStatefulNotification.cs b/src/Umbraco.Core/Notifications/IStatefulNotification.cs index c7319524ff..65603f5bfa 100644 --- a/src/Umbraco.Core/Notifications/IStatefulNotification.cs +++ b/src/Umbraco.Core/Notifications/IStatefulNotification.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public interface IStatefulNotification : INotification { - public interface IStatefulNotification : INotification - { - IDictionary State { get; set; } - } + IDictionary State { get; set; } } diff --git a/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs index 4b0ea6826a..8d8ea73fe6 100644 --- a/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs +++ b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs @@ -1,17 +1,16 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Represents an Umbraco application lifetime (starting, started, stopping, stopped) notification. +/// +/// +public interface IUmbracoApplicationLifetimeNotification : INotification { /// - /// Represents an Umbraco application lifetime (starting, started, stopping, stopped) notification. + /// Gets a value indicating whether Umbraco is restarting (e.g. after an install or upgrade). /// - /// - public interface IUmbracoApplicationLifetimeNotification : INotification - { - /// - /// Gets a value indicating whether Umbraco is restarting (e.g. after an install or upgrade). - /// - /// - /// true if Umbraco is restarting; otherwise, false. - /// - bool IsRestarting { get; } - } + /// + /// true if Umbraco is restarting; otherwise, false. + /// + bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/ImportedPackageNotification.cs b/src/Umbraco.Core/Notifications/ImportedPackageNotification.cs index 8f3538d448..62114722c1 100644 --- a/src/Umbraco.Core/Notifications/ImportedPackageNotification.cs +++ b/src/Umbraco.Core/Notifications/ImportedPackageNotification.cs @@ -1,15 +1,11 @@ -using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; -namespace Umbraco.Cms.Core.Notifications -{ - public class ImportedPackageNotification : StatefulNotification - { - public ImportedPackageNotification(InstallationSummary installationSummary) - { - InstallationSummary = installationSummary; - } +namespace Umbraco.Cms.Core.Notifications; - public InstallationSummary InstallationSummary { get; } - } +public class ImportedPackageNotification : StatefulNotification +{ + public ImportedPackageNotification(InstallationSummary installationSummary) => + InstallationSummary = installationSummary; + + public InstallationSummary InstallationSummary { get; } } diff --git a/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs b/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs index 7fb6c8f9fc..67a02f254c 100644 --- a/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs +++ b/src/Umbraco.Core/Notifications/ImportingPackageNotification.cs @@ -1,16 +1,10 @@ -using Umbraco.Cms.Core.Models.Packaging; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public class ImportingPackageNotification : StatefulNotification, ICancelableNotification { - public class ImportingPackageNotification : StatefulNotification, ICancelableNotification - { - public ImportingPackageNotification(string packageName) - { - PackageName = packageName; - } + public ImportingPackageNotification(string packageName) => PackageName = packageName; - public string PackageName { get; } + public string PackageName { get; } - public bool Cancel { get; set; } - } + public bool Cancel { get; set; } } diff --git a/src/Umbraco.Core/Notifications/LanguageCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/LanguageCacheRefresherNotification.cs index 436d8a4fbf..8e62c68b1d 100644 --- a/src/Umbraco.Core/Notifications/LanguageCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class LanguageCacheRefresherNotification : CacheRefresherNotification { - public class LanguageCacheRefresherNotification : CacheRefresherNotification + public LanguageCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public LanguageCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/LanguageDeletedNotification.cs b/src/Umbraco.Core/Notifications/LanguageDeletedNotification.cs index ccc17c8a90..9f435775aa 100644 --- a/src/Umbraco.Core/Notifications/LanguageDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class LanguageDeletedNotification : DeletedNotification { - public class LanguageDeletedNotification : DeletedNotification + public LanguageDeletedNotification(ILanguage target, EventMessages messages) + : base(target, messages) { - public LanguageDeletedNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/LanguageDeletingNotification.cs b/src/Umbraco.Core/Notifications/LanguageDeletingNotification.cs index c4e4682500..1fdff6538f 100644 --- a/src/Umbraco.Core/Notifications/LanguageDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class LanguageDeletingNotification : DeletingNotification - { - public LanguageDeletingNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public LanguageDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class LanguageDeletingNotification : DeletingNotification +{ + public LanguageDeletingNotification(ILanguage target, EventMessages messages) + : base(target, messages) + { + } + + public LanguageDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/LanguageSavedNotification.cs b/src/Umbraco.Core/Notifications/LanguageSavedNotification.cs index 29265c86ca..b3e58e9b83 100644 --- a/src/Umbraco.Core/Notifications/LanguageSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class LanguageSavedNotification : SavedNotification - { - public LanguageSavedNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public LanguageSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class LanguageSavedNotification : SavedNotification +{ + public LanguageSavedNotification(ILanguage target, EventMessages messages) + : base(target, messages) + { + } + + public LanguageSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/LanguageSavingNotification.cs b/src/Umbraco.Core/Notifications/LanguageSavingNotification.cs index 5fcb892e25..adbba95ad4 100644 --- a/src/Umbraco.Core/Notifications/LanguageSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/LanguageSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class LanguageSavingNotification : SavingNotification - { - public LanguageSavingNotification(ILanguage target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public LanguageSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class LanguageSavingNotification : SavingNotification +{ + public LanguageSavingNotification(ILanguage target, EventMessages messages) + : base(target, messages) + { + } + + public LanguageSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MacroCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/MacroCacheRefresherNotification.cs index 5fb5554b1b..4d88155074 100644 --- a/src/Umbraco.Core/Notifications/MacroCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MacroCacheRefresherNotification : CacheRefresherNotification { - public class MacroCacheRefresherNotification : CacheRefresherNotification + public MacroCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public MacroCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MacroDeletedNotification.cs b/src/Umbraco.Core/Notifications/MacroDeletedNotification.cs index 237cce38fe..b42779415a 100644 --- a/src/Umbraco.Core/Notifications/MacroDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroDeletedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MacroDeletedNotification : DeletedNotification { - public class MacroDeletedNotification : DeletedNotification + public MacroDeletedNotification(IMacro target, EventMessages messages) + : base(target, messages) { - public MacroDeletedNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MacroDeletingNotification.cs b/src/Umbraco.Core/Notifications/MacroDeletingNotification.cs index d36a9896bc..8d262cb8aa 100644 --- a/src/Umbraco.Core/Notifications/MacroDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroDeletingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MacroDeletingNotification : DeletingNotification - { - public MacroDeletingNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MacroDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MacroDeletingNotification : DeletingNotification +{ + public MacroDeletingNotification(IMacro target, EventMessages messages) + : base(target, messages) + { + } + + public MacroDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MacroSavedNotification.cs b/src/Umbraco.Core/Notifications/MacroSavedNotification.cs index 8aa776dcc6..145ac6eb3d 100644 --- a/src/Umbraco.Core/Notifications/MacroSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MacroSavedNotification : SavedNotification - { - public MacroSavedNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MacroSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MacroSavedNotification : SavedNotification +{ + public MacroSavedNotification(IMacro target, EventMessages messages) + : base(target, messages) + { + } + + public MacroSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MacroSavingNotification.cs b/src/Umbraco.Core/Notifications/MacroSavingNotification.cs index 965ee6b22e..5786b76d81 100644 --- a/src/Umbraco.Core/Notifications/MacroSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MacroSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MacroSavingNotification : SavingNotification - { - public MacroSavingNotification(IMacro target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MacroSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MacroSavingNotification : SavingNotification +{ + public MacroSavingNotification(IMacro target, EventMessages messages) + : base(target, messages) + { + } + + public MacroSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/MediaCacheRefresherNotification.cs index 079475232d..9277e20423 100644 --- a/src/Umbraco.Core/Notifications/MediaCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MediaCacheRefresherNotification : CacheRefresherNotification { - public class MediaCacheRefresherNotification : CacheRefresherNotification + public MediaCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public MediaCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaDeletedNotification.cs b/src/Umbraco.Core/Notifications/MediaDeletedNotification.cs index b8cce7e747..643f907ab8 100644 --- a/src/Umbraco.Core/Notifications/MediaDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaDeletedNotification : DeletedNotification { - public sealed class MediaDeletedNotification : DeletedNotification + public MediaDeletedNotification(IMedia target, EventMessages messages) + : base(target, messages) { - public MediaDeletedNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaDeletedVersionsNotification.cs b/src/Umbraco.Core/Notifications/MediaDeletedVersionsNotification.cs index 6bbdb3c098..b8520e5274 100644 --- a/src/Umbraco.Core/Notifications/MediaDeletedVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaDeletedVersionsNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaDeletedVersionsNotification : DeletedVersionsNotification { - public sealed class MediaDeletedVersionsNotification : DeletedVersionsNotification + public MediaDeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - public MediaDeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaDeletingNotification.cs b/src/Umbraco.Core/Notifications/MediaDeletingNotification.cs index 358a553b28..8973b9861f 100644 --- a/src/Umbraco.Core/Notifications/MediaDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaDeletingNotification : DeletingNotification - { - public MediaDeletingNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaDeletingNotification : DeletingNotification +{ + public MediaDeletingNotification(IMedia target, EventMessages messages) + : base(target, messages) + { + } + + public MediaDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaDeletingVersionsNotification.cs b/src/Umbraco.Core/Notifications/MediaDeletingVersionsNotification.cs index fa7b3ba8e0..0d7ff01ca3 100644 --- a/src/Umbraco.Core/Notifications/MediaDeletingVersionsNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaDeletingVersionsNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaDeletingVersionsNotification : DeletingVersionsNotification { - public sealed class MediaDeletingVersionsNotification : DeletingVersionsNotification + public MediaDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) + : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) { - public MediaDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaEmptiedRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MediaEmptiedRecycleBinNotification.cs index 0862296925..3aea97d608 100644 --- a/src/Umbraco.Core/Notifications/MediaEmptiedRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaEmptiedRecycleBinNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaEmptiedRecycleBinNotification : EmptiedRecycleBinNotification { - public sealed class MediaEmptiedRecycleBinNotification : EmptiedRecycleBinNotification + public MediaEmptiedRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) + : base(deletedEntities, messages) { - public MediaEmptiedRecycleBinNotification(IEnumerable deletedEntities,EventMessages messages) : base(deletedEntities, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaEmptyingRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MediaEmptyingRecycleBinNotification.cs index 4e257cfb38..432d480847 100644 --- a/src/Umbraco.Core/Notifications/MediaEmptyingRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaEmptyingRecycleBinNotification.cs @@ -1,16 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MediaEmptyingRecycleBinNotification : EmptyingRecycleBinNotification { - public sealed class MediaEmptyingRecycleBinNotification : EmptyingRecycleBinNotification + public MediaEmptyingRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) + : base(deletedEntities, messages) { - public MediaEmptyingRecycleBinNotification(IEnumerable deletedEntities, EventMessages messages) : base(deletedEntities, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaMovedNotification.cs b/src/Umbraco.Core/Notifications/MediaMovedNotification.cs index 2012f16f4b..d7cf614ed9 100644 --- a/src/Umbraco.Core/Notifications/MediaMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaMovedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaMovedNotification : MovedNotification - { - public MediaMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaMovedNotification : MovedNotification +{ + public MediaMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaMovedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaMovedToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MediaMovedToRecycleBinNotification.cs index 44120674bd..78d771847b 100644 --- a/src/Umbraco.Core/Notifications/MediaMovedToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaMovedToRecycleBinNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaMovedToRecycleBinNotification : MovedToRecycleBinNotification - { - public MediaMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaMovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaMovedToRecycleBinNotification : MovedToRecycleBinNotification +{ + public MediaMovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaMovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaMovingNotification.cs b/src/Umbraco.Core/Notifications/MediaMovingNotification.cs index fcfb50787b..c1f5a7ab94 100644 --- a/src/Umbraco.Core/Notifications/MediaMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaMovingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaMovingNotification : MovingNotification - { - public MediaMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaMovingNotification : MovingNotification +{ + public MediaMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaMovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaMovingToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MediaMovingToRecycleBinNotification.cs index 856b66c0c4..ee5618f9fb 100644 --- a/src/Umbraco.Core/Notifications/MediaMovingToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaMovingToRecycleBinNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaMovingToRecycleBinNotification : MovingToRecycleBinNotification - { - public MediaMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaMovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaMovingToRecycleBinNotification : MovingToRecycleBinNotification +{ + public MediaMovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaMovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaRefreshNotification.cs b/src/Umbraco.Core/Notifications/MediaRefreshNotification.cs index 1c8b8b9bea..bd4cb3efda 100644 --- a/src/Umbraco.Core/Notifications/MediaRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaRefreshNotification.cs @@ -1,16 +1,15 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class MediaRefreshNotification : EntityRefreshNotification { - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class MediaRefreshNotification : EntityRefreshNotification + public MediaRefreshNotification(IMedia target, EventMessages messages) + : base(target, messages) { - public MediaRefreshNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MediaSavedNotification.cs b/src/Umbraco.Core/Notifications/MediaSavedNotification.cs index addeda617e..bf9f507521 100644 --- a/src/Umbraco.Core/Notifications/MediaSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaSavedNotification : SavedNotification - { - public MediaSavedNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaSavedNotification : SavedNotification +{ + public MediaSavedNotification(IMedia target, EventMessages messages) + : base(target, messages) + { + } + + public MediaSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaSavingNotification.cs b/src/Umbraco.Core/Notifications/MediaSavingNotification.cs index 638d27c968..d902de6ba7 100644 --- a/src/Umbraco.Core/Notifications/MediaSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MediaSavingNotification : SavingNotification - { - public MediaSavingNotification(IMedia target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MediaSavingNotification : SavingNotification +{ + public MediaSavingNotification(IMedia target, EventMessages messages) + : base(target, messages) + { + } + + public MediaSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTreeChangeNotification.cs b/src/Umbraco.Core/Notifications/MediaTreeChangeNotification.cs index 00e0e6b42c..cd896cd1fc 100644 --- a/src/Umbraco.Core/Notifications/MediaTreeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTreeChangeNotification.cs @@ -1,30 +1,31 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MediaTreeChangeNotification : TreeChangeNotification { - public class MediaTreeChangeNotification : TreeChangeNotification + public MediaTreeChangeNotification(TreeChange target, EventMessages messages) + : base(target, messages) { - public MediaTreeChangeNotification(TreeChange target, EventMessages messages) : base(target, messages) - { - } + } - public MediaTreeChangeNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } + public MediaTreeChangeNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } - public MediaTreeChangeNotification(IEnumerable target, - TreeChangeTypes changeTypes, - EventMessages messages) : base(target.Select(x => new TreeChange(x, changeTypes)), messages) - { - } + public MediaTreeChangeNotification( + IEnumerable target, + TreeChangeTypes changeTypes, + EventMessages messages) + : base(target.Select(x => new TreeChange(x, changeTypes)), messages) + { + } - public MediaTreeChangeNotification(IMedia target, TreeChangeTypes changeTypes, EventMessages messages) : base( - new TreeChange(target, changeTypes), messages) - { - } + public MediaTreeChangeNotification(IMedia target, TreeChangeTypes changeTypes, EventMessages messages) + : base(new TreeChange(target, changeTypes), messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeChangedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeChangedNotification.cs index 322a6bb1ab..1882c7cc74 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeChangedNotification.cs @@ -1,18 +1,18 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeChangedNotification : ContentTypeChangeNotification - { - public MediaTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeChangedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeChangedNotification : ContentTypeChangeNotification +{ + public MediaTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeChangedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeDeletedNotification.cs index 59c7114ca0..8ad8e1bce5 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeDeletedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeDeletedNotification : DeletedNotification - { - public MediaTypeDeletedNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeDeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeDeletedNotification : DeletedNotification +{ + public MediaTypeDeletedNotification(IMediaType target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeDeletedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeDeletingNotification.cs index 1cb4f7c99d..a819ef0d8c 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeDeletingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeDeletingNotification : DeletingNotification - { - public MediaTypeDeletingNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeDeletingNotification : DeletingNotification +{ + public MediaTypeDeletingNotification(IMediaType target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeMovedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeMovedNotification.cs index c17aa222de..f05d5fd37b 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeMovedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeMovedNotification : MovedNotification - { - public MediaTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeMovedNotification : MovedNotification +{ + public MediaTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeMovedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeMovingNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeMovingNotification.cs index 43499430b0..9b7ac27c13 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeMovingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeMovingNotification : MovingNotification - { - public MediaTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeMovingNotification : MovingNotification +{ + public MediaTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeMovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeRefreshedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeRefreshedNotification.cs index 6b59e3220e..5b6814fdb1 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeRefreshedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeRefreshedNotification.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class MediaTypeRefreshedNotification : ContentTypeRefreshNotification - { - public MediaTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeRefreshedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class MediaTypeRefreshedNotification : ContentTypeRefreshNotification +{ + public MediaTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeRefreshedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeSavedNotification.cs index b4b2372b7f..17063f5252 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeSavedNotification : SavedNotification - { - public MediaTypeSavedNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeSavedNotification : SavedNotification +{ + public MediaTypeSavedNotification(IMediaType target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MediaTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/MediaTypeSavingNotification.cs index 0a93f08671..46bc588b39 100644 --- a/src/Umbraco.Core/Notifications/MediaTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MediaTypeSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MediaTypeSavingNotification : SavingNotification - { - public MediaTypeSavingNotification(IMediaType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MediaTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MediaTypeSavingNotification : SavingNotification +{ + public MediaTypeSavingNotification(IMediaType target, EventMessages messages) + : base(target, messages) + { + } + + public MediaTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/MemberCacheRefresherNotification.cs index c2d920843d..46101878aa 100644 --- a/src/Umbraco.Core/Notifications/MemberCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MemberCacheRefresherNotification : CacheRefresherNotification { - public class MemberCacheRefresherNotification : CacheRefresherNotification + public MemberCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public MemberCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberDeletedNotification.cs b/src/Umbraco.Core/Notifications/MemberDeletedNotification.cs index 7539d6b133..b1578fd998 100644 --- a/src/Umbraco.Core/Notifications/MemberDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class MemberDeletedNotification : DeletedNotification { - public sealed class MemberDeletedNotification : DeletedNotification + public MemberDeletedNotification(IMember target, EventMessages messages) + : base(target, messages) { - public MemberDeletedNotification(IMember target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberDeletingNotification.cs b/src/Umbraco.Core/Notifications/MemberDeletingNotification.cs index 9d09d40e15..df599d7b08 100644 --- a/src/Umbraco.Core/Notifications/MemberDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MemberDeletingNotification : DeletingNotification - { - public MemberDeletingNotification(IMember target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MemberDeletingNotification : DeletingNotification +{ + public MemberDeletingNotification(IMember target, EventMessages messages) + : base(target, messages) + { + } + + public MemberDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupCacheRefresherNotification.cs index f882b61167..333a8fbb55 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MemberGroupCacheRefresherNotification : CacheRefresherNotification { - public class MemberGroupCacheRefresherNotification : CacheRefresherNotification + public MemberGroupCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public MemberGroupCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupDeletedNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupDeletedNotification.cs index 8665cc5f71..528dc37254 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupDeletedNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class MemberGroupDeletedNotification : DeletedNotification { - public class MemberGroupDeletedNotification : DeletedNotification + public MemberGroupDeletedNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) { - public MemberGroupDeletedNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupDeletingNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupDeletingNotification.cs index 2b0f94af64..f0ed3dc49c 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupDeletingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberGroupDeletingNotification : DeletingNotification - { - public MemberGroupDeletingNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberGroupDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberGroupDeletingNotification : DeletingNotification +{ + public MemberGroupDeletingNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) + { + } + + public MemberGroupDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupSavedNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupSavedNotification.cs index e5beffe76b..9f8671d923 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberGroupSavedNotification : SavedNotification - { - public MemberGroupSavedNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberGroupSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberGroupSavedNotification : SavedNotification +{ + public MemberGroupSavedNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) + { + } + + public MemberGroupSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberGroupSavingNotification.cs b/src/Umbraco.Core/Notifications/MemberGroupSavingNotification.cs index a0341ab2ef..233714c542 100644 --- a/src/Umbraco.Core/Notifications/MemberGroupSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberGroupSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberGroupSavingNotification : SavingNotification - { - public MemberGroupSavingNotification(IMemberGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberGroupSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberGroupSavingNotification : SavingNotification +{ + public MemberGroupSavingNotification(IMemberGroup target, EventMessages messages) + : base(target, messages) + { + } + + public MemberGroupSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberRefreshNotification.cs b/src/Umbraco.Core/Notifications/MemberRefreshNotification.cs index a22c48348f..ddab089c0b 100644 --- a/src/Umbraco.Core/Notifications/MemberRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberRefreshNotification.cs @@ -1,16 +1,15 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class MemberRefreshNotification : EntityRefreshNotification { - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class MemberRefreshNotification : EntityRefreshNotification + public MemberRefreshNotification(IMember target, EventMessages messages) + : base(target, messages) { - public MemberRefreshNotification(IMember target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/MemberRolesNotification.cs b/src/Umbraco.Core/Notifications/MemberRolesNotification.cs index 9ea6548833..446faee237 100644 --- a/src/Umbraco.Core/Notifications/MemberRolesNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberRolesNotification.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MemberRolesNotification : INotification { - public abstract class MemberRolesNotification : INotification + protected MemberRolesNotification(int[] memberIds, string[] roles) { - protected MemberRolesNotification(int[] memberIds, string[] roles) - { - MemberIds = memberIds; - Roles = roles; - } - - public int[] MemberIds { get; } - - public string[] Roles { get; } + MemberIds = memberIds; + Roles = roles; } + + public int[] MemberIds { get; } + + public string[] Roles { get; } } diff --git a/src/Umbraco.Core/Notifications/MemberSavedNotification.cs b/src/Umbraco.Core/Notifications/MemberSavedNotification.cs index 2c4f4755eb..f59f41f0ec 100644 --- a/src/Umbraco.Core/Notifications/MemberSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MemberSavedNotification : SavedNotification - { - public MemberSavedNotification(IMember target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MemberSavedNotification : SavedNotification +{ + public MemberSavedNotification(IMember target, EventMessages messages) + : base(target, messages) + { + } + + public MemberSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberSavingNotification.cs b/src/Umbraco.Core/Notifications/MemberSavingNotification.cs index fc8198c6f9..813e6f7269 100644 --- a/src/Umbraco.Core/Notifications/MemberSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class MemberSavingNotification : SavingNotification - { - public MemberSavingNotification(IMember target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class MemberSavingNotification : SavingNotification +{ + public MemberSavingNotification(IMember target, EventMessages messages) + : base(target, messages) + { + } + + public MemberSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs b/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs index e06de2624e..fc9e392598 100644 --- a/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs @@ -1,14 +1,8 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public class MemberTwoFactorRequestedNotification : INotification { - public class MemberTwoFactorRequestedNotification : INotification - { - public MemberTwoFactorRequestedNotification(Guid? memberKey) - { - MemberKey = memberKey; - } + public MemberTwoFactorRequestedNotification(Guid? memberKey) => MemberKey = memberKey; - public Guid? MemberKey { get; } - } + public Guid? MemberKey { get; } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeChangedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeChangedNotification.cs index c22908c108..cbce239394 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeChangedNotification.cs @@ -1,18 +1,18 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeChangedNotification : ContentTypeChangeNotification - { - public MemberTypeChangedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeChangedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeChangedNotification : ContentTypeChangeNotification +{ + public MemberTypeChangedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeChangedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeDeletedNotification.cs index 490db24cf3..b3061cc074 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeDeletedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeDeletedNotification : DeletedNotification - { - public MemberTypeDeletedNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeDeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeDeletedNotification : DeletedNotification +{ + public MemberTypeDeletedNotification(IMemberType target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeDeletedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeDeletingNotification.cs index 04821eb0c2..d80fcd1c16 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeDeletingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeDeletingNotification : DeletingNotification - { - public MemberTypeDeletingNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeDeletingNotification : DeletingNotification +{ + public MemberTypeDeletingNotification(IMemberType target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeMovedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeMovedNotification.cs index 8e74076119..5ab6056124 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeMovedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeMovedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeMovedNotification : MovedNotification - { - public MemberTypeMovedNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeMovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeMovedNotification : MovedNotification +{ + public MemberTypeMovedNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeMovedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeMovingNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeMovingNotification.cs index b4627aaf30..9b4445c171 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeMovingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeMovingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeMovingNotification : MovingNotification - { - public MemberTypeMovingNotification(MoveEventInfo target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeMovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeMovingNotification : MovingNotification +{ + public MemberTypeMovingNotification(MoveEventInfo target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeMovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeRefreshedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeRefreshedNotification.cs index 89147a523f..050c24a9e7 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeRefreshedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeRefreshedNotification.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications -{ - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class MemberTypeRefreshedNotification : ContentTypeRefreshNotification - { - public MemberTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeRefreshedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class MemberTypeRefreshedNotification : ContentTypeRefreshNotification +{ + public MemberTypeRefreshedNotification(ContentTypeChange target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeRefreshedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeSavedNotification.cs index 768f9e8bb0..3101c794e2 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeSavedNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeSavedNotification : SavedNotification - { - public MemberTypeSavedNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeSavedNotification : SavedNotification +{ + public MemberTypeSavedNotification(IMemberType target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/MemberTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/MemberTypeSavingNotification.cs index 598aadffa4..7cfcb12b91 100644 --- a/src/Umbraco.Core/Notifications/MemberTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/MemberTypeSavingNotification.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class MemberTypeSavingNotification : SavingNotification - { - public MemberTypeSavingNotification(IMemberType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public MemberTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class MemberTypeSavingNotification : SavingNotification +{ + public MemberTypeSavingNotification(IMemberType target, EventMessages messages) + : base(target, messages) + { + } + + public MemberTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ModelBindingErrorNotification.cs b/src/Umbraco.Core/Notifications/ModelBindingErrorNotification.cs index e4adadcd52..0048699e09 100644 --- a/src/Umbraco.Core/Notifications/ModelBindingErrorNotification.cs +++ b/src/Umbraco.Core/Notifications/ModelBindingErrorNotification.cs @@ -1,37 +1,35 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Contains event data for the event. +/// +public class ModelBindingErrorNotification : INotification { /// - /// Contains event data for the event. + /// Initializes a new instance of the class. /// - public class ModelBindingErrorNotification : INotification + public ModelBindingErrorNotification(Type sourceType, Type modelType, StringBuilder message) { - /// - /// Initializes a new instance of the class. - /// - public ModelBindingErrorNotification(Type sourceType, Type modelType, StringBuilder message) - { - SourceType = sourceType; - ModelType = modelType; - Message = message; - } - - /// - /// Gets the type of the source object. - /// - public Type SourceType { get; } - - /// - /// Gets the type of the view model. - /// - public Type ModelType { get; } - - /// - /// Gets the message string builder. - /// - /// Handlers of the event can append text to the message. - public StringBuilder Message { get; } + SourceType = sourceType; + ModelType = modelType; + Message = message; } + + /// + /// Gets the type of the source object. + /// + public Type SourceType { get; } + + /// + /// Gets the type of the view model. + /// + public Type ModelType { get; } + + /// + /// Gets the message string builder. + /// + /// Handlers of the event can append text to the message. + public StringBuilder Message { get; } } diff --git a/src/Umbraco.Core/Notifications/MovedNotification.cs b/src/Umbraco.Core/Notifications/MovedNotification.cs index 4573d5e45a..f67273a6d4 100644 --- a/src/Umbraco.Core/Notifications/MovedNotification.cs +++ b/src/Umbraco.Core/Notifications/MovedNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MovedNotification : ObjectNotification>> { - public abstract class MovedNotification : ObjectNotification>> + protected MovedNotification(MoveEventInfo target, EventMessages messages) + : base(new[] { target }, messages) { - protected MovedNotification(MoveEventInfo target, EventMessages messages) : base(new[] { target }, messages) - { - } - - protected MovedNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> MoveInfoCollection => Target; } + + protected MovedNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> MoveInfoCollection => Target; } diff --git a/src/Umbraco.Core/Notifications/MovedToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MovedToRecycleBinNotification.cs index 1e02d30eb7..fddb0ab106 100644 --- a/src/Umbraco.Core/Notifications/MovedToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MovedToRecycleBinNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MovedToRecycleBinNotification : ObjectNotification>> { - public abstract class MovedToRecycleBinNotification : ObjectNotification>> + protected MovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(new[] { target }, messages) { - protected MovedToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(new[] { target }, messages) - { - } - - protected MovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> MoveInfoCollection => Target; } + + protected MovedToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> MoveInfoCollection => Target; } diff --git a/src/Umbraco.Core/Notifications/MovingNotification.cs b/src/Umbraco.Core/Notifications/MovingNotification.cs index 6bf493fc1b..47a2ecf7bf 100644 --- a/src/Umbraco.Core/Notifications/MovingNotification.cs +++ b/src/Umbraco.Core/Notifications/MovingNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MovingNotification : CancelableObjectNotification>> { - public abstract class MovingNotification : CancelableObjectNotification>> + protected MovingNotification(MoveEventInfo target, EventMessages messages) + : base(new[] { target }, messages) { - protected MovingNotification(MoveEventInfo target, EventMessages messages) : base(new[] {target}, messages) - { - } - - protected MovingNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> MoveInfoCollection => Target; } + + protected MovingNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> MoveInfoCollection => Target; } diff --git a/src/Umbraco.Core/Notifications/MovingToRecycleBinNotification.cs b/src/Umbraco.Core/Notifications/MovingToRecycleBinNotification.cs index ef8c36ce6f..37e486e3ff 100644 --- a/src/Umbraco.Core/Notifications/MovingToRecycleBinNotification.cs +++ b/src/Umbraco.Core/Notifications/MovingToRecycleBinNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class MovingToRecycleBinNotification : CancelableObjectNotification>> { - public abstract class MovingToRecycleBinNotification : CancelableObjectNotification>> + protected MovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) + : base(new[] { target }, messages) { - protected MovingToRecycleBinNotification(MoveEventInfo target, EventMessages messages) : base(new[] { target }, messages) - { - } - - protected MovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> MoveInfoCollection => Target; } + + protected MovingToRecycleBinNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> MoveInfoCollection => Target; } diff --git a/src/Umbraco.Core/Notifications/NotificationExtensions.cs b/src/Umbraco.Core/Notifications/NotificationExtensions.cs index d907d3dcfa..540cf0840a 100644 --- a/src/Umbraco.Core/Notifications/NotificationExtensions.cs +++ b/src/Umbraco.Core/Notifications/NotificationExtensions.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public static class NotificationExtensions { - public static class NotificationExtensions + public static T WithState(this T notification, IDictionary? state) + where T : IStatefulNotification { - public static T WithState(this T notification, IDictionary? state) where T : IStatefulNotification - { - notification.State = state!; - return notification; - } - - public static T WithStateFrom(this T notification, TSource source) - where T : IStatefulNotification where TSource : IStatefulNotification - => notification.WithState(source.State); + notification.State = state!; + return notification; } + + public static T WithStateFrom(this T notification, TSource source) + where T : IStatefulNotification + where TSource : IStatefulNotification + => notification.WithState(source.State); } diff --git a/src/Umbraco.Core/Notifications/ObjectNotification.cs b/src/Umbraco.Core/Notifications/ObjectNotification.cs index a550754d32..e7c60c5bbc 100644 --- a/src/Umbraco.Core/Notifications/ObjectNotification.cs +++ b/src/Umbraco.Core/Notifications/ObjectNotification.cs @@ -3,18 +3,18 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class ObjectNotification : StatefulNotification + where T : class { - public abstract class ObjectNotification : StatefulNotification where T : class + protected ObjectNotification(T target, EventMessages messages) { - protected ObjectNotification(T target, EventMessages messages) - { - Messages = messages; - Target = target; - } - - public EventMessages Messages { get; } - - protected T Target { get; } + Messages = messages; + Target = target; } + + public EventMessages Messages { get; } + + protected T Target { get; } } diff --git a/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs b/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs index 3f34c4b1c6..3fe571843d 100644 --- a/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewCreatedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewCreatedNotification : CreatedNotification { - public class PartialViewCreatedNotification : CreatedNotification + public PartialViewCreatedNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewCreatedNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PartialViewCreatingNotification.cs b/src/Umbraco.Core/Notifications/PartialViewCreatingNotification.cs index 425879fb06..d53b4eb1c8 100644 --- a/src/Umbraco.Core/Notifications/PartialViewCreatingNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewCreatingNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewCreatingNotification : CreatingNotification { - public class PartialViewCreatingNotification : CreatingNotification + public PartialViewCreatingNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewCreatingNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PartialViewDeletedNotification.cs b/src/Umbraco.Core/Notifications/PartialViewDeletedNotification.cs index 4ef4058b5c..29e1548bf3 100644 --- a/src/Umbraco.Core/Notifications/PartialViewDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class PartialViewDeletedNotification : DeletedNotification { - public class PartialViewDeletedNotification : DeletedNotification + public PartialViewDeletedNotification(IPartialView target, EventMessages messages) + : base(target, messages) { - public PartialViewDeletedNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PartialViewDeletingNotification.cs b/src/Umbraco.Core/Notifications/PartialViewDeletingNotification.cs index 6473713408..26a6fa86e0 100644 --- a/src/Umbraco.Core/Notifications/PartialViewDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class PartialViewDeletingNotification : DeletingNotification - { - public PartialViewDeletingNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PartialViewDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class PartialViewDeletingNotification : DeletingNotification +{ + public PartialViewDeletingNotification(IPartialView target, EventMessages messages) + : base(target, messages) + { + } + + public PartialViewDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PartialViewSavedNotification.cs b/src/Umbraco.Core/Notifications/PartialViewSavedNotification.cs index d50ed08faf..e7d0702e02 100644 --- a/src/Umbraco.Core/Notifications/PartialViewSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class PartialViewSavedNotification : SavedNotification - { - public PartialViewSavedNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PartialViewSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class PartialViewSavedNotification : SavedNotification +{ + public PartialViewSavedNotification(IPartialView target, EventMessages messages) + : base(target, messages) + { + } + + public PartialViewSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PartialViewSavingNotification.cs b/src/Umbraco.Core/Notifications/PartialViewSavingNotification.cs index fd2e0ee34a..ee7401c772 100644 --- a/src/Umbraco.Core/Notifications/PartialViewSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/PartialViewSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class PartialViewSavingNotification : SavingNotification - { - public PartialViewSavingNotification(IPartialView target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PartialViewSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class PartialViewSavingNotification : SavingNotification +{ + public PartialViewSavingNotification(IPartialView target, EventMessages messages) + : base(target, messages) + { + } + + public PartialViewSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessCacheRefresherNotification.cs index 1e753217ab..223cf16cc3 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class PublicAccessCacheRefresherNotification : CacheRefresherNotification { - public class PublicAccessCacheRefresherNotification : CacheRefresherNotification + public PublicAccessCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public PublicAccessCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessEntryDeletedNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessEntryDeletedNotification.cs index f6aa16500a..a90601cf50 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessEntryDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessEntryDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class PublicAccessEntryDeletedNotification : DeletedNotification { - public sealed class PublicAccessEntryDeletedNotification : DeletedNotification + public PublicAccessEntryDeletedNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) { - public PublicAccessEntryDeletedNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessEntryDeletingNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessEntryDeletingNotification.cs index 42c4c1bdb9..d135af805b 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessEntryDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessEntryDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class PublicAccessEntryDeletingNotification : DeletingNotification - { - public PublicAccessEntryDeletingNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PublicAccessEntryDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class PublicAccessEntryDeletingNotification : DeletingNotification +{ + public PublicAccessEntryDeletingNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) + { + } + + public PublicAccessEntryDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessEntrySavedNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessEntrySavedNotification.cs index 8c0d253500..1f92d935d7 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessEntrySavedNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessEntrySavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class PublicAccessEntrySavedNotification : SavedNotification - { - public PublicAccessEntrySavedNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PublicAccessEntrySavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class PublicAccessEntrySavedNotification : SavedNotification +{ + public PublicAccessEntrySavedNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) + { + } + + public PublicAccessEntrySavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/PublicAccessEntrySavingNotification.cs b/src/Umbraco.Core/Notifications/PublicAccessEntrySavingNotification.cs index 3fbd666b8d..9f9e6f8a4a 100644 --- a/src/Umbraco.Core/Notifications/PublicAccessEntrySavingNotification.cs +++ b/src/Umbraco.Core/Notifications/PublicAccessEntrySavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class PublicAccessEntrySavingNotification : SavingNotification - { - public PublicAccessEntrySavingNotification(PublicAccessEntry target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public PublicAccessEntrySavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class PublicAccessEntrySavingNotification : SavingNotification +{ + public PublicAccessEntrySavingNotification(PublicAccessEntry target, EventMessages messages) + : base(target, messages) + { + } + + public PublicAccessEntrySavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationDeletedNotification.cs b/src/Umbraco.Core/Notifications/RelationDeletedNotification.cs index f7af0e9b29..2d93e077c5 100644 --- a/src/Umbraco.Core/Notifications/RelationDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationDeletedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationDeletedNotification : DeletedNotification - { - public RelationDeletedNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationDeletedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationDeletedNotification : DeletedNotification +{ + public RelationDeletedNotification(IRelation target, EventMessages messages) + : base(target, messages) + { + } + + public RelationDeletedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationDeletingNotification.cs b/src/Umbraco.Core/Notifications/RelationDeletingNotification.cs index 8873d95226..54b49afb54 100644 --- a/src/Umbraco.Core/Notifications/RelationDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationDeletingNotification : DeletingNotification - { - public RelationDeletingNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationDeletingNotification : DeletingNotification +{ + public RelationDeletingNotification(IRelation target, EventMessages messages) + : base(target, messages) + { + } + + public RelationDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationSavedNotification.cs b/src/Umbraco.Core/Notifications/RelationSavedNotification.cs index 8b0313f87c..3a0b4d9ec8 100644 --- a/src/Umbraco.Core/Notifications/RelationSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationSavedNotification : SavedNotification - { - public RelationSavedNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationSavedNotification : SavedNotification +{ + public RelationSavedNotification(IRelation target, EventMessages messages) + : base(target, messages) + { + } + + public RelationSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationSavingNotification.cs b/src/Umbraco.Core/Notifications/RelationSavingNotification.cs index 5afe71da53..069e0d5fdc 100644 --- a/src/Umbraco.Core/Notifications/RelationSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationSavingNotification : SavingNotification - { - public RelationSavingNotification(IRelation target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationSavingNotification : SavingNotification +{ + public RelationSavingNotification(IRelation target, EventMessages messages) + : base(target, messages) + { + } + + public RelationSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeCacheRefresherNotification.cs index ff8cf52891..1d816a4067 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeCacheRefresherNotification.cs @@ -1,11 +1,11 @@ -using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class RelationTypeCacheRefresherNotification : CacheRefresherNotification { - public class RelationTypeCacheRefresherNotification : CacheRefresherNotification + public RelationTypeCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public RelationTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeDeletedNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeDeletedNotification.cs index 8534edcb49..498a4c4370 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class RelationTypeDeletedNotification : DeletedNotification { - public class RelationTypeDeletedNotification : DeletedNotification + public RelationTypeDeletedNotification(IRelationType target, EventMessages messages) + : base(target, messages) { - public RelationTypeDeletedNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeDeletingNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeDeletingNotification.cs index 904a82c08b..d9ba61b2b5 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationTypeDeletingNotification : DeletingNotification - { - public RelationTypeDeletingNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationTypeDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationTypeDeletingNotification : DeletingNotification +{ + public RelationTypeDeletingNotification(IRelationType target, EventMessages messages) + : base(target, messages) + { + } + + public RelationTypeDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeSavedNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeSavedNotification.cs index e2e69475d7..d0a1aaf16e 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationTypeSavedNotification : SavedNotification - { - public RelationTypeSavedNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationTypeSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationTypeSavedNotification : SavedNotification +{ + public RelationTypeSavedNotification(IRelationType target, EventMessages messages) + : base(target, messages) + { + } + + public RelationTypeSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RelationTypeSavingNotification.cs b/src/Umbraco.Core/Notifications/RelationTypeSavingNotification.cs index 2fdebe97e7..e2f7979e86 100644 --- a/src/Umbraco.Core/Notifications/RelationTypeSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/RelationTypeSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class RelationTypeSavingNotification : SavingNotification - { - public RelationTypeSavingNotification(IRelationType target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public RelationTypeSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class RelationTypeSavingNotification : SavingNotification +{ + public RelationTypeSavingNotification(IRelationType target, EventMessages messages) + : base(target, messages) + { + } + + public RelationTypeSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs b/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs index ed76cfbf69..4ae0a720f7 100644 --- a/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs +++ b/src/Umbraco.Core/Notifications/RemovedMemberRolesNotification.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications -{ - public class RemovedMemberRolesNotification : MemberRolesNotification - { - public RemovedMemberRolesNotification(int[] memberIds, string[] roles) : base(memberIds, roles) - { +namespace Umbraco.Cms.Core.Notifications; - } +public class RemovedMemberRolesNotification : MemberRolesNotification +{ + public RemovedMemberRolesNotification(int[] memberIds, string[] roles) + : base(memberIds, roles) + { } } diff --git a/src/Umbraco.Core/Notifications/RenamedNotification.cs b/src/Umbraco.Core/Notifications/RenamedNotification.cs index 724069aba7..ab25fbdeb9 100644 --- a/src/Umbraco.Core/Notifications/RenamedNotification.cs +++ b/src/Umbraco.Core/Notifications/RenamedNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class RenamedNotification : EnumerableObjectNotification { - public abstract class RenamedNotification : EnumerableObjectNotification + protected RenamedNotification(T target, EventMessages messages) + : base(target, messages) { - protected RenamedNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected RenamedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable Entities => Target; } + + protected RenamedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable Entities => Target; } diff --git a/src/Umbraco.Core/Notifications/RenamingNotification.cs b/src/Umbraco.Core/Notifications/RenamingNotification.cs index 1e4184bc3d..4f15827ae4 100644 --- a/src/Umbraco.Core/Notifications/RenamingNotification.cs +++ b/src/Umbraco.Core/Notifications/RenamingNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class RenamingNotification : CancelableEnumerableObjectNotification { - public abstract class RenamingNotification : CancelableEnumerableObjectNotification + protected RenamingNotification(T target, EventMessages messages) + : base(target, messages) { - protected RenamingNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected RenamingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable Entities => Target; } + + protected RenamingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable Entities => Target; } diff --git a/src/Umbraco.Core/Notifications/RolledBackNotification.cs b/src/Umbraco.Core/Notifications/RolledBackNotification.cs index fded45c6b1..280f55538e 100644 --- a/src/Umbraco.Core/Notifications/RolledBackNotification.cs +++ b/src/Umbraco.Core/Notifications/RolledBackNotification.cs @@ -3,14 +3,15 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class RolledBackNotification : ObjectNotification where T : class - { - protected RolledBackNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T Entity => Target; +public abstract class RolledBackNotification : ObjectNotification + where T : class +{ + protected RolledBackNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/RollingBackNotification.cs b/src/Umbraco.Core/Notifications/RollingBackNotification.cs index 1064a7897c..3d06d443ea 100644 --- a/src/Umbraco.Core/Notifications/RollingBackNotification.cs +++ b/src/Umbraco.Core/Notifications/RollingBackNotification.cs @@ -3,14 +3,15 @@ using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class RollingBackNotification : CancelableObjectNotification where T : class - { - protected RollingBackNotification(T target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public T Entity => Target; +public abstract class RollingBackNotification : CancelableObjectNotification + where T : class +{ + protected RollingBackNotification(T target, EventMessages messages) + : base(target, messages) + { } + + public T Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs b/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs index c8b2d8e0d6..b5169aa0ab 100644 --- a/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs +++ b/src/Umbraco.Core/Notifications/RoutingRequestNotification.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Used for notifying when an Umbraco request is being built +/// +public class RoutingRequestNotification : INotification { /// - /// Used for notifying when an Umbraco request is being built + /// Initializes a new instance of the class. /// - public class RoutingRequestNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public RoutingRequestNotification(IPublishedRequestBuilder requestBuilder) => RequestBuilder = requestBuilder; + public RoutingRequestNotification(IPublishedRequestBuilder requestBuilder) => RequestBuilder = requestBuilder; - /// - /// Gets the - /// - public IPublishedRequestBuilder RequestBuilder { get; } - } + /// + /// Gets the + /// + public IPublishedRequestBuilder RequestBuilder { get; } } diff --git a/src/Umbraco.Core/Notifications/RuntimeUnattendedInstallNotification.cs b/src/Umbraco.Core/Notifications/RuntimeUnattendedInstallNotification.cs index f638ec2d3c..e0ef991e70 100644 --- a/src/Umbraco.Core/Notifications/RuntimeUnattendedInstallNotification.cs +++ b/src/Umbraco.Core/Notifications/RuntimeUnattendedInstallNotification.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Used to notify when the core runtime can do an unattended install. +/// +/// +/// It is entirely up to the handler to determine if an unattended installation should occur and +/// to perform the logic. +/// +public class RuntimeUnattendedInstallNotification : INotification { - /// - /// Used to notify when the core runtime can do an unattended install. - /// - /// - /// It is entirely up to the handler to determine if an unattended installation should occur and - /// to perform the logic. - /// - public class RuntimeUnattendedInstallNotification : INotification - { - } } diff --git a/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs b/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs index 4d676f68ce..fd1fa02113 100644 --- a/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs +++ b/src/Umbraco.Core/Notifications/RuntimeUnattendedUpgradeNotification.cs @@ -1,26 +1,24 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Used to notify when the core runtime can do an unattended upgrade. +/// +/// +/// It is entirely up to the handler to determine if an unattended upgrade should occur and +/// to perform the logic. +/// +public class RuntimeUnattendedUpgradeNotification : INotification { + public enum UpgradeResult + { + NotRequired = 0, + HasErrors = 1, + CoreUpgradeComplete = 100, + PackageMigrationComplete = 101, + } /// - /// Used to notify when the core runtime can do an unattended upgrade. + /// Gets/sets the result of the unattended upgrade /// - /// - /// It is entirely up to the handler to determine if an unattended upgrade should occur and - /// to perform the logic. - /// - public class RuntimeUnattendedUpgradeNotification : INotification - { - /// - /// Gets/sets the result of the unattended upgrade - /// - public UpgradeResult UnattendedUpgradeResult { get; set; } = UpgradeResult.NotRequired; - - public enum UpgradeResult - { - NotRequired = 0, - HasErrors = 1, - CoreUpgradeComplete = 100, - PackageMigrationComplete = 101 - } - } + public UpgradeResult UnattendedUpgradeResult { get; set; } = UpgradeResult.NotRequired; } diff --git a/src/Umbraco.Core/Notifications/SavedNotification.cs b/src/Umbraco.Core/Notifications/SavedNotification.cs index 0a9af8c1ff..655b9b66d1 100644 --- a/src/Umbraco.Core/Notifications/SavedNotification.cs +++ b/src/Umbraco.Core/Notifications/SavedNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class SavedNotification : EnumerableObjectNotification { - public abstract class SavedNotification : EnumerableObjectNotification + protected SavedNotification(T target, EventMessages messages) + : base(target, messages) { - protected SavedNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected SavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable SavedEntities => Target; } + + protected SavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable SavedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/SavingNotification.cs b/src/Umbraco.Core/Notifications/SavingNotification.cs index 34962f5396..9724d4580a 100644 --- a/src/Umbraco.Core/Notifications/SavingNotification.cs +++ b/src/Umbraco.Core/Notifications/SavingNotification.cs @@ -1,21 +1,21 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class SavingNotification : CancelableEnumerableObjectNotification { - public abstract class SavingNotification : CancelableEnumerableObjectNotification + protected SavingNotification(T target, EventMessages messages) + : base(target, messages) { - protected SavingNotification(T target, EventMessages messages) : base(target, messages) - { - } - - protected SavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable SavedEntities => Target; } + + protected SavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable SavedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/ScopedEntityRemoveNotification.cs b/src/Umbraco.Core/Notifications/ScopedEntityRemoveNotification.cs index 307ae2103c..f72af376c3 100644 --- a/src/Umbraco.Core/Notifications/ScopedEntityRemoveNotification.cs +++ b/src/Umbraco.Core/Notifications/ScopedEntityRemoveNotification.cs @@ -1,18 +1,17 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - [Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public class ScopedEntityRemoveNotification : ObjectNotification - { - public ScopedEntityRemoveNotification(IContentBase target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IContentBase Entity => Target; +[Obsolete("This is only used for the internal cache and will change, use tree change notifications instead")] +[EditorBrowsable(EditorBrowsableState.Never)] +public class ScopedEntityRemoveNotification : ObjectNotification +{ + public ScopedEntityRemoveNotification(IContentBase target, EventMessages messages) + : base(target, messages) + { } + + public IContentBase Entity => Target; } diff --git a/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs b/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs index 650f2d0564..3ca5f1dc42 100644 --- a/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/ScriptDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class ScriptDeletedNotification : DeletedNotification { - public class ScriptDeletedNotification : DeletedNotification + public ScriptDeletedNotification(IScript target, EventMessages messages) + : base(target, messages) { - public ScriptDeletedNotification(IScript target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/ScriptDeletingNotification.cs b/src/Umbraco.Core/Notifications/ScriptDeletingNotification.cs index 085c98d600..946dc7f750 100644 --- a/src/Umbraco.Core/Notifications/ScriptDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/ScriptDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ScriptDeletingNotification : DeletingNotification - { - public ScriptDeletingNotification(IScript target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ScriptDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ScriptDeletingNotification : DeletingNotification +{ + public ScriptDeletingNotification(IScript target, EventMessages messages) + : base(target, messages) + { + } + + public ScriptDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ScriptSavedNotification.cs b/src/Umbraco.Core/Notifications/ScriptSavedNotification.cs index 6ccb9f1446..2a292383e9 100644 --- a/src/Umbraco.Core/Notifications/ScriptSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/ScriptSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ScriptSavedNotification : SavedNotification - { - public ScriptSavedNotification(IScript target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ScriptSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ScriptSavedNotification : SavedNotification +{ + public ScriptSavedNotification(IScript target, EventMessages messages) + : base(target, messages) + { + } + + public ScriptSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/ScriptSavingNotification.cs b/src/Umbraco.Core/Notifications/ScriptSavingNotification.cs index 92ad0ded4e..3ab2b13ce4 100644 --- a/src/Umbraco.Core/Notifications/ScriptSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/ScriptSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class ScriptSavingNotification : SavingNotification - { - public ScriptSavingNotification(IScript target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public ScriptSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class ScriptSavingNotification : SavingNotification +{ + public ScriptSavingNotification(IScript target, EventMessages messages) + : base(target, messages) + { + } + + public ScriptSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/SendEmailNotification.cs b/src/Umbraco.Core/Notifications/SendEmailNotification.cs index f87a2a0ba8..66d7ee038a 100644 --- a/src/Umbraco.Core/Notifications/SendEmailNotification.cs +++ b/src/Umbraco.Core/Notifications/SendEmailNotification.cs @@ -1,30 +1,29 @@ using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendEmailNotification : INotification { - public class SendEmailNotification : INotification + public SendEmailNotification(NotificationEmailModel message, string emailType) { - public SendEmailNotification(NotificationEmailModel message, string emailType) - { - Message = message; - EmailType = emailType; - } - - public NotificationEmailModel Message { get; } - - /// - /// Some metadata about the email which can be used by handlers to determine if they should handle the email or not - /// - public string EmailType { get; } - - /// - /// Call to tell Umbraco that the email sending is handled. - /// - public void HandleEmail() => IsHandled = true; - - /// - /// Returns true if the email sending is handled. - /// - public bool IsHandled { get; private set; } + Message = message; + EmailType = emailType; } + + public NotificationEmailModel Message { get; } + + /// + /// Some metadata about the email which can be used by handlers to determine if they should handle the email or not + /// + public string EmailType { get; } + + /// + /// Returns true if the email sending is handled. + /// + public bool IsHandled { get; private set; } + + /// + /// Call to tell Umbraco that the email sending is handled. + /// + public void HandleEmail() => IsHandled = true; } diff --git a/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs b/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs index 07ab3c3626..ff57f9c902 100644 --- a/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs @@ -1,19 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingAllowedChildrenNotification : INotification { - public class SendingAllowedChildrenNotification : INotification + public SendingAllowedChildrenNotification(IEnumerable children, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public IEnumerable Children { get; set; } - - public SendingAllowedChildrenNotification(IEnumerable children, IUmbracoContext umbracoContext) - { - UmbracoContext = umbracoContext; - Children = children; - } + UmbracoContext = umbracoContext; + Children = children; } + + public IUmbracoContext UmbracoContext { get; } + + public IEnumerable Children { get; set; } } diff --git a/src/Umbraco.Core/Notifications/SendingContentNotification.cs b/src/Umbraco.Core/Notifications/SendingContentNotification.cs index 4d8d93ce75..a42fefca68 100644 --- a/src/Umbraco.Core/Notifications/SendingContentNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingContentNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingContentNotification : INotification { - public class SendingContentNotification : INotification + public SendingContentNotification(ContentItemDisplay content, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public ContentItemDisplay Content { get; } - - public SendingContentNotification(ContentItemDisplay content, IUmbracoContext umbracoContext) - { - Content = content; - UmbracoContext = umbracoContext; - } + Content = content; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public ContentItemDisplay Content { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs b/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs index b81339fcbf..886e257529 100644 --- a/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingDashboardsNotification.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingDashboardsNotification : INotification { - public class SendingDashboardsNotification : INotification + public SendingDashboardsNotification(IEnumerable> dashboards, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public IEnumerable> Dashboards { get; } - - public SendingDashboardsNotification(IEnumerable> dashboards, IUmbracoContext umbracoContext) - { - Dashboards = dashboards; - UmbracoContext = umbracoContext; - } + Dashboards = dashboards; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public IEnumerable> Dashboards { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingMediaNotification.cs b/src/Umbraco.Core/Notifications/SendingMediaNotification.cs index 2fd8f65a4d..cca282b3ea 100644 --- a/src/Umbraco.Core/Notifications/SendingMediaNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingMediaNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingMediaNotification : INotification { - public class SendingMediaNotification : INotification + public SendingMediaNotification(MediaItemDisplay media, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public MediaItemDisplay Media { get; } - - public SendingMediaNotification(MediaItemDisplay media, IUmbracoContext umbracoContext) - { - Media = media; - UmbracoContext = umbracoContext; - } + Media = media; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public MediaItemDisplay Media { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingMemberNotification.cs b/src/Umbraco.Core/Notifications/SendingMemberNotification.cs index cc868836f9..e9e03a868f 100644 --- a/src/Umbraco.Core/Notifications/SendingMemberNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingMemberNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingMemberNotification : INotification { - public class SendingMemberNotification : INotification + public SendingMemberNotification(MemberDisplay member, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public MemberDisplay Member { get; } - - public SendingMemberNotification(MemberDisplay member, IUmbracoContext umbracoContext) - { - Member = member; - UmbracoContext = umbracoContext; - } + Member = member; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public MemberDisplay Member { get; } } diff --git a/src/Umbraco.Core/Notifications/SendingUserNotification.cs b/src/Umbraco.Core/Notifications/SendingUserNotification.cs index 9e3422f1d9..da46ec749e 100644 --- a/src/Umbraco.Core/Notifications/SendingUserNotification.cs +++ b/src/Umbraco.Core/Notifications/SendingUserNotification.cs @@ -1,18 +1,17 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class SendingUserNotification : INotification { - public class SendingUserNotification : INotification + public SendingUserNotification(UserDisplay user, IUmbracoContext umbracoContext) { - public IUmbracoContext UmbracoContext { get; } - - public UserDisplay User { get; } - - public SendingUserNotification(UserDisplay user, IUmbracoContext umbracoContext) - { - User = user; - UmbracoContext = umbracoContext; - } + User = user; + UmbracoContext = umbracoContext; } + + public IUmbracoContext UmbracoContext { get; } + + public UserDisplay User { get; } } diff --git a/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs b/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs index 7fa83a5a6d..0171009bf2 100644 --- a/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs +++ b/src/Umbraco.Core/Notifications/ServerVariablesParsingNotification.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +/// +/// A notification for when server variables are parsing +/// +public class ServerVariablesParsingNotification : INotification { /// - /// A notification for when server variables are parsing + /// Initializes a new instance of the class. /// - public class ServerVariablesParsingNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public ServerVariablesParsingNotification(IDictionary serverVariables) => ServerVariables = serverVariables; + public ServerVariablesParsingNotification(IDictionary serverVariables) => + ServerVariables = serverVariables; - /// - /// Gets a mutable dictionary of server variables - /// - public IDictionary ServerVariables { get; } - } + /// + /// Gets a mutable dictionary of server variables + /// + public IDictionary ServerVariables { get; } } diff --git a/src/Umbraco.Core/Notifications/SortedNotification.cs b/src/Umbraco.Core/Notifications/SortedNotification.cs index ffc50d6bc9..49910f8223 100644 --- a/src/Umbraco.Core/Notifications/SortedNotification.cs +++ b/src/Umbraco.Core/Notifications/SortedNotification.cs @@ -1,17 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class SortedNotification : EnumerableObjectNotification - { - protected SortedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IEnumerable SortedEntities => Target; +public abstract class SortedNotification : EnumerableObjectNotification +{ + protected SortedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } + + public IEnumerable SortedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/SortingNotification.cs b/src/Umbraco.Core/Notifications/SortingNotification.cs index 1801bfa656..26e735f91b 100644 --- a/src/Umbraco.Core/Notifications/SortingNotification.cs +++ b/src/Umbraco.Core/Notifications/SortingNotification.cs @@ -1,17 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public abstract class SortingNotification : CancelableEnumerableObjectNotification - { - protected SortingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public IEnumerable SortedEntities => Target; +public abstract class SortingNotification : CancelableEnumerableObjectNotification +{ + protected SortingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } + + public IEnumerable SortedEntities => Target; } diff --git a/src/Umbraco.Core/Notifications/StatefulNotification.cs b/src/Umbraco.Core/Notifications/StatefulNotification.cs index 15ee320a40..5f84000d48 100644 --- a/src/Umbraco.Core/Notifications/StatefulNotification.cs +++ b/src/Umbraco.Core/Notifications/StatefulNotification.cs @@ -1,21 +1,18 @@ // Copyright (c) Umbraco. -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public abstract class StatefulNotification : IStatefulNotification { - public abstract class StatefulNotification : IStatefulNotification - { - private IDictionary? _state; + private IDictionary? _state; - /// - /// This can be used by event subscribers to store state in the notification so they easily deal with custom state data between - /// a starting ("ing") and an ending ("ed") notification - /// - public IDictionary State - { - get => _state ??= new Dictionary(); - set => _state = value; - } + /// + /// This can be used by event subscribers to store state in the notification so they easily deal with custom state data + /// between a starting ("ing") and an ending ("ed") notification + /// + public IDictionary State + { + get => _state ??= new Dictionary(); + set => _state = value; } } diff --git a/src/Umbraco.Core/Notifications/StylesheetDeletedNotification.cs b/src/Umbraco.Core/Notifications/StylesheetDeletedNotification.cs index 743cadab63..4b359d60ec 100644 --- a/src/Umbraco.Core/Notifications/StylesheetDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/StylesheetDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class StylesheetDeletedNotification : DeletedNotification { - public class StylesheetDeletedNotification : DeletedNotification + public StylesheetDeletedNotification(IStylesheet target, EventMessages messages) + : base(target, messages) { - public StylesheetDeletedNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/StylesheetDeletingNotification.cs b/src/Umbraco.Core/Notifications/StylesheetDeletingNotification.cs index 8a0c411b13..8689363577 100644 --- a/src/Umbraco.Core/Notifications/StylesheetDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/StylesheetDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class StylesheetDeletingNotification : DeletingNotification - { - public StylesheetDeletingNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public StylesheetDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class StylesheetDeletingNotification : DeletingNotification +{ + public StylesheetDeletingNotification(IStylesheet target, EventMessages messages) + : base(target, messages) + { + } + + public StylesheetDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/StylesheetSavedNotification.cs b/src/Umbraco.Core/Notifications/StylesheetSavedNotification.cs index 0ceeb209e0..2f12bebe15 100644 --- a/src/Umbraco.Core/Notifications/StylesheetSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/StylesheetSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class StylesheetSavedNotification : SavedNotification - { - public StylesheetSavedNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public StylesheetSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class StylesheetSavedNotification : SavedNotification +{ + public StylesheetSavedNotification(IStylesheet target, EventMessages messages) + : base(target, messages) + { + } + + public StylesheetSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/StylesheetSavingNotification.cs b/src/Umbraco.Core/Notifications/StylesheetSavingNotification.cs index d08bdebac4..0d6804a76c 100644 --- a/src/Umbraco.Core/Notifications/StylesheetSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/StylesheetSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class StylesheetSavingNotification : SavingNotification - { - public StylesheetSavingNotification(IStylesheet target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public StylesheetSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class StylesheetSavingNotification : SavingNotification +{ + public StylesheetSavingNotification(IStylesheet target, EventMessages messages) + : base(target, messages) + { + } + + public StylesheetSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/TemplateCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/TemplateCacheRefresherNotification.cs index 689d2a52ff..a8b119390f 100644 --- a/src/Umbraco.Core/Notifications/TemplateCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateCacheRefresherNotification.cs @@ -1,11 +1,11 @@ using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateCacheRefresherNotification : CacheRefresherNotification { - public class TemplateCacheRefresherNotification : CacheRefresherNotification + public TemplateCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public TemplateCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/TemplateDeletedNotification.cs b/src/Umbraco.Core/Notifications/TemplateDeletedNotification.cs index 01d6dc7e6d..1bab7d2dc5 100644 --- a/src/Umbraco.Core/Notifications/TemplateDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateDeletedNotification : DeletedNotification { - public class TemplateDeletedNotification : DeletedNotification + public TemplateDeletedNotification(ITemplate target, EventMessages messages) + : base(target, messages) { - public TemplateDeletedNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/TemplateDeletingNotification.cs b/src/Umbraco.Core/Notifications/TemplateDeletingNotification.cs index 6434c47c46..791f43d116 100644 --- a/src/Umbraco.Core/Notifications/TemplateDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications -{ - public class TemplateDeletingNotification : DeletingNotification - { - public TemplateDeletingNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public TemplateDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public class TemplateDeletingNotification : DeletingNotification +{ + public TemplateDeletingNotification(ITemplate target, EventMessages messages) + : base(target, messages) + { + } + + public TemplateDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/TemplateSavedNotification.cs b/src/Umbraco.Core/Notifications/TemplateSavedNotification.cs index ad75a32c02..8b51e795d4 100644 --- a/src/Umbraco.Core/Notifications/TemplateSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateSavedNotification.cs @@ -1,68 +1,69 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateSavedNotification : SavedNotification { - public class TemplateSavedNotification : SavedNotification + private const string TemplateForContentTypeKey = "CreateTemplateForContentType"; + private const string ContentTypeAliasKey = "ContentTypeAlias"; + + public TemplateSavedNotification(ITemplate target, EventMessages messages) + : base(target, messages) { - private const string s_templateForContentTypeKey = "CreateTemplateForContentType"; - private const string s_contentTypeAliasKey = "ContentTypeAlias"; + } - public TemplateSavedNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } + public TemplateSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } - public TemplateSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) + public bool CreateTemplateForContentType + { + get { - } - - public bool CreateTemplateForContentType - { - get + if (State?.TryGetValue(TemplateForContentTypeKey, out var result) ?? false) { - if (State?.TryGetValue(s_templateForContentTypeKey, out var result) ?? false) + if (result is not bool createTemplate) { - if (result is not bool createTemplate) - { - return false; - } - - return createTemplate; + return false; } - return false; - } - set - { - if (!value is bool && State is not null) - { - State[s_templateForContentTypeKey] = value; - } - } - } - - public string? ContentTypeAlias - { - get - { - if (State?.TryGetValue(s_contentTypeAliasKey, out var result) ?? false) - { - return result as string; - } - - return null; + return createTemplate; } - set + return false; + } + + set + { + if (!value is bool && State is not null) { - if (value is not null && State is not null) - { - State[s_contentTypeAliasKey] = value; - } + State[TemplateForContentTypeKey] = value; + } + } + } + + public string? ContentTypeAlias + { + get + { + if (State?.TryGetValue(ContentTypeAliasKey, out var result) ?? false) + { + return result as string; + } + + return null; + } + + set + { + if (value is not null && State is not null) + { + State[ContentTypeAliasKey] = value; } } } diff --git a/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs b/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs index 95a681d2f8..45a325feed 100644 --- a/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/TemplateSavingNotification.cs @@ -1,83 +1,83 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class TemplateSavingNotification : SavingNotification { - public class TemplateSavingNotification : SavingNotification + private const string TemplateForContentTypeKey = "CreateTemplateForContentType"; + private const string ContentTypeAliasKey = "ContentTypeAlias"; + + public TemplateSavingNotification(ITemplate target, EventMessages messages) + : base(target, messages) { - private const string s_templateForContentTypeKey = "CreateTemplateForContentType"; - private const string s_contentTypeAliasKey = "ContentTypeAlias"; + } - public TemplateSavingNotification(ITemplate target, EventMessages messages) : base(target, messages) - { - } + public TemplateSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { + } - public TemplateSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } + public TemplateSavingNotification(ITemplate target, EventMessages messages, bool createTemplateForContentType, string contentTypeAlias) + : base(target, messages) + { + CreateTemplateForContentType = createTemplateForContentType; + ContentTypeAlias = contentTypeAlias; + } - public TemplateSavingNotification(ITemplate target, EventMessages messages, bool createTemplateForContentType, - string contentTypeAlias) : base(target, messages) - { - CreateTemplateForContentType = createTemplateForContentType; - ContentTypeAlias = contentTypeAlias; - } + public TemplateSavingNotification(IEnumerable target, EventMessages messages, bool createTemplateForContentType, string contentTypeAlias) + : base(target, messages) + { + CreateTemplateForContentType = createTemplateForContentType; + ContentTypeAlias = contentTypeAlias; + } - public TemplateSavingNotification(IEnumerable target, EventMessages messages, - bool createTemplateForContentType, - string contentTypeAlias) : base(target, messages) + public bool CreateTemplateForContentType + { + get { - CreateTemplateForContentType = createTemplateForContentType; - ContentTypeAlias = contentTypeAlias; - } - - public bool CreateTemplateForContentType - { - get + if (State?.TryGetValue(TemplateForContentTypeKey, out var result) ?? false) { - if (State?.TryGetValue(s_templateForContentTypeKey, out var result) ?? false) + if (result is not bool createTemplate) { - if (result is not bool createTemplate) - { - return false; - } - - return createTemplate; + return false; } - return false; - } - set - { - if (!value is bool && State is not null) - { - State[s_templateForContentTypeKey] = value; - } - } - } - - public string? ContentTypeAlias - { - get - { - if (State?.TryGetValue(s_contentTypeAliasKey, out var result) ?? false) - { - return result as string; - } - - return null; + return createTemplate; } - set + return false; + } + + set + { + if (!value is bool && State is not null) { - if (value is not null && State is not null) - { - State[s_contentTypeAliasKey] = value; - } + State[TemplateForContentTypeKey] = value; + } + } + } + + public string? ContentTypeAlias + { + get + { + if (State?.TryGetValue(ContentTypeAliasKey, out var result) ?? false) + { + return result as string; + } + + return null; + } + + set + { + if (value is not null && State is not null) + { + State[ContentTypeAliasKey] = value; } } } diff --git a/src/Umbraco.Core/Notifications/TreeChangeNotification.cs b/src/Umbraco.Core/Notifications/TreeChangeNotification.cs index bdbd0fc044..2187f72659 100644 --- a/src/Umbraco.Core/Notifications/TreeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/TreeChangeNotification.cs @@ -1,19 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public abstract class TreeChangeNotification : EnumerableObjectNotification> { - public abstract class TreeChangeNotification : EnumerableObjectNotification> + protected TreeChangeNotification(TreeChange target, EventMessages messages) + : base(target, messages) { - protected TreeChangeNotification(TreeChange target, EventMessages messages) : base(target, messages) - { - } - - protected TreeChangeNotification(IEnumerable> target, EventMessages messages) : base(target, messages) - { - } - - public IEnumerable> Changes => Target; } + + protected TreeChangeNotification(IEnumerable> target, EventMessages messages) + : base(target, messages) + { + } + + public IEnumerable> Changes => Target; } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationComponentsInstallingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationComponentsInstallingNotification.cs index 7f8d852115..036d5cf8a4 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationComponentsInstallingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationComponentsInstallingNotification.cs @@ -1,29 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +// TODO (V10): Remove this class. + +/// +/// Notification that occurs during the Umbraco boot process, before instances of initialize. +/// +[Obsolete( + "This notification was added to the core runtime start-up as a hook for Umbraco Cloud local connection string and database setup. " + + "Following re-work they are no longer used (from Deploy 9.2.0)." + + "Given they are non-documented and no other use is expected, they can be removed in the next major release")] +public class UmbracoApplicationComponentsInstallingNotification : INotification { - // TODO (V10): Remove this class. + /// + /// Initializes a new instance of the class. + /// + /// The runtime level + public UmbracoApplicationComponentsInstallingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; /// - /// Notification that occurs during the Umbraco boot process, before instances of initialize. + /// Gets the runtime level of execution. /// - [Obsolete("This notification was added to the core runtime start-up as a hook for Umbraco Cloud local connection string and database setup. " + - "Following re-work they are no longer used (from Deploy 9.2.0)." + - "Given they are non-documented and no other use is expected, they can be removed in the next major release")] - public class UmbracoApplicationComponentsInstallingNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - /// The runtime level - public UmbracoApplicationComponentsInstallingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; - - /// - /// Gets the runtime level of execution. - /// - public RuntimeLevel RuntimeLevel { get; } - } + public RuntimeLevel RuntimeLevel { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs index 66593ab086..2bbab6e7ec 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs @@ -1,26 +1,23 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +// TODO (V10): Remove this class. + +/// +/// Notification that occurs during Umbraco boot after the MainDom has been acquired. +/// +[Obsolete( + "This notification was added to the core runtime start-up as a hook for Umbraco Cloud local connection string and database setup. " + + "Following re-work they are no longer used (from Deploy 9.2.0)." + + "Given they are non-documented and no other use is expected, they can be removed in the next major release")] +public class UmbracoApplicationMainDomAcquiredNotification : INotification { - // TODO (V10): Remove this class. - /// - /// Notification that occurs during Umbraco boot after the MainDom has been acquired. + /// Initializes a new instance of the class. /// - [Obsolete("This notification was added to the core runtime start-up as a hook for Umbraco Cloud local connection string and database setup. " + - "Following re-work they are no longer used (from Deploy 9.2.0)." + - "Given they are non-documented and no other use is expected, they can be removed in the next major release")] - public class UmbracoApplicationMainDomAcquiredNotification : INotification + public UmbracoApplicationMainDomAcquiredNotification() { - /// - /// Initializes a new instance of the class. - /// - /// The runtime level - public UmbracoApplicationMainDomAcquiredNotification() - { - } } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs index 196af7dfe1..1e3f2b7bfd 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured. +/// +/// +public class UmbracoApplicationStartedNotification : IUmbracoApplicationLifetimeNotification { /// - /// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured. + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStartedNotification : IUmbracoApplicationLifetimeNotification - { - /// - /// Initializes a new instance of the class. - /// - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStartedNotification(bool isRestarting) => IsRestarting = isRestarting; + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartedNotification(bool isRestarting) => IsRestarting = isRestarting; - /// - public bool IsRestarting { get; } - } + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index 82b87aa3bf..7c7e97f29f 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,44 +1,42 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +/// +/// Notification that occurs at the very end of the Umbraco boot process (after all s are +/// initialized). +/// +/// +public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification { /// - /// Notification that occurs at the very end of the Umbraco boot process (after all s are initialized). + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification + /// The runtime level + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) + : this(runtimeLevel, false) { - /// - /// Initializes a new instance of the class. - /// - /// The runtime level - [Obsolete("Use ctor with all params")] - public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) - : this(runtimeLevel, false) - { - // TODO: Remove this constructor in V10 - } - - /// - /// Initializes a new instance of the class. - /// - /// The runtime level - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) - { - RuntimeLevel = runtimeLevel; - IsRestarting = isRestarting; - } - - /// - /// Gets the runtime level. - /// - /// - /// The runtime level. - /// - public RuntimeLevel RuntimeLevel { get; } - - /// - public bool IsRestarting { get; } + // TODO: Remove this constructor in V10 } + + /// + /// Initializes a new instance of the class. + /// + /// The runtime level + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) + { + RuntimeLevel = runtimeLevel; + IsRestarting = isRestarting; + } + + /// + /// Gets the runtime level. + /// + /// + /// The runtime level. + /// + public RuntimeLevel RuntimeLevel { get; } + + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs index c6dac40a26..ce9936a137 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Notification that occurs when Umbraco has completely shutdown. +/// +/// +public class UmbracoApplicationStoppedNotification : IUmbracoApplicationLifetimeNotification { /// - /// Notification that occurs when Umbraco has completely shutdown. + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStoppedNotification : IUmbracoApplicationLifetimeNotification - { - /// - /// Initializes a new instance of the class. - /// - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStoppedNotification(bool isRestarting) => IsRestarting = isRestarting; + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppedNotification(bool isRestarting) => IsRestarting = isRestarting; - /// - public bool IsRestarting { get; } - } + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index 062ca954d9..a877bd3162 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,30 +1,27 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +/// +/// Notification that occurs when Umbraco is shutting down (after all s are terminated). +/// +/// +public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification { /// - /// Notification that occurs when Umbraco is shutting down (after all s are terminated). + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStoppingNotification() + : this(false) { - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Use ctor with all params")] - public UmbracoApplicationStoppingNotification() - : this(false) - { - // TODO: Remove this constructor in V10 - } - - /// - /// Initializes a new instance of the class. - /// - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; - - /// - public bool IsRestarting { get; } + // TODO: Remove this constructor in V10 } + + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs b/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs index 76683f8d65..fedbb6c35b 100644 --- a/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoRequestBeginNotification.cs @@ -3,21 +3,20 @@ using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Notification raised on each request begin. +/// +public class UmbracoRequestBeginNotification : INotification { /// - /// Notification raised on each request begin. + /// Initializes a new instance of the class. /// - public class UmbracoRequestBeginNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestBeginNotification(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; + public UmbracoRequestBeginNotification(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; - /// - /// Gets the - /// - public IUmbracoContext UmbracoContext { get; } - } + /// + /// Gets the + /// + public IUmbracoContext UmbracoContext { get; } } diff --git a/src/Umbraco.Core/Notifications/UmbracoRequestEndNotification.cs b/src/Umbraco.Core/Notifications/UmbracoRequestEndNotification.cs index 27fb6ff09d..a3f9771153 100644 --- a/src/Umbraco.Core/Notifications/UmbracoRequestEndNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoRequestEndNotification.cs @@ -3,21 +3,20 @@ using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Notification raised on each request end. +/// +public class UmbracoRequestEndNotification : INotification { /// - /// Notification raised on each request end. + /// Initializes a new instance of the class. /// - public class UmbracoRequestEndNotification : INotification - { - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestEndNotification(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; + public UmbracoRequestEndNotification(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; - /// - /// Gets the - /// - public IUmbracoContext UmbracoContext { get; } - } + /// + /// Gets the + /// + public IUmbracoContext UmbracoContext { get; } } diff --git a/src/Umbraco.Core/Notifications/UnattendedInstallNotification.cs b/src/Umbraco.Core/Notifications/UnattendedInstallNotification.cs index 7f9b239ce2..c2e4f27b49 100644 --- a/src/Umbraco.Core/Notifications/UnattendedInstallNotification.cs +++ b/src/Umbraco.Core/Notifications/UnattendedInstallNotification.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +/// +/// Used to notify that an Unattended install has completed +/// +public class UnattendedInstallNotification : INotification { - /// - /// Used to notify that an Unattended install has completed - /// - public class UnattendedInstallNotification : INotification - { - } } diff --git a/src/Umbraco.Core/Notifications/UserCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/UserCacheRefresherNotification.cs index 4181d74dd7..589a2df682 100644 --- a/src/Umbraco.Core/Notifications/UserCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/UserCacheRefresherNotification.cs @@ -1,11 +1,11 @@ using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserCacheRefresherNotification : CacheRefresherNotification { - public class UserCacheRefresherNotification : CacheRefresherNotification + public UserCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public UserCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserDeletedNotification.cs b/src/Umbraco.Core/Notifications/UserDeletedNotification.cs index c272e51b22..a5d89bf167 100644 --- a/src/Umbraco.Core/Notifications/UserDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserDeletedNotification : DeletedNotification { - public sealed class UserDeletedNotification : DeletedNotification + public UserDeletedNotification(IUser target, EventMessages messages) + : base(target, messages) { - public UserDeletedNotification(IUser target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserDeletingNotification.cs b/src/Umbraco.Core/Notifications/UserDeletingNotification.cs index febfa27d94..611f8aa0ea 100644 --- a/src/Umbraco.Core/Notifications/UserDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserDeletingNotification : DeletingNotification - { - public UserDeletingNotification(IUser target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserDeletingNotification : DeletingNotification +{ + public UserDeletingNotification(IUser target, EventMessages messages) + : base(target, messages) + { + } + + public UserDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserForgotPasswordChangedNotification.cs b/src/Umbraco.Core/Notifications/UserForgotPasswordChangedNotification.cs index b4e93f8b67..b40e902e10 100644 --- a/src/Umbraco.Core/Notifications/UserForgotPasswordChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserForgotPasswordChangedNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserForgotPasswordChangedNotification : UserNotification { - public class UserForgotPasswordChangedNotification : UserNotification + public UserForgotPasswordChangedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserForgotPasswordChangedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserForgotPasswordRequestedNotification.cs b/src/Umbraco.Core/Notifications/UserForgotPasswordRequestedNotification.cs index 608e5c0f63..6181a33809 100644 --- a/src/Umbraco.Core/Notifications/UserForgotPasswordRequestedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserForgotPasswordRequestedNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserForgotPasswordRequestedNotification : UserNotification { - public class UserForgotPasswordRequestedNotification : UserNotification + public UserForgotPasswordRequestedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserForgotPasswordRequestedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserGroupCacheRefresherNotification.cs b/src/Umbraco.Core/Notifications/UserGroupCacheRefresherNotification.cs index 7aca0d5edb..d8e519ee9d 100644 --- a/src/Umbraco.Core/Notifications/UserGroupCacheRefresherNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupCacheRefresherNotification.cs @@ -1,11 +1,11 @@ using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserGroupCacheRefresherNotification : CacheRefresherNotification { - public class UserGroupCacheRefresherNotification : CacheRefresherNotification + public UserGroupCacheRefresherNotification(object messageObject, MessageType messageType) + : base(messageObject, messageType) { - public UserGroupCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserGroupDeletedNotification.cs b/src/Umbraco.Core/Notifications/UserGroupDeletedNotification.cs index 9877d95441..0555611f3a 100644 --- a/src/Umbraco.Core/Notifications/UserGroupDeletedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupDeletedNotification.cs @@ -4,12 +4,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public sealed class UserGroupDeletedNotification : DeletedNotification { - public sealed class UserGroupDeletedNotification : DeletedNotification + public UserGroupDeletedNotification(IUserGroup target, EventMessages messages) + : base(target, messages) { - public UserGroupDeletedNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserGroupDeletingNotification.cs b/src/Umbraco.Core/Notifications/UserGroupDeletingNotification.cs index af0e8d76d6..aea73393ab 100644 --- a/src/Umbraco.Core/Notifications/UserGroupDeletingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupDeletingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupDeletingNotification : DeletingNotification - { - public UserGroupDeletingNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupDeletingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupDeletingNotification : DeletingNotification +{ + public UserGroupDeletingNotification(IUserGroup target, EventMessages messages) + : base(target, messages) + { + } + + public UserGroupDeletingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserGroupSavedNotification.cs b/src/Umbraco.Core/Notifications/UserGroupSavedNotification.cs index fee23c06ea..aa4484c3d3 100644 --- a/src/Umbraco.Core/Notifications/UserGroupSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupSavedNotification : SavedNotification - { - public UserGroupSavedNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupSavedNotification : SavedNotification +{ + public UserGroupSavedNotification(IUserGroup target, EventMessages messages) + : base(target, messages) + { + } + + public UserGroupSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserGroupSavingNotification.cs b/src/Umbraco.Core/Notifications/UserGroupSavingNotification.cs index 0dc074bfdc..06c82c0298 100644 --- a/src/Umbraco.Core/Notifications/UserGroupSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupSavingNotification : SavingNotification - { - public UserGroupSavingNotification(IUserGroup target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupSavingNotification : SavingNotification +{ + public UserGroupSavingNotification(IUserGroup target, EventMessages messages) + : base(target, messages) + { + } + + public UserGroupSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserGroupWithUsersSavedNotification.cs b/src/Umbraco.Core/Notifications/UserGroupWithUsersSavedNotification.cs index 5e239660aa..399d194690 100644 --- a/src/Umbraco.Core/Notifications/UserGroupWithUsersSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupWithUsersSavedNotification.cs @@ -1,19 +1,19 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupWithUsersSavedNotification : SavedNotification - { - public UserGroupWithUsersSavedNotification(UserGroupWithUsers target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupWithUsersSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupWithUsersSavedNotification : SavedNotification +{ + public UserGroupWithUsersSavedNotification(UserGroupWithUsers target, EventMessages messages) + : base(target, messages) + { + } + + public UserGroupWithUsersSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserGroupWithUsersSavingNotification.cs b/src/Umbraco.Core/Notifications/UserGroupWithUsersSavingNotification.cs index f3dd362c20..c34d66841c 100644 --- a/src/Umbraco.Core/Notifications/UserGroupWithUsersSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserGroupWithUsersSavingNotification.cs @@ -1,19 +1,22 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserGroupWithUsersSavingNotification : SavingNotification - { - public UserGroupWithUsersSavingNotification(UserGroupWithUsers target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserGroupWithUsersSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserGroupWithUsersSavingNotification : SavingNotification +{ + public UserGroupWithUsersSavingNotification(UserGroupWithUsers target, EventMessages messages) + : base( + target, + messages) + { + } + + public UserGroupWithUsersSavingNotification(IEnumerable target, EventMessages messages) + : base( + target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserLockedNotification.cs b/src/Umbraco.Core/Notifications/UserLockedNotification.cs index b7485d9852..81fc798f63 100644 --- a/src/Umbraco.Core/Notifications/UserLockedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLockedNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLockedNotification : UserNotification { - public class UserLockedNotification : UserNotification + public UserLockedNotification(string ipAddress, string? affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserLockedNotification(string ipAddress, string? affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserLoginFailedNotification.cs b/src/Umbraco.Core/Notifications/UserLoginFailedNotification.cs index ff07b57832..a8cb3e9cc4 100644 --- a/src/Umbraco.Core/Notifications/UserLoginFailedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLoginFailedNotification.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLoginFailedNotification : UserNotification { - public class UserLoginFailedNotification : UserNotification + public UserLoginFailedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base( + ipAddress, affectedUserId, performingUserId) { - public UserLoginFailedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserLoginRequiresVerificationNotification.cs b/src/Umbraco.Core/Notifications/UserLoginRequiresVerificationNotification.cs index 5a975a1951..57f037712c 100644 --- a/src/Umbraco.Core/Notifications/UserLoginRequiresVerificationNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLoginRequiresVerificationNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLoginRequiresVerificationNotification : UserNotification { - public class UserLoginRequiresVerificationNotification : UserNotification + public UserLoginRequiresVerificationNotification(string ipAddress, string? affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserLoginRequiresVerificationNotification(string ipAddress, string? affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserLoginSuccessNotification.cs b/src/Umbraco.Core/Notifications/UserLoginSuccessNotification.cs index e9b79c68fe..5b20ca48ef 100644 --- a/src/Umbraco.Core/Notifications/UserLoginSuccessNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLoginSuccessNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserLoginSuccessNotification : UserNotification { - public class UserLoginSuccessNotification : UserNotification + public UserLoginSuccessNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserLoginSuccessNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs b/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs index 92e7dea03f..c93d42accf 100644 --- a/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs +++ b/src/Umbraco.Core/Notifications/UserLogoutSuccessNotification.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Notifications -{ - public class UserLogoutSuccessNotification : UserNotification - { - public UserLogoutSuccessNotification(string ipAddress, string? affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } +namespace Umbraco.Cms.Core.Notifications; - public string? SignOutRedirectUrl { get; set; } +public class UserLogoutSuccessNotification : UserNotification +{ + public UserLogoutSuccessNotification(string ipAddress, string? affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) + { } + + public string? SignOutRedirectUrl { get; set; } } diff --git a/src/Umbraco.Core/Notifications/UserNotification.cs b/src/Umbraco.Core/Notifications/UserNotification.cs index f0ce83c8fb..6141cdf389 100644 --- a/src/Umbraco.Core/Notifications/UserNotification.cs +++ b/src/Umbraco.Core/Notifications/UserNotification.cs @@ -1,35 +1,32 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public abstract class UserNotification : INotification { - public abstract class UserNotification : INotification + protected UserNotification(string ipAddress, string? affectedUserId, string performingUserId) { - protected UserNotification(string ipAddress, string? affectedUserId, string performingUserId) - { - DateTimeUtc = DateTime.UtcNow; - IpAddress = ipAddress; - AffectedUserId = affectedUserId; - PerformingUserId = performingUserId; - } - - /// - /// Current date/time in UTC format - /// - public DateTime DateTimeUtc { get; } - - /// - /// The source IP address of the user performing the action - /// - public string IpAddress { get; } - - /// - /// The user affected by the event raised - /// - public string? AffectedUserId { get; } - - /// - /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 - /// - public string PerformingUserId { get; } + DateTimeUtc = DateTime.UtcNow; + IpAddress = ipAddress; + AffectedUserId = affectedUserId; + PerformingUserId = performingUserId; } + + /// + /// Current date/time in UTC format + /// + public DateTime DateTimeUtc { get; } + + /// + /// The source IP address of the user performing the action + /// + public string IpAddress { get; } + + /// + /// The user affected by the event raised + /// + public string? AffectedUserId { get; } + + /// + /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 + /// + public string PerformingUserId { get; } } diff --git a/src/Umbraco.Core/Notifications/UserPasswordChangedNotification.cs b/src/Umbraco.Core/Notifications/UserPasswordChangedNotification.cs index 098be36867..a7cd1e51ae 100644 --- a/src/Umbraco.Core/Notifications/UserPasswordChangedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserPasswordChangedNotification.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserPasswordChangedNotification : UserNotification { - public class UserPasswordChangedNotification : UserNotification + public UserPasswordChangedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base( + ipAddress, affectedUserId, performingUserId) { - public UserPasswordChangedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserPasswordResetNotification.cs b/src/Umbraco.Core/Notifications/UserPasswordResetNotification.cs index fc60eef61e..8b23b5aa4f 100644 --- a/src/Umbraco.Core/Notifications/UserPasswordResetNotification.cs +++ b/src/Umbraco.Core/Notifications/UserPasswordResetNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserPasswordResetNotification : UserNotification { - public class UserPasswordResetNotification : UserNotification + public UserPasswordResetNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserPasswordResetNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserResetAccessFailedCountNotification.cs b/src/Umbraco.Core/Notifications/UserResetAccessFailedCountNotification.cs index 5cd03cc140..f1cce2df63 100644 --- a/src/Umbraco.Core/Notifications/UserResetAccessFailedCountNotification.cs +++ b/src/Umbraco.Core/Notifications/UserResetAccessFailedCountNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserResetAccessFailedCountNotification : UserNotification { - public class UserResetAccessFailedCountNotification : UserNotification + public UserResetAccessFailedCountNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserResetAccessFailedCountNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Notifications/UserSavedNotification.cs b/src/Umbraco.Core/Notifications/UserSavedNotification.cs index 892218af82..8292cb9f6d 100644 --- a/src/Umbraco.Core/Notifications/UserSavedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserSavedNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserSavedNotification : SavedNotification - { - public UserSavedNotification(IUser target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserSavedNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserSavedNotification : SavedNotification +{ + public UserSavedNotification(IUser target, EventMessages messages) + : base(target, messages) + { + } + + public UserSavedNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserSavingNotification.cs b/src/Umbraco.Core/Notifications/UserSavingNotification.cs index 57c0d867fa..3760f02881 100644 --- a/src/Umbraco.Core/Notifications/UserSavingNotification.cs +++ b/src/Umbraco.Core/Notifications/UserSavingNotification.cs @@ -1,20 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Notifications -{ - public sealed class UserSavingNotification : SavingNotification - { - public UserSavingNotification(IUser target, EventMessages messages) : base(target, messages) - { - } +namespace Umbraco.Cms.Core.Notifications; - public UserSavingNotification(IEnumerable target, EventMessages messages) : base(target, messages) - { - } +public sealed class UserSavingNotification : SavingNotification +{ + public UserSavingNotification(IUser target, EventMessages messages) + : base(target, messages) + { + } + + public UserSavingNotification(IEnumerable target, EventMessages messages) + : base(target, messages) + { } } diff --git a/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs b/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs index ccb07c593c..1eb6d774d0 100644 --- a/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs @@ -1,14 +1,8 @@ -using System; +namespace Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Notifications +public class UserTwoFactorRequestedNotification : INotification { - public class UserTwoFactorRequestedNotification : INotification - { - public UserTwoFactorRequestedNotification(Guid userKey) - { - UserKey = userKey; - } + public UserTwoFactorRequestedNotification(Guid userKey) => UserKey = userKey; - public Guid UserKey { get; } - } + public Guid UserKey { get; } } diff --git a/src/Umbraco.Core/Notifications/UserUnlockedNotification.cs b/src/Umbraco.Core/Notifications/UserUnlockedNotification.cs index 0c6cc7b9fd..7883595733 100644 --- a/src/Umbraco.Core/Notifications/UserUnlockedNotification.cs +++ b/src/Umbraco.Core/Notifications/UserUnlockedNotification.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Notifications +namespace Umbraco.Cms.Core.Notifications; + +public class UserUnlockedNotification : UserNotification { - public class UserUnlockedNotification : UserNotification + public UserUnlockedNotification(string ipAddress, string affectedUserId, string performingUserId) + : base(ipAddress, affectedUserId, performingUserId) { - public UserUnlockedNotification(string ipAddress, string affectedUserId, string performingUserId) : base(ipAddress, affectedUserId, performingUserId) - { - } } } diff --git a/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs b/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs index 16cd4ad0a4..fdb76f4bc2 100644 --- a/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs +++ b/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs @@ -1,73 +1,86 @@ -using System; -using System.IO; -using System.Linq; using System.Xml.Linq; using Umbraco.Cms.Core.Models.Packaging; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Parses the xml document contained in a compiled (zip) Umbraco package +/// +public class CompiledPackageXmlParser { - /// - /// Parses the xml document contained in a compiled (zip) Umbraco package - /// - public class CompiledPackageXmlParser + private readonly ConflictingPackageData _conflictingPackageData; + + public CompiledPackageXmlParser(ConflictingPackageData conflictingPackageData) => + _conflictingPackageData = conflictingPackageData; + + public CompiledPackage ToCompiledPackage(XDocument xml) { - private readonly ConflictingPackageData _conflictingPackageData; - - public CompiledPackageXmlParser(ConflictingPackageData conflictingPackageData) => _conflictingPackageData = conflictingPackageData; - - public CompiledPackage ToCompiledPackage(XDocument xml) + if (xml is null) { - if (xml is null) - { - throw new ArgumentNullException(nameof(xml)); - } - - if (xml.Root == null) throw new InvalidOperationException("The xml document is invalid"); - if (xml.Root.Name != "umbPackage") throw new FormatException("The xml document is invalid"); - - var info = xml.Root.Element("info"); - if (info == null) throw new FormatException("The xml document is invalid"); - var package = info.Element("package"); - if (package == null) throw new FormatException("The xml document is invalid"); - - var def = new CompiledPackage - { - // will be null because we don't know where this data is coming from and - // this value is irrelevant during install. - PackageFile = null, - Name = package.Element("name")?.Value ?? string.Empty, - Macros = xml.Root.Element("Macros")?.Elements("macro") ?? Enumerable.Empty(), - MacroPartialViews = xml.Root.Element("MacroPartialViews")?.Elements("View") ?? Enumerable.Empty(), - PartialViews = xml.Root.Element("PartialViews")?.Elements("View") ?? Enumerable.Empty(), - Templates = xml.Root.Element("Templates")?.Elements("Template") ?? Enumerable.Empty(), - Stylesheets = xml.Root.Element("Stylesheets")?.Elements("Stylesheet") ?? Enumerable.Empty(), - Scripts = xml.Root.Element("Scripts")?.Elements("Script") ?? Enumerable.Empty(), - DataTypes = xml.Root.Element("DataTypes")?.Elements("DataType") ?? Enumerable.Empty(), - Languages = xml.Root.Element("Languages")?.Elements("Language") ?? Enumerable.Empty(), - DictionaryItems = xml.Root.Element("DictionaryItems")?.Elements("DictionaryItem") ?? Enumerable.Empty(), - DocumentTypes = xml.Root.Element("DocumentTypes")?.Elements("DocumentType") ?? Enumerable.Empty(), - MediaTypes = xml.Root.Element("MediaTypes")?.Elements("MediaType") ?? Enumerable.Empty(), - Documents = xml.Root.Element("Documents")?.Elements("DocumentSet")?.Select(CompiledPackageContentBase.Create) ?? Enumerable.Empty(), - Media = xml.Root.Element("MediaItems")?.Elements()?.Select(CompiledPackageContentBase.Create) ?? Enumerable.Empty(), - }; - - def.Warnings = GetInstallWarnings(def); - - return def; + throw new ArgumentNullException(nameof(xml)); } - private InstallWarnings GetInstallWarnings(CompiledPackage package) + if (xml.Root == null) { - var installWarnings = new InstallWarnings - { - ConflictingMacros = _conflictingPackageData.FindConflictingMacros(package.Macros), - ConflictingTemplates = _conflictingPackageData.FindConflictingTemplates(package.Templates), - ConflictingStylesheets = _conflictingPackageData.FindConflictingStylesheets(package.Stylesheets) - }; - - return installWarnings; + throw new InvalidOperationException("The xml document is invalid"); } + if (xml.Root.Name != "umbPackage") + { + throw new FormatException("The xml document is invalid"); + } + + XElement? info = xml.Root.Element("info"); + if (info == null) + { + throw new FormatException("The xml document is invalid"); + } + + XElement? package = info.Element("package"); + if (package == null) + { + throw new FormatException("The xml document is invalid"); + } + + var def = new CompiledPackage + { + // will be null because we don't know where this data is coming from and + // this value is irrelevant during install. + PackageFile = null, + Name = package.Element("name")?.Value ?? string.Empty, + Macros = xml.Root.Element("Macros")?.Elements("macro") ?? Enumerable.Empty(), + MacroPartialViews = xml.Root.Element("MacroPartialViews")?.Elements("View") ?? Enumerable.Empty(), + PartialViews = xml.Root.Element("PartialViews")?.Elements("View") ?? Enumerable.Empty(), + Templates = xml.Root.Element("Templates")?.Elements("Template") ?? Enumerable.Empty(), + Stylesheets = xml.Root.Element("Stylesheets")?.Elements("Stylesheet") ?? Enumerable.Empty(), + Scripts = xml.Root.Element("Scripts")?.Elements("Script") ?? Enumerable.Empty(), + DataTypes = xml.Root.Element("DataTypes")?.Elements("DataType") ?? Enumerable.Empty(), + Languages = xml.Root.Element("Languages")?.Elements("Language") ?? Enumerable.Empty(), + DictionaryItems = + xml.Root.Element("DictionaryItems")?.Elements("DictionaryItem") ?? Enumerable.Empty(), + DocumentTypes = xml.Root.Element("DocumentTypes")?.Elements("DocumentType") ?? Enumerable.Empty(), + MediaTypes = xml.Root.Element("MediaTypes")?.Elements("MediaType") ?? Enumerable.Empty(), + Documents = + xml.Root.Element("Documents")?.Elements("DocumentSet")?.Select(CompiledPackageContentBase.Create) ?? + Enumerable.Empty(), + Media = xml.Root.Element("MediaItems")?.Elements()?.Select(CompiledPackageContentBase.Create) ?? + Enumerable.Empty(), + }; + + def.Warnings = GetInstallWarnings(def); + + return def; + } + + private InstallWarnings GetInstallWarnings(CompiledPackage package) + { + var installWarnings = new InstallWarnings + { + ConflictingMacros = _conflictingPackageData.FindConflictingMacros(package.Macros), + ConflictingTemplates = _conflictingPackageData.FindConflictingTemplates(package.Templates), + ConflictingStylesheets = _conflictingPackageData.FindConflictingStylesheets(package.Stylesheets), + }; + + return installWarnings; } } diff --git a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs index 239f1ba66d..d71eada618 100644 --- a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs +++ b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs @@ -1,64 +1,60 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Xml.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public class ConflictingPackageData { - public class ConflictingPackageData + private readonly IFileService _fileService; + private readonly IMacroService _macroService; + + public ConflictingPackageData(IMacroService macroService, IFileService fileService) { - private readonly IMacroService _macroService; - private readonly IFileService _fileService; - - public ConflictingPackageData(IMacroService macroService, IFileService fileService) - { - _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); - _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); - } - - public IEnumerable? FindConflictingStylesheets(IEnumerable? stylesheetNodes) - { - return stylesheetNodes? - .Select(n => - { - var xElement = n.Element("Name") ?? n.Element("name"); - if (xElement == null) - throw new FormatException("Missing \"Name\" element"); - - return _fileService.GetStylesheet(xElement.Value) as IFile; - }) - .Where(v => v != null); - } - - public IEnumerable? FindConflictingTemplates(IEnumerable? templateNodes) - { - return templateNodes? - .Select(n => - { - var xElement = n.Element("Alias") ?? n.Element("alias"); - if (xElement == null) - throw new FormatException("missing a \"Alias\" element"); - - return _fileService.GetTemplate(xElement.Value); - }) - .WhereNotNull(); - } - - public IEnumerable? FindConflictingMacros(IEnumerable? macroNodes) - { - return macroNodes? - .Select(n => - { - var xElement = n.Element("alias") ?? n.Element("Alias"); - if (xElement == null) - throw new FormatException("missing a \"alias\" element in alias element"); - - return _macroService.GetByAlias(xElement.Value); - }) - .Where(v => v != null); - } + _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); + _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); } + + public IEnumerable? FindConflictingStylesheets(IEnumerable? stylesheetNodes) => + stylesheetNodes? + .Select(n => + { + XElement? xElement = n.Element("Name") ?? n.Element("name"); + if (xElement == null) + { + throw new FormatException("Missing \"Name\" element"); + } + + return _fileService.GetStylesheet(xElement.Value) as IFile; + }) + .Where(v => v != null); + + public IEnumerable? FindConflictingTemplates(IEnumerable? templateNodes) => + templateNodes? + .Select(n => + { + XElement? xElement = n.Element("Alias") ?? n.Element("alias"); + if (xElement == null) + { + throw new FormatException("missing a \"Alias\" element"); + } + + return _fileService.GetTemplate(xElement.Value); + }) + .WhereNotNull(); + + public IEnumerable? FindConflictingMacros(IEnumerable? macroNodes) => + macroNodes? + .Select(n => + { + XElement? xElement = n.Element("alias") ?? n.Element("Alias"); + if (xElement == null) + { + throw new FormatException("missing a \"alias\" element in alias element"); + } + + return _macroService.GetByAlias(xElement.Value); + }) + .Where(v => v != null); } diff --git a/src/Umbraco.Core/Packaging/ICreatedPackagesRepository.cs b/src/Umbraco.Core/Packaging/ICreatedPackagesRepository.cs index ba99fdd9a7..3c873eb908 100644 --- a/src/Umbraco.Core/Packaging/ICreatedPackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/ICreatedPackagesRepository.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Manages the storage of created package definitions +/// +public interface ICreatedPackagesRepository : IPackageDefinitionRepository { /// - /// Manages the storage of created package definitions + /// Creates the package file and returns it's physical path /// - public interface ICreatedPackagesRepository : IPackageDefinitionRepository - { - /// - /// Creates the package file and returns it's physical path - /// - /// - string ExportPackage(PackageDefinition definition); - } + /// + string ExportPackage(PackageDefinition definition); } diff --git a/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs b/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs index fe015006a8..b66f4884af 100644 --- a/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs +++ b/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs @@ -1,22 +1,21 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Packaging; -namespace Umbraco.Cms.Core.Packaging +/// +/// Defines methods for persisting package definitions to storage +/// +public interface IPackageDefinitionRepository { - /// - /// Defines methods for persisting package definitions to storage - /// - public interface IPackageDefinitionRepository - { - IEnumerable GetAll(); - PackageDefinition? GetById(int id); - void Delete(int id); + IEnumerable GetAll(); - /// - /// Persists a package definition to storage - /// - /// - /// true if creating/updating the package was successful, otherwise false - /// - bool SavePackage(PackageDefinition definition); - } + PackageDefinition? GetById(int id); + + void Delete(int id); + + /// + /// Persists a package definition to storage + /// + /// + /// true if creating/updating the package was successful, otherwise false + /// + bool SavePackage(PackageDefinition definition); } diff --git a/src/Umbraco.Core/Packaging/IPackageInstallation.cs b/src/Umbraco.Core/Packaging/IPackageInstallation.cs index 9a744a91fa..7fc714bfdb 100644 --- a/src/Umbraco.Core/Packaging/IPackageInstallation.cs +++ b/src/Umbraco.Core/Packaging/IPackageInstallation.cs @@ -1,30 +1,28 @@ -using System.IO; using System.Xml.Linq; using Umbraco.Cms.Core.Models.Packaging; -namespace Umbraco.Cms.Core.Packaging -{ - public interface IPackageInstallation - { - /// - /// Installs a packages data and entities - /// - /// - /// - /// - /// - // TODO: The resulting PackageDefinition is only if we wanted to persist what was saved during package data installation. - // This used to be for the installedPackages.config but we don't have that anymore and don't really want it if we can help it. - // Possibly, we could continue to persist that file so that you could uninstall package data for an installed package in the - // back office (but it won't actually uninstall the package until you do that via nuget). If we want that functionality we'll have - // to restore a bunch of deleted code. - InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition); +namespace Umbraco.Cms.Core.Packaging; - /// - /// Reads the package xml and returns the model - /// - /// - /// - CompiledPackage ReadPackage(XDocument? packageXmlFile); - } +public interface IPackageInstallation +{ + /// + /// Installs a packages data and entities + /// + /// + /// + /// + /// + // TODO: The resulting PackageDefinition is only if we wanted to persist what was saved during package data installation. + // This used to be for the installedPackages.config but we don't have that anymore and don't really want it if we can help it. + // Possibly, we could continue to persist that file so that you could uninstall package data for an installed package in the + // back office (but it won't actually uninstall the package until you do that via nuget). If we want that functionality we'll have + // to restore a bunch of deleted code. + InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition); + + /// + /// Reads the package xml and returns the model + /// + /// + /// + CompiledPackage ReadPackage(XDocument? packageXmlFile); } diff --git a/src/Umbraco.Core/Packaging/InstallationSummary.cs b/src/Umbraco.Core/Packaging/InstallationSummary.cs index d5d7ad343b..d72ede1494 100644 --- a/src/Umbraco.Core/Packaging/InstallationSummary.cs +++ b/src/Umbraco.Core/Packaging/InstallationSummary.cs @@ -1,88 +1,97 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using System.Text; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Packaging; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +[Serializable] +[DataContract(IsReference = true)] +public class InstallationSummary { - [Serializable] - [DataContract(IsReference = true)] - public class InstallationSummary + public InstallationSummary(string packageName) + => PackageName = packageName; + + public string PackageName { get; } + + public InstallWarnings Warnings { get; set; } = new(); + + public IEnumerable DataTypesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable LanguagesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable DictionaryItemsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable MacrosInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable MacroPartialViewsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable TemplatesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable DocumentTypesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable MediaTypesInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable StylesheetsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable ScriptsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable PartialViewsInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable ContentInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable MediaInstalled { get; set; } = Enumerable.Empty(); + + public IEnumerable EntityContainersInstalled { get; set; } = Enumerable.Empty(); + + public override string ToString() { - public InstallationSummary(string packageName) - => PackageName = packageName; + var sb = new StringBuilder(); - public string PackageName { get; } - - public InstallWarnings Warnings { get; set; } = new InstallWarnings(); - - public IEnumerable DataTypesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable LanguagesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable DictionaryItemsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable MacrosInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable MacroPartialViewsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable TemplatesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable DocumentTypesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable MediaTypesInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable StylesheetsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable ScriptsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable PartialViewsInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable ContentInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable MediaInstalled { get; set; } = Enumerable.Empty(); - public IEnumerable EntityContainersInstalled { get; set; } = Enumerable.Empty(); - - public override string ToString() + void WriteConflicts(IEnumerable? source, Func selector, string message, bool appendLine = true) { - var sb = new StringBuilder(); - - void WriteConflicts(IEnumerable? source, Func selector, string message, bool appendLine = true) - { - var result = source?.Select(selector).ToList(); - if (result?.Count > 0) - { - sb.Append(message); - sb.Append(string.Join(", ", result)); - - if (appendLine) - { - sb.AppendLine(); - } - } - } - - void WriteCount(string message, IEnumerable source, bool appendLine = true) + var result = source?.Select(selector).ToList(); + if (result?.Count > 0) { sb.Append(message); - sb.Append(source?.Count() ?? 0); + sb.Append(string.Join(", ", result)); if (appendLine) { sb.AppendLine(); } } - - WriteConflicts(Warnings?.ConflictingMacros, x => x?.Alias, "Conflicting macros found, they will be overwritten: "); - WriteConflicts(Warnings?.ConflictingTemplates, x => x.Alias, "Conflicting templates found, they will be overwritten: "); - WriteConflicts(Warnings?.ConflictingStylesheets, x => x?.Alias, "Conflicting stylesheets found, they will be overwritten: "); - WriteCount("Data types installed: ", DataTypesInstalled); - WriteCount("Languages installed: ", LanguagesInstalled); - WriteCount("Dictionary items installed: ", DictionaryItemsInstalled); - WriteCount("Macros installed: ", MacrosInstalled); - WriteCount("Macro partial views installed: ", MacroPartialViewsInstalled); - WriteCount("Templates installed: ", TemplatesInstalled); - WriteCount("Document types installed: ", DocumentTypesInstalled); - WriteCount("Media types installed: ", MediaTypesInstalled); - WriteCount("Stylesheets installed: ", StylesheetsInstalled); - WriteCount("Scripts installed: ", ScriptsInstalled); - WriteCount("Partial views installed: ", PartialViewsInstalled); - WriteCount("Entity containers installed: ", EntityContainersInstalled); - WriteCount("Content items installed: ", ContentInstalled); - WriteCount("Media items installed: ", MediaInstalled, false); - - return sb.ToString(); } + + void WriteCount(string message, IEnumerable source, bool appendLine = true) + { + sb.Append(message); + sb.Append(source?.Count() ?? 0); + + if (appendLine) + { + sb.AppendLine(); + } + } + + WriteConflicts(Warnings?.ConflictingMacros, x => x?.Alias, "Conflicting macros found, they will be overwritten: "); + WriteConflicts(Warnings?.ConflictingTemplates, x => x.Alias, "Conflicting templates found, they will be overwritten: "); + WriteConflicts(Warnings?.ConflictingStylesheets, x => x?.Alias, "Conflicting stylesheets found, they will be overwritten: "); + WriteCount("Data types installed: ", DataTypesInstalled); + WriteCount("Languages installed: ", LanguagesInstalled); + WriteCount("Dictionary items installed: ", DictionaryItemsInstalled); + WriteCount("Macros installed: ", MacrosInstalled); + WriteCount("Macro partial views installed: ", MacroPartialViewsInstalled); + WriteCount("Templates installed: ", TemplatesInstalled); + WriteCount("Document types installed: ", DocumentTypesInstalled); + WriteCount("Media types installed: ", MediaTypesInstalled); + WriteCount("Stylesheets installed: ", StylesheetsInstalled); + WriteCount("Scripts installed: ", ScriptsInstalled); + WriteCount("Partial views installed: ", PartialViewsInstalled); + WriteCount("Entity containers installed: ", EntityContainersInstalled); + WriteCount("Content items installed: ", ContentInstalled); + WriteCount("Media items installed: ", MediaInstalled, false); + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Packaging/InstalledPackage.cs b/src/Umbraco.Core/Packaging/InstalledPackage.cs index ded901512b..3f3cc24a2a 100644 --- a/src/Umbraco.Core/Packaging/InstalledPackage.cs +++ b/src/Umbraco.Core/Packaging/InstalledPackage.cs @@ -1,36 +1,32 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +[DataContract(Name = "installedPackage")] +public class InstalledPackage { - [DataContract(Name = "installedPackage")] - public class InstalledPackage - { - [DataMember(Name = "name", IsRequired = true)] - [Required] - public string? PackageName { get; set; } + [DataMember(Name = "name", IsRequired = true)] + [Required] + public string? PackageName { get; set; } - // TODO: Version? Icon? Other metadata? This would need to come from querying the package on Our + // TODO: Version? Icon? Other metadata? This would need to come from querying the package on Our + [DataMember(Name = "packageView")] + public string? PackageView { get; set; } - [DataMember(Name = "packageView")] - public string? PackageView { get; set; } + [DataMember(Name = "plans")] + public IEnumerable PackageMigrationPlans { get; set; } = + Enumerable.Empty(); - [DataMember(Name = "plans")] - public IEnumerable PackageMigrationPlans { get; set; } = Enumerable.Empty(); - - /// - /// It the package contains any migrations at all - /// - [DataMember(Name = "hasMigrations")] - public bool HasMigrations => PackageMigrationPlans.Any(); - - /// - /// If the package has any pending migrations to run - /// - [DataMember(Name = "hasPendingMigrations")] - public bool HasPendingMigrations => PackageMigrationPlans.Any(x => x.HasPendingMigrations); - } + /// + /// It the package contains any migrations at all + /// + [DataMember(Name = "hasMigrations")] + public bool HasMigrations => PackageMigrationPlans.Any(); + /// + /// If the package has any pending migrations to run + /// + [DataMember(Name = "hasPendingMigrations")] + public bool HasPendingMigrations => PackageMigrationPlans.Any(x => x.HasPendingMigrations); } diff --git a/src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs b/src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs index 5aaca2e9f2..50cafd1d20 100644 --- a/src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs +++ b/src/Umbraco.Core/Packaging/InstalledPackageMigrationPlans.cs @@ -1,27 +1,25 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +[DataContract(Name = "installedPackageMigrations")] +public class InstalledPackageMigrationPlans { - [DataContract(Name = "installedPackageMigrations")] - public class InstalledPackageMigrationPlans - { - [DataMember(Name = "hasPendingMigrations")] - public bool HasPendingMigrations => FinalMigrationId != CurrentMigrationId; + [DataMember(Name = "hasPendingMigrations")] + public bool HasPendingMigrations => FinalMigrationId != CurrentMigrationId; - /// - /// If the package has migrations, this will be it's final migration Id - /// - /// - /// This can be used to determine if the package advertises any migrations - /// - [DataMember(Name = "finalMigrationId")] - public string? FinalMigrationId { get; set; } - - /// - /// If the package has migrations, this will be it's current migration Id - /// - [DataMember(Name = "currentMigrationId")] - public string? CurrentMigrationId { get; set; } - } + /// + /// If the package has migrations, this will be it's final migration Id + /// + /// + /// This can be used to determine if the package advertises any migrations + /// + [DataMember(Name = "finalMigrationId")] + public string? FinalMigrationId { get; set; } + /// + /// If the package has migrations, this will be it's current migration Id + /// + [DataMember(Name = "currentMigrationId")] + public string? CurrentMigrationId { get; set; } } diff --git a/src/Umbraco.Core/Packaging/PackageDefinition.cs b/src/Umbraco.Core/Packaging/PackageDefinition.cs index 66a0a9e102..7b0b5f5df4 100644 --- a/src/Umbraco.Core/Packaging/PackageDefinition.cs +++ b/src/Umbraco.Core/Packaging/PackageDefinition.cs @@ -1,79 +1,74 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -using Umbraco.Cms.Core.Models.Packaging; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// A created package in the back office. +/// +/// +/// This data structure is persisted to createdPackages.config when creating packages in the back office. +/// +[DataContract(Name = "packageInstance")] +public class PackageDefinition { + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "packageGuid")] + public Guid PackageId { get; set; } + + [DataMember(Name = "name")] + [Required] + public string Name { get; set; } = string.Empty; /// - /// A created package in the back office. + /// The full path to the package's XML file. /// - /// - /// This data structure is persisted to createdPackages.config when creating packages in the back office. - /// - [DataContract(Name = "packageInstance")] - public class PackageDefinition - { - [DataMember(Name = "id")] - public int Id { get; set; } + [ReadOnly(true)] + [DataMember(Name = "packagePath")] + public string PackagePath { get; set; } = string.Empty; - [DataMember(Name = "packageGuid")] - public Guid PackageId { get; set; } + [DataMember(Name = "contentLoadChildNodes")] + public bool ContentLoadChildNodes { get; set; } - [DataMember(Name = "name")] - [Required] - public string Name { get; set; } = string.Empty; + [DataMember(Name = "contentNodeId")] + public string? ContentNodeId { get; set; } - /// - /// The full path to the package's XML file. - /// - [ReadOnly(true)] - [DataMember(Name = "packagePath")] - public string PackagePath { get; set; } = string.Empty; + [DataMember(Name = "macros")] + public IList Macros { get; set; } = new List(); - [DataMember(Name = "contentLoadChildNodes")] - public bool ContentLoadChildNodes { get; set; } + [DataMember(Name = "languages")] + public IList Languages { get; set; } = new List(); - [DataMember(Name = "contentNodeId")] - public string? ContentNodeId { get; set; } + [DataMember(Name = "dictionaryItems")] + public IList DictionaryItems { get; set; } = new List(); - [DataMember(Name = "macros")] - public IList Macros { get; set; } = new List(); + [DataMember(Name = "templates")] + public IList Templates { get; set; } = new List(); - [DataMember(Name = "languages")] - public IList Languages { get; set; } = new List(); + [DataMember(Name = "partialViews")] + public IList PartialViews { get; set; } = new List(); - [DataMember(Name = "dictionaryItems")] - public IList DictionaryItems { get; set; } = new List(); + [DataMember(Name = "documentTypes")] + public IList DocumentTypes { get; set; } = new List(); - [DataMember(Name = "templates")] - public IList Templates { get; set; } = new List(); + [DataMember(Name = "mediaTypes")] + public IList MediaTypes { get; set; } = new List(); - [DataMember(Name = "partialViews")] - public IList PartialViews { get; set; } = new List(); + [DataMember(Name = "stylesheets")] + public IList Stylesheets { get; set; } = new List(); - [DataMember(Name = "documentTypes")] - public IList DocumentTypes { get; set; } = new List(); + [DataMember(Name = "scripts")] + public IList Scripts { get; set; } = new List(); - [DataMember(Name = "mediaTypes")] - public IList MediaTypes { get; set; } = new List(); + [DataMember(Name = "dataTypes")] + public IList DataTypes { get; set; } = new List(); - [DataMember(Name = "stylesheets")] - public IList Stylesheets { get; set; } = new List(); + [DataMember(Name = "mediaUdis")] + public IList MediaUdis { get; set; } = new List(); - [DataMember(Name = "scripts")] - public IList Scripts { get; set; } = new List(); - - [DataMember(Name = "dataTypes")] - public IList DataTypes { get; set; } = new List(); - - [DataMember(Name = "mediaUdis")] - public IList MediaUdis { get; set; } = new List(); - - [DataMember(Name = "mediaLoadChildNodes")] - public bool MediaLoadChildNodes { get; set; } - } + [DataMember(Name = "mediaLoadChildNodes")] + public bool MediaLoadChildNodes { get; set; } } diff --git a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs index df5375ad92..99a18dbcf9 100644 --- a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs +++ b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs @@ -1,84 +1,104 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Xml.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Converts a to and from XML +/// +public class PackageDefinitionXmlParser { - /// - /// Converts a to and from XML - /// - public class PackageDefinitionXmlParser + private static readonly IList EmptyStringList = new List(); + private static readonly IList EmptyGuidUdiList = new List(); + + public PackageDefinition? ToPackageDefinition(XElement xml) { - private static readonly IList s_emptyStringList = new List(); - private static readonly IList s_emptyGuidUdiList = new List(); - - - public PackageDefinition? ToPackageDefinition(XElement xml) + if (xml == null) { - if (xml == null) - { - return null; - } - - var retVal = new PackageDefinition - { - Id = xml.AttributeValue("id"), - Name = xml.AttributeValue("name") ?? string.Empty, - PackagePath = xml.AttributeValue("packagePath") ?? string.Empty, - PackageId = xml.AttributeValue("packageGuid"), - ContentNodeId = xml.Element("content")?.AttributeValue("nodeId") ?? string.Empty, - ContentLoadChildNodes = xml.Element("content")?.AttributeValue("loadChildNodes") ?? false, - MediaUdis = xml.Element("media")?.Elements("nodeUdi").Select(x => (GuidUdi)UdiParser.Parse(x.Value)).ToList() ?? s_emptyGuidUdiList, - MediaLoadChildNodes = xml.Element("media")?.AttributeValue("loadChildNodes") ?? false, - Macros = xml.Element("macros")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - Templates = xml.Element("templates")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - Stylesheets = xml.Element("stylesheets")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - Scripts = xml.Element("scripts")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - PartialViews = xml.Element("partialViews")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - DocumentTypes = xml.Element("documentTypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - MediaTypes = xml.Element("mediaTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - Languages = xml.Element("languages")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - DictionaryItems = xml.Element("dictionaryitems")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - DataTypes = xml.Element("datatypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, - }; - - return retVal; + return null; } - public XElement ToXml(PackageDefinition def) + var retVal = new PackageDefinition { - var packageXml = new XElement("package", - new XAttribute("id", def.Id), - new XAttribute("name", def.Name ?? string.Empty), - new XAttribute("packagePath", def.PackagePath ?? string.Empty), - new XAttribute("packageGuid", def.PackageId), - new XElement("datatypes", string.Join(",", def.DataTypes ?? Array.Empty())), - - new XElement("content", - new XAttribute("nodeId", def.ContentNodeId ?? string.Empty), - new XAttribute("loadChildNodes", def.ContentLoadChildNodes)), - - new XElement("templates", string.Join(",", def.Templates ?? Array.Empty())), - new XElement("stylesheets", string.Join(",", def.Stylesheets ?? Array.Empty())), - new XElement("scripts", string.Join(",", def.Scripts ?? Array.Empty())), - new XElement("partialViews", string.Join(",", def.PartialViews ?? Array.Empty())), - new XElement("documentTypes", string.Join(",", def.DocumentTypes ?? Array.Empty())), - new XElement("mediaTypes", string.Join(",", def.MediaTypes ?? Array.Empty())), - new XElement("macros", string.Join(",", def.Macros ?? Array.Empty())), - new XElement("languages", string.Join(",", def.Languages ?? Array.Empty())), - new XElement("dictionaryitems", string.Join(",", def.DictionaryItems ?? Array.Empty())), - - new XElement( - "media", - def.MediaUdis.Select(x => (object)new XElement("nodeUdi", x)) - .Union(new[] { new XAttribute("loadChildNodes", def.MediaLoadChildNodes) })) - ); - return packageXml; - } + Id = xml.AttributeValue("id"), + Name = xml.AttributeValue("name") ?? string.Empty, + PackagePath = xml.AttributeValue("packagePath") ?? string.Empty, + PackageId = xml.AttributeValue("packageGuid"), + ContentNodeId = xml.Element("content")?.AttributeValue("nodeId") ?? string.Empty, + ContentLoadChildNodes = xml.Element("content")?.AttributeValue("loadChildNodes") ?? false, + MediaUdis = + xml.Element("media")?.Elements("nodeUdi").Select(x => (GuidUdi)UdiParser.Parse(x.Value)).ToList() ?? + EmptyGuidUdiList, + MediaLoadChildNodes = xml.Element("media")?.AttributeValue("loadChildNodes") ?? false, + Macros = + xml.Element("macros")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + Templates = + xml.Element("templates")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + Stylesheets = + xml.Element("stylesheets")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + Scripts = + xml.Element("scripts")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + PartialViews = + xml.Element("partialViews")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + DocumentTypes = + xml.Element("documentTypes")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + MediaTypes = + xml.Element("mediaTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .ToList() ?? EmptyStringList, + Languages = + xml.Element("languages")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + DictionaryItems = + xml.Element("dictionaryitems")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + DataTypes = xml.Element("datatypes")?.Value + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? + EmptyStringList, + }; + return retVal; + } + public XElement ToXml(PackageDefinition def) + { + var packageXml = new XElement( + "package", + new XAttribute("id", def.Id), + new XAttribute("name", def.Name ?? string.Empty), + new XAttribute("packagePath", def.PackagePath ?? string.Empty), + new XAttribute("packageGuid", def.PackageId), + new XElement("datatypes", string.Join(",", def.DataTypes ?? Array.Empty())), + new XElement( + "content", + new XAttribute("nodeId", def.ContentNodeId ?? string.Empty), + new XAttribute("loadChildNodes", def.ContentLoadChildNodes)), + new XElement("templates", string.Join(",", def.Templates ?? Array.Empty())), + new XElement("stylesheets", string.Join(",", def.Stylesheets ?? Array.Empty())), + new XElement("scripts", string.Join(",", def.Scripts ?? Array.Empty())), + new XElement("partialViews", string.Join(",", def.PartialViews ?? Array.Empty())), + new XElement("documentTypes", string.Join(",", def.DocumentTypes ?? Array.Empty())), + new XElement("mediaTypes", string.Join(",", def.MediaTypes ?? Array.Empty())), + new XElement("macros", string.Join(",", def.Macros ?? Array.Empty())), + new XElement("languages", string.Join(",", def.Languages ?? Array.Empty())), + new XElement("dictionaryitems", string.Join(",", def.DictionaryItems ?? Array.Empty())), + new XElement( + "media", + def.MediaUdis.Select(x => (object)new XElement("nodeUdi", x)) + .Union(new[] { new XAttribute("loadChildNodes", def.MediaLoadChildNodes) }))); + return packageXml; } } diff --git a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs index b972a2cf08..0d72cad38a 100644 --- a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs +++ b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs @@ -1,127 +1,118 @@ -using System; -using System.IO; using System.IO.Compression; using System.Reflection; -using System.Security.Cryptography; -using System.Text; using System.Xml; using System.Xml.Linq; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public static class PackageMigrationResource { - public static class PackageMigrationResource + public static XDocument? GetEmbeddedPackageDataManifest(Type planType, out ZipArchive? zipArchive) { - private static Stream? GetEmbeddedPackageZipStream(Type planType) + XDocument? packageXml; + Stream? zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) { - // lookup the embedded resource by convention - Assembly currentAssembly = planType.Assembly; - var fileName = $"{planType.Namespace}.package.zip"; - Stream? stream = currentAssembly.GetManifestResourceStream(fileName); - - return stream; - } - - public static XDocument? GetEmbeddedPackageDataManifest(Type planType, out ZipArchive? zipArchive) - { - XDocument? packageXml; - var zipStream = GetEmbeddedPackageZipStream(planType); - if (zipStream is not null) - { - zipArchive = GetPackageDataManifest(zipStream, out packageXml); - return packageXml; - } - - zipArchive = null; - packageXml = GetEmbeddedPackageXmlDoc(planType); + zipArchive = GetPackageDataManifest(zipStream, out packageXml); return packageXml; } - public static XDocument? GetEmbeddedPackageDataManifest(Type planType) + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml; + } + + private static Stream? GetEmbeddedPackageZipStream(Type planType) + { + // lookup the embedded resource by convention + Assembly currentAssembly = planType.Assembly; + var fileName = $"{planType.Namespace}.package.zip"; + Stream? stream = currentAssembly.GetManifestResourceStream(fileName); + + return stream; + } + + public static XDocument? GetEmbeddedPackageDataManifest(Type planType) => + GetEmbeddedPackageDataManifest(planType, out _); + + public static string GetEmbeddedPackageDataManifestHash(Type planType) + { + // SEE: HashFromStreams in the benchmarks project for how fast this is. It will run + // on every startup for every embedded package.zip. The bigger the zip, the more time it takes. + // But it is still very fast ~303ms for a 100MB file. This will only be an issue if there are + // several very large package.zips. + using Stream? stream = GetEmbeddedPackageZipStream(planType); + + if (stream is not null) { - return GetEmbeddedPackageDataManifest(planType, out _); + return stream.GetStreamHash(); } - private static XDocument? GetEmbeddedPackageXmlDoc(Type planType) + XDocument? xml = GetEmbeddedPackageXmlDoc(planType); + + if (xml is not null) { - // lookup the embedded resource by convention - Assembly currentAssembly = planType.Assembly; - var fileName = $"{planType.Namespace}.package.xml"; - Stream? stream = currentAssembly.GetManifestResourceStream(fileName); - if (stream == null) - { - return null; - } - XDocument xml; - using (stream) - { - xml = XDocument.Load(stream); - } - return xml; + return xml.ToString(); } - public static string GetEmbeddedPackageDataManifestHash(Type planType) + throw new IOException("Missing embedded files for planType: " + planType); + } + + private static XDocument? GetEmbeddedPackageXmlDoc(Type planType) + { + // lookup the embedded resource by convention + Assembly currentAssembly = planType.Assembly; + var fileName = $"{planType.Namespace}.package.xml"; + Stream? stream = currentAssembly.GetManifestResourceStream(fileName); + if (stream == null) { - // SEE: HashFromStreams in the benchmarks project for how fast this is. It will run - // on every startup for every embedded package.zip. The bigger the zip, the more time it takes. - // But it is still very fast ~303ms for a 100MB file. This will only be an issue if there are - // several very large package.zips. - - using Stream? stream = GetEmbeddedPackageZipStream(planType); - - if (stream is not null) - { - return stream.GetStreamHash(); - } - - var xml = GetEmbeddedPackageXmlDoc(planType); - - if (xml is not null) - { - return xml.ToString(); - } - - throw new IOException("Missing embedded files for planType: " + planType); + return null; } - public static bool TryGetEmbeddedPackageDataManifest(Type planType, out XDocument? packageXml, out ZipArchive? zipArchive) + XDocument xml; + using (stream) { - var zipStream = GetEmbeddedPackageZipStream(planType); - if (zipStream is not null) - { - zipArchive = GetPackageDataManifest(zipStream, out packageXml); - return true; - } - - zipArchive = null; - packageXml = GetEmbeddedPackageXmlDoc(planType); - return packageXml is not null; + xml = XDocument.Load(stream); } - public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) + return xml; + } + + public static bool TryGetEmbeddedPackageDataManifest(Type planType, out XDocument? packageXml, out ZipArchive? zipArchive) + { + Stream? zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) { - if (packageZipStream == null) - { - throw new ArgumentNullException(nameof(packageZipStream)); - } - - var zip = new ZipArchive(packageZipStream, ZipArchiveMode.Read); - ZipArchiveEntry? packageXmlEntry = zip.GetEntry("package.xml"); - if (packageXmlEntry == null) - { - throw new InvalidOperationException("Zip package does not contain the required package.xml file"); - } - - using (Stream packageXmlStream = packageXmlEntry.Open()) - using (var xmlReader = XmlReader.Create(packageXmlStream, new XmlReaderSettings - { - IgnoreWhitespace = true - })) - { - packageXml = XDocument.Load(xmlReader); - } - - return zip; + zipArchive = GetPackageDataManifest(zipStream, out packageXml); + return true; } + + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml is not null; + } + + public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) + { + if (packageZipStream == null) + { + throw new ArgumentNullException(nameof(packageZipStream)); + } + + var zip = new ZipArchive(packageZipStream, ZipArchiveMode.Read); + ZipArchiveEntry? packageXmlEntry = zip.GetEntry("package.xml"); + if (packageXmlEntry == null) + { + throw new InvalidOperationException("Zip package does not contain the required package.xml file"); + } + + using (Stream packageXmlStream = packageXmlEntry.Open()) + using (var xmlReader = XmlReader.Create(packageXmlStream, new XmlReaderSettings { IgnoreWhitespace = true })) + { + packageXml = XDocument.Load(xmlReader); + } + + return zip; } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 174faa37fd..a5982aef7e 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Xml.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -15,749 +11,848 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; using File = System.IO.File; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Manages the storage of installed/created package definitions +/// +[Obsolete( + "Packages have now been moved to the database instead of local files, please use CreatedPackageSchemaRepository instead")] +public class PackagesRepository : ICreatedPackagesRepository { + private readonly IContentService _contentService; + private readonly IContentTypeService _contentTypeService; + private readonly string _createdPackagesFolderPath; + private readonly IDataTypeService _dataTypeService; + private readonly IFileService _fileService; + private readonly FileSystems _fileSystems; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizationService _languageService; + private readonly IMacroService _macroService; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + private readonly string _packageRepositoryFileName; + private readonly string _packagesFolderPath; + private readonly PackageDefinitionXmlParser _parser; + private readonly IEntityXmlSerializer _serializer; + private readonly string _tempFolderPath; + /// - /// Manages the storage of installed/created package definitions + /// Constructor /// - [Obsolete("Packages have now been moved to the database instead of local files, please use CreatedPackageSchemaRepository instead")] - public class PackagesRepository : ICreatedPackagesRepository + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The file name for storing the package definitions (i.e. "createdPackages.config") + /// + /// + /// + /// + /// + /// + /// + /// + public PackagesRepository( + IContentService contentService, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IFileService fileService, + IMacroService macroService, + ILocalizationService languageService, + IHostingEnvironment hostingEnvironment, + IEntityXmlSerializer serializer, + IOptions globalSettings, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + MediaFileManager mediaFileManager, + FileSystems fileSystems, + string packageRepositoryFileName, + string? tempFolderPath = null, + string? packagesFolderPath = null, + string? mediaFolderPath = null) { - private readonly IContentService _contentService; - private readonly IContentTypeService _contentTypeService; - private readonly IDataTypeService _dataTypeService; - private readonly IFileService _fileService; - private readonly IMacroService _macroService; - private readonly ILocalizationService _languageService; - private readonly IEntityXmlSerializer _serializer; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly string _packageRepositoryFileName; - private readonly string _createdPackagesFolderPath; - private readonly string _packagesFolderPath; - private readonly string _tempFolderPath; - private readonly PackageDefinitionXmlParser _parser; - private readonly IMediaService _mediaService; - private readonly IMediaTypeService _mediaTypeService; - private readonly MediaFileManager _mediaFileManager; - private readonly FileSystems _fileSystems; - - /// - /// Constructor - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// The file name for storing the package definitions (i.e. "createdPackages.config") - /// - /// - /// - /// - public PackagesRepository( - IContentService contentService, - IContentTypeService contentTypeService, - IDataTypeService dataTypeService, - IFileService fileService, - IMacroService macroService, - ILocalizationService languageService, - IHostingEnvironment hostingEnvironment, - IEntityXmlSerializer serializer, - IOptions globalSettings, - IMediaService mediaService, - IMediaTypeService mediaTypeService, - MediaFileManager mediaFileManager, - FileSystems fileSystems, - string packageRepositoryFileName, - string? tempFolderPath = null, - string? packagesFolderPath = null, - string? mediaFolderPath = null) + if (string.IsNullOrWhiteSpace(packageRepositoryFileName)) { - if (string.IsNullOrWhiteSpace(packageRepositoryFileName)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(packageRepositoryFileName)); - _contentService = contentService; - _contentTypeService = contentTypeService; - _dataTypeService = dataTypeService; - _fileService = fileService; - _macroService = macroService; - _languageService = languageService; - _serializer = serializer; - _hostingEnvironment = hostingEnvironment; - _packageRepositoryFileName = packageRepositoryFileName; - - _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; - _packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages; - _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; - - _parser = new PackageDefinitionXmlParser(); - _mediaService = mediaService; - _mediaTypeService = mediaTypeService; - _mediaFileManager = mediaFileManager; - _fileSystems = fileSystems; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(packageRepositoryFileName)); } - private string CreatedPackagesFile => _packagesFolderPath.EnsureEndsWith('/') + _packageRepositoryFileName; + _contentService = contentService; + _contentTypeService = contentTypeService; + _dataTypeService = dataTypeService; + _fileService = fileService; + _macroService = macroService; + _languageService = languageService; + _serializer = serializer; + _hostingEnvironment = hostingEnvironment; + _packageRepositoryFileName = packageRepositoryFileName; - public IEnumerable GetAll() + _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; + _packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages; + _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; + + _parser = new PackageDefinitionXmlParser(); + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + _mediaFileManager = mediaFileManager; + _fileSystems = fileSystems; + } + + private string CreatedPackagesFile => _packagesFolderPath.EnsureEndsWith('/') + _packageRepositoryFileName; + + public IEnumerable GetAll() + { + XDocument packagesXml = EnsureStorage(out _); + if (packagesXml?.Root == null) { - var packagesXml = EnsureStorage(out _); - if (packagesXml?.Root == null) - yield break; - - foreach (var packageXml in packagesXml.Root.Elements("package")) - yield return _parser.ToPackageDefinition(packageXml); + yield break; } - public PackageDefinition? GetById(int id) + foreach (XElement packageXml in packagesXml.Root.Elements("package")) { - var packagesXml = EnsureStorage(out var packageFile); - var packageXml = packagesXml?.Root?.Elements("package").FirstOrDefault(x => x.AttributeValue("id") == id); - return packageXml == null ? null : _parser.ToPackageDefinition(packageXml); + yield return _parser.ToPackageDefinition(packageXml); + } + } + + public PackageDefinition? GetById(int id) + { + XDocument packagesXml = EnsureStorage(out var packageFile); + XElement? packageXml = packagesXml?.Root?.Elements("package") + .FirstOrDefault(x => x.AttributeValue("id") == id); + return packageXml == null ? null : _parser.ToPackageDefinition(packageXml); + } + + public void Delete(int id) + { + XDocument packagesXml = EnsureStorage(out var packagesFile); + XElement? packageXml = packagesXml?.Root?.Elements("package") + .FirstOrDefault(x => x.AttributeValue("id") == id); + if (packageXml == null) + { + return; } - public void Delete(int id) + packageXml.Remove(); + + packagesXml?.Save(packagesFile); + } + + public bool SavePackage(PackageDefinition definition) + { + if (definition == null) { - var packagesXml = EnsureStorage(out var packagesFile); - var packageXml = packagesXml?.Root?.Elements("package").FirstOrDefault(x => x.AttributeValue("id") == id); + throw new ArgumentNullException(nameof(definition)); + } + + XDocument packagesXml = EnsureStorage(out var packagesFile); + + if (packagesXml?.Root == null) + { + return false; + } + + // ensure it's valid + ValidatePackage(definition); + + if (definition.Id == default) + { + // need to gen an id and persist + // Find max id + var maxId = packagesXml.Root.Elements("package").Max(x => x.AttributeValue("id")) ?? 0; + var newId = maxId + 1; + definition.Id = newId; + definition.PackageId = definition.PackageId == default ? Guid.NewGuid() : definition.PackageId; + XElement packageXml = _parser.ToXml(definition); + packagesXml.Root.Add(packageXml); + } + else + { + // existing + XElement? packageXml = packagesXml.Root.Elements("package") + .FirstOrDefault(x => x.AttributeValue("id") == definition.Id); if (packageXml == null) - return; + { + return false; + } - packageXml.Remove(); - - packagesXml?.Save(packagesFile); + XElement updatedXml = _parser.ToXml(definition); + packageXml.ReplaceWith(updatedXml); } - public bool SavePackage(PackageDefinition definition) + packagesXml.Save(packagesFile); + + return true; + } + + public string ExportPackage(PackageDefinition definition) + { + if (definition.Id == default) { - if (definition == null) - throw new ArgumentNullException(nameof(definition)); + throw new ArgumentException( + "The package definition does not have an ID, it must be saved before being exported"); + } - var packagesXml = EnsureStorage(out var packagesFile); + if (definition.PackageId == default) + { + throw new ArgumentException( + "the package definition does not have a GUID, it must be saved before being exported"); + } - if (packagesXml?.Root == null) - return false; + // ensure it's valid + ValidatePackage(definition); - //ensure it's valid - ValidatePackage(definition); + // Create a folder for building this package + var temporaryPath = + _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid()); + if (Directory.Exists(temporaryPath) == false) + { + Directory.CreateDirectory(temporaryPath); + } - if (definition.Id == default) + try + { + // Init package file + XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + + // Info section + root.Add(GetPackageInfoXml(definition)); + + PackageDocumentsAndTags(definition, root); + PackageDocumentTypes(definition, root); + PackageMediaTypes(definition, root); + PackageTemplates(definition, root); + PackageStylesheets(definition, root); + PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem); + PackageMacros(definition, root); + PackageDictionaryItems(definition, root); + PackageLanguages(definition, root); + PackageDataTypes(definition, root); + Dictionary mediaFiles = PackageMedia(definition, root); + + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) { - //need to gen an id and persist - // Find max id - var maxId = packagesXml.Root.Elements("package").Max(x => x.AttributeValue("id")) ?? 0; - var newId = maxId + 1; - definition.Id = newId; - definition.PackageId = definition.PackageId == default ? Guid.NewGuid() : definition.PackageId; - var packageXml = _parser.ToXml(definition); - packagesXml.Root.Add(packageXml); + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + { + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) + { + compiledPackageXml.Save(entryStream); + } + + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) + { + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); + } + } + } } else { - //existing - var packageXml = packagesXml.Root.Elements("package").FirstOrDefault(x => x.AttributeValue("id") == definition.Id); - if (packageXml == null) - return false; + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); - var updatedXml = _parser.ToXml(definition); - packageXml.ReplaceWith(updatedXml); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); + } } - packagesXml.Save(packagesFile); + var directoryName = + _hostingEnvironment.MapPathContentRoot(Path.Combine( + _createdPackagesFolderPath, + definition.Name.Replace(' ', '_'))); + Directory.CreateDirectory(directoryName); - return true; + var finalPackagePath = Path.Combine(directoryName, fileName); + + if (File.Exists(finalPackagePath)) + { + File.Delete(finalPackagePath); + } + + File.Move(tempPackagePath, finalPackagePath); + + definition.PackagePath = finalPackagePath; + SavePackage(definition); + + return finalPackagePath; + } + finally + { + // Clean up + Directory.Delete(temporaryPath, true); + } + } + + public void DeleteLocalRepositoryFiles() + { + var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); + if (File.Exists(packagesFile)) + { + File.Delete(packagesFile); } - public string ExportPackage(PackageDefinition definition) + var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); + if (Directory.Exists(packagesFolder)) { - if (definition.Id == default) - throw new ArgumentException("The package definition does not have an ID, it must be saved before being exported"); - if (definition.PackageId == default) - throw new ArgumentException("the package definition does not have a GUID, it must be saved before being exported"); + Directory.Delete(packagesFolder); + } + } - //ensure it's valid - ValidatePackage(definition); + private static XElement GetPackageInfoXml(PackageDefinition definition) + { + var info = new XElement("info"); - //Create a folder for building this package - var temporaryPath = _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid()); - if (Directory.Exists(temporaryPath) == false) + // Package info + var package = new XElement("package"); + package.Add(new XElement("name", definition.Name)); + info.Add(package); + return info; + } + + private void ValidatePackage(PackageDefinition definition) + { + // ensure it's valid + var context = new ValidationContext(definition, null, null); + var results = new List(); + var isValid = Validator.TryValidateObject(definition, context, results); + if (!isValid) + { + throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + + string.Join(", ", results.Select(x => x.ErrorMessage))); + } + } + + private void PackageDataTypes(PackageDefinition definition, XContainer root) + { + var dataTypes = new XElement("DataTypes"); + foreach (var dtId in definition.DataTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - Directory.CreateDirectory(temporaryPath); + continue; } - try + IDataType? dataType = _dataTypeService.GetDataType(outInt); + if (dataType == null) { - //Init package file - XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + continue; + } - //Info section - root.Add(GetPackageInfoXml(definition)); + dataTypes.Add(_serializer.Serialize(dataType)); + } - PackageDocumentsAndTags(definition, root); - PackageDocumentTypes(definition, root); - PackageMediaTypes(definition, root); - PackageTemplates(definition, root); - PackageStylesheets(definition, root); - PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem); - PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem); - PackageMacros(definition, root); - PackageDictionaryItems(definition, root); - PackageLanguages(definition, root); - PackageDataTypes(definition, root); - Dictionary mediaFiles = PackageMedia(definition, root); + root.Add(dataTypes); + } - string fileName; - string tempPackagePath; - if (mediaFiles.Count > 0) + private void PackageLanguages(PackageDefinition definition, XContainer root) + { + var languages = new XElement("Languages"); + foreach (var langId in definition.Languages) + { + if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + ILanguage? lang = _languageService.GetLanguageById(outInt); + if (lang == null) + { + continue; + } + + languages.Add(_serializer.Serialize(lang)); + } + + root.Add(languages); + } + + private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + { + var rootDictionaryItems = new XElement("DictionaryItems"); + var items = new Dictionary(); + + foreach (var dictionaryId in definition.DictionaryItems) + { + if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDictionaryItem? di = _languageService.GetDictionaryItemById(outInt); + + if (di == null) + { + continue; + } + + items[di.Key] = (di, _serializer.Serialize(di, false)); + } + + // organize them in hierarchy ... + var itemCount = items.Count; + var processed = new Dictionary(); + while (processed.Count < itemCount) + { + foreach (Guid key in items.Keys.ToList()) + { + (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; + + if (!dictionaryItem.ParentId.HasValue) { - fileName = "package.zip"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) - using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) - { - ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); - using (Stream entryStream = packageXmlEntry.Open()) - { - compiledPackageXml.Save(entryStream); - } - - foreach (KeyValuePair mediaFile in mediaFiles) - { - var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); - using (Stream entryStream = mediaEntry.Open()) - using (mediaFile.Value) - { - mediaFile.Value.Seek(0, SeekOrigin.Begin); - mediaFile.Value.CopyTo(entryStream); - } - } - } + // if it has no parent, its definitely just at the root + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); } else { - fileName = "package.xml"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + if (processed.ContainsKey(dictionaryItem.ParentId.Value)) { - compiledPackageXml.Save(fileStream); + // we've processed this parent element already so we can just append this xml child to it + AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, serializedDictionaryValue); } - } - - var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_'))); - Directory.CreateDirectory(directoryName); - - var finalPackagePath = Path.Combine(directoryName, fileName); - - if (File.Exists(finalPackagePath)) - { - File.Delete(finalPackagePath); - } - - File.Move(tempPackagePath, finalPackagePath); - - definition.PackagePath = finalPackagePath; - SavePackage(definition); - - return finalPackagePath; - } - finally - { - // Clean up - Directory.Delete(temporaryPath, true); - } - } - - private void ValidatePackage(PackageDefinition definition) - { - // ensure it's valid - var context = new ValidationContext(definition, serviceProvider: null, items: null); - var results = new List(); - var isValid = Validator.TryValidateObject(definition, context, results); - if (!isValid) - throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + string.Join(", ", results.Select(x => x.ErrorMessage))); - } - - private void PackageDataTypes(PackageDefinition definition, XContainer root) - { - var dataTypes = new XElement("DataTypes"); - foreach (var dtId in definition.DataTypes) - { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var dataType = _dataTypeService.GetDataType(outInt); - if (dataType == null) - continue; - dataTypes.Add(_serializer.Serialize(dataType)); - } - root.Add(dataTypes); - } - - private void PackageLanguages(PackageDefinition definition, XContainer root) - { - var languages = new XElement("Languages"); - foreach (var langId in definition.Languages) - { - if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var lang = _languageService.GetLanguageById(outInt); - if (lang == null) - continue; - languages.Add(_serializer.Serialize(lang)); - } - root.Add(languages); - } - - private void PackageDictionaryItems(PackageDefinition definition, XContainer root) - { - var rootDictionaryItems = new XElement("DictionaryItems"); - var items = new Dictionary(); - - foreach (var dictionaryId in definition.DictionaryItems) - { - if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IDictionaryItem? di = _languageService.GetDictionaryItemById(outInt); - - if (di == null) - { - continue; - } - - items[di.Key] = (di, _serializer.Serialize(di, false)); - } - - // organize them in hierarchy ... - var itemCount = items.Count; - var processed = new Dictionary(); - while (processed.Count < itemCount) - { - foreach (Guid key in items.Keys.ToList()) - { - (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; - - if (!dictionaryItem.ParentId.HasValue) + else if (items.ContainsKey(dictionaryItem.ParentId.Value)) { - // if it has no parent, its definitely just at the root - AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + // we know the parent exists in the dictionary but + // we haven't processed it yet so we'll leave it for the next loop } else { - if (processed.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we've processed this parent element already so we can just append this xml child to it - AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, serializedDictionaryValue); - } - else if (items.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we know the parent exists in the dictionary but - // we haven't processed it yet so we'll leave it for the next loop - continue; - } - else - { - // in this case, the parent of this item doesn't exist in our collection, we have no - // choice but to add it to the root. - AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); - } + // in this case, the parent of this item doesn't exist in our collection, we have no + // choice but to add it to the root. + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); } } } + } - root.Add(rootDictionaryItems); + root.Add(rootDictionaryItems); - static void AppendDictionaryElement(XElement rootDictionaryItems, Dictionary items, Dictionary processed, Guid key, XElement serializedDictionaryValue) + static void AppendDictionaryElement( + XElement rootDictionaryItems, + Dictionary items, + Dictionary processed, + Guid key, + XElement serializedDictionaryValue) + { + // track it + processed.Add(key, serializedDictionaryValue); + + // append it + rootDictionaryItems.Add(serializedDictionaryValue); + + // remove it so its not re-processed + items.Remove(key); + } + } + + private void PackageMacros(PackageDefinition definition, XContainer root) + { + var packagedMacros = new List(); + var macros = new XElement("Macros"); + foreach (var macroId in definition.Macros) + { + if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - // track it - processed.Add(key, serializedDictionaryValue); - // append it - rootDictionaryItems.Add(serializedDictionaryValue); - // remove it so its not re-processed - items.Remove(key); + continue; + } + + XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); + if (macroXml == null) + { + continue; + } + + macros.Add(macroXml); + if (macro is not null) + { + packagedMacros.Add(macro); } } - private void PackageMacros(PackageDefinition definition, XContainer root) + root.Add(macros); + + // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) + IEnumerable views = packagedMacros.Where(x => x.MacroSource is not null) + .Where(x => x.MacroSource!.StartsWith(Constants.SystemDirectories.MacroPartials)) + .Select(x => x.MacroSource![Constants.SystemDirectories.MacroPartials.Length..].Replace('/', '\\')); + PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem); + } + + private void PackageStylesheets(PackageDefinition definition, XContainer root) + { + var stylesheetsXml = new XElement("Stylesheets"); + foreach (var stylesheet in definition.Stylesheets) { - var packagedMacros = new List(); - var macros = new XElement("Macros"); - foreach (var macroId in definition.Macros) + if (stylesheet.IsNullOrWhiteSpace()) { - if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt)) - { - continue; - } - - XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); - if (macroXml == null) - { - continue; - } - - macros.Add(macroXml); - if (macro is not null) - { - packagedMacros.Add(macro); - } + continue; } - root.Add(macros); - - // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) - IEnumerable views = packagedMacros.Where(x => x.MacroSource is not null).Where(x => x.MacroSource!.StartsWith(Constants.SystemDirectories.MacroPartials)) - .Select(x => x.MacroSource!.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\')); - PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem); + XElement? xml = GetStylesheetXml(stylesheet, true); + if (xml is not null) + { + stylesheetsXml.Add(xml); + } } - private void PackageStylesheets(PackageDefinition definition, XContainer root) - { - var stylesheetsXml = new XElement("Stylesheets"); - foreach (var stylesheet in definition.Stylesheets) - { - if (stylesheet.IsNullOrWhiteSpace()) - { - continue; - } + root.Add(stylesheetsXml); + } - XElement? xml = GetStylesheetXml(stylesheet, true); - if (xml is not null) + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem? fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) + { + if (file.IsNullOrWhiteSpace()) + { + continue; + } + + if (!fileSystem?.FileExists(file) ?? false) + { + throw new InvalidOperationException("No file found with path " + file); + } + + using (Stream stream = fileSystem!.OpenFile(file)) + { + using (var reader = new StreamReader(stream)) { - stylesheetsXml.Add(xml); + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); } } - root.Add(stylesheetsXml); } - private void PackageStaticFiles( - IEnumerable filePaths, - XContainer root, - string containerName, - string elementName, - IFileSystem? fileSystem) + root.Add(scriptsXml); + } + + private void PackageTemplates(PackageDefinition definition, XContainer root) + { + var templatesXml = new XElement("Templates"); + foreach (var templateId in definition.Templates) { - var scriptsXml = new XElement(containerName); - foreach (var file in filePaths) + if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (file.IsNullOrWhiteSpace()) - { - continue; - } + continue; + } - if (!fileSystem?.FileExists(file) ?? false) - { - throw new InvalidOperationException("No file found with path " + file); - } + ITemplate? template = _fileService.GetTemplate(outInt); + if (template == null) + { + continue; + } - using (Stream stream = fileSystem!.OpenFile(file)) + templatesXml.Add(_serializer.Serialize(template)); + } + + root.Add(templatesXml); + } + + private void PackageDocumentTypes(PackageDefinition definition, XContainer root) + { + var contentTypes = new HashSet(); + var docTypesXml = new XElement("DocumentTypes"); + foreach (var dtId in definition.DocumentTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IContentType? contentType = _contentTypeService.Get(outInt); + if (contentType == null) + { + continue; + } + + AddDocumentType(contentType, contentTypes); + } + + foreach (IContentType contentType in contentTypes) + { + docTypesXml.Add(_serializer.Serialize(contentType)); + } + + root.Add(docTypesXml); + } + + private void PackageMediaTypes(PackageDefinition definition, XContainer root) + { + var mediaTypes = new HashSet(); + var mediaTypesXml = new XElement("MediaTypes"); + foreach (var mediaTypeId in definition.MediaTypes) + { + if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IMediaType? mediaType = _mediaTypeService.Get(outInt); + if (mediaType == null) + { + continue; + } + + AddMediaType(mediaType, mediaTypes); + } + + foreach (IMediaType mediaType in mediaTypes) + { + mediaTypesXml.Add(_serializer.Serialize(mediaType)); + } + + root.Add(mediaTypesXml); + } + + private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + { + // Documents and tags + if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) + { + if (contentNodeId > 0) + { + // load content from umbraco. + IContent? content = _contentService.GetById(contentNodeId); + if (content != null) { - using (var reader = new StreamReader(stream)) - { - var fileContents = reader.ReadToEnd(); - scriptsXml.Add( + XElement contentXml = definition.ContentLoadChildNodes + ? content.ToDeepXml(_serializer) + : content.ToXml(_serializer); + + // Create the Documents/DocumentSet node + root.Add( + new XElement( + "Documents", new XElement( - elementName, - new XAttribute("path", file), - new XCData(fileContents))); - } + "DocumentSet", + new XAttribute("importMode", "root"), + contentXml))); + + // TODO: I guess tags has been broken for a very long time for packaging, we should get this working again sometime + ////Create the TagProperties node - this is used to store a definition for all + //// document properties that are tags, this ensures that we can re-import tags properly + // XmlNode tagProps = new XElement("TagProperties"); + + ////before we try to populate this, we'll do a quick lookup to see if any of the documents + //// being exported contain published tags. + // var allExportedIds = documents.SelectNodes("//@id").Cast() + // .Select(x => x.Value.TryConvertTo()) + // .Where(x => x.Success) + // .Select(x => x.Result) + // .ToArray(); + // var allContentTags = new List(); + // foreach (var exportedId in allExportedIds) + // { + // allContentTags.AddRange( + // Current.Services.TagService.GetTagsForEntity(exportedId)); + // } + + ////This is pretty round-about but it works. Essentially we need to get the properties that are tagged + //// but to do that we need to lookup by a tag (string) + // var allTaggedEntities = new List(); + // foreach (var group in allContentTags.Select(x => x.Group).Distinct()) + // { + // allTaggedEntities.AddRange( + // Current.Services.TagService.GetTaggedContentByTagGroup(group)); + // } + + ////Now, we have all property Ids/Aliases and their referenced document Ids and tags + // var allExportedTaggedEntities = allTaggedEntities.Where(x => allExportedIds.Contains(x.EntityId)) + // .DistinctBy(x => x.EntityId) + // .OrderBy(x => x.EntityId); + + // foreach (var taggedEntity in allExportedTaggedEntities) + // { + // foreach (var taggedProperty in taggedEntity.TaggedProperties.Where(x => x.Tags.Any())) + // { + // XmlNode tagProp = new XElement("TagProperty"); + // var docId = packageManifest.CreateAttribute("docId", ""); + // docId.Value = taggedEntity.EntityId.ToString(CultureInfo.InvariantCulture); + // tagProp.Attributes.Append(docId); + + // var propertyAlias = packageManifest.CreateAttribute("propertyAlias", ""); + // propertyAlias.Value = taggedProperty.PropertyTypeAlias; + // tagProp.Attributes.Append(propertyAlias); + + // var group = packageManifest.CreateAttribute("group", ""); + // group.Value = taggedProperty.Tags.First().Group; + // tagProp.Attributes.Append(group); + + // tagProp.AppendChild(packageManifest.CreateCDataSection( + // JsonConvert.SerializeObject(taggedProperty.Tags.Select(x => x.Text).ToArray()))); + + // tagProps.AppendChild(tagProp); + // } + // } + + // manifestRoot.Add(tagProps); } } - - root.Add(scriptsXml); - } - - private void PackageTemplates(PackageDefinition definition, XContainer root) - { - var templatesXml = new XElement("Templates"); - foreach (var templateId in definition.Templates) - { - if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var template = _fileService.GetTemplate(outInt); - if (template == null) - continue; - templatesXml.Add(_serializer.Serialize(template)); - } - root.Add(templatesXml); - } - - private void PackageDocumentTypes(PackageDefinition definition, XContainer root) - { - var contentTypes = new HashSet(); - var docTypesXml = new XElement("DocumentTypes"); - foreach (var dtId in definition.DocumentTypes) - { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var contentType = _contentTypeService.Get(outInt); - if (contentType == null) - continue; - AddDocumentType(contentType, contentTypes); - } - foreach (var contentType in contentTypes) - docTypesXml.Add(_serializer.Serialize(contentType)); - - root.Add(docTypesXml); - } - - private void PackageMediaTypes(PackageDefinition definition, XContainer root) - { - var mediaTypes = new HashSet(); - var mediaTypesXml = new XElement("MediaTypes"); - foreach (var mediaTypeId in definition.MediaTypes) - { - if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - continue; - var mediaType = _mediaTypeService.Get(outInt); - if (mediaType == null) - continue; - AddMediaType(mediaType, mediaTypes); - } - foreach (var mediaType in mediaTypes) - mediaTypesXml.Add(_serializer.Serialize(mediaType)); - - root.Add(mediaTypesXml); - } - - private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) - { - //Documents and tags - if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) - { - if (contentNodeId > 0) - { - //load content from umbraco. - var content = _contentService.GetById(contentNodeId); - if (content != null) - { - var contentXml = definition.ContentLoadChildNodes ? content.ToDeepXml(_serializer) : content.ToXml(_serializer); - - //Create the Documents/DocumentSet node - - root.Add( - new XElement("Documents", - new XElement("DocumentSet", - new XAttribute("importMode", "root"), - contentXml))); - - // TODO: I guess tags has been broken for a very long time for packaging, we should get this working again sometime - ////Create the TagProperties node - this is used to store a definition for all - //// document properties that are tags, this ensures that we can re-import tags properly - //XmlNode tagProps = new XElement("TagProperties"); - - ////before we try to populate this, we'll do a quick lookup to see if any of the documents - //// being exported contain published tags. - //var allExportedIds = documents.SelectNodes("//@id").Cast() - // .Select(x => x.Value.TryConvertTo()) - // .Where(x => x.Success) - // .Select(x => x.Result) - // .ToArray(); - //var allContentTags = new List(); - //foreach (var exportedId in allExportedIds) - //{ - // allContentTags.AddRange( - // Current.Services.TagService.GetTagsForEntity(exportedId)); - //} - - ////This is pretty round-about but it works. Essentially we need to get the properties that are tagged - //// but to do that we need to lookup by a tag (string) - //var allTaggedEntities = new List(); - //foreach (var group in allContentTags.Select(x => x.Group).Distinct()) - //{ - // allTaggedEntities.AddRange( - // Current.Services.TagService.GetTaggedContentByTagGroup(group)); - //} - - ////Now, we have all property Ids/Aliases and their referenced document Ids and tags - //var allExportedTaggedEntities = allTaggedEntities.Where(x => allExportedIds.Contains(x.EntityId)) - // .DistinctBy(x => x.EntityId) - // .OrderBy(x => x.EntityId); - - //foreach (var taggedEntity in allExportedTaggedEntities) - //{ - // foreach (var taggedProperty in taggedEntity.TaggedProperties.Where(x => x.Tags.Any())) - // { - // XmlNode tagProp = new XElement("TagProperty"); - // var docId = packageManifest.CreateAttribute("docId", ""); - // docId.Value = taggedEntity.EntityId.ToString(CultureInfo.InvariantCulture); - // tagProp.Attributes.Append(docId); - - // var propertyAlias = packageManifest.CreateAttribute("propertyAlias", ""); - // propertyAlias.Value = taggedProperty.PropertyTypeAlias; - // tagProp.Attributes.Append(propertyAlias); - - // var group = packageManifest.CreateAttribute("group", ""); - // group.Value = taggedProperty.Tags.First().Group; - // tagProp.Attributes.Append(group); - - // tagProp.AppendChild(packageManifest.CreateCDataSection( - // JsonConvert.SerializeObject(taggedProperty.Tags.Select(x => x.Text).ToArray()))); - - // tagProps.AppendChild(tagProp); - // } - //} - - //manifestRoot.Add(tagProps); - } - } - } - } - - - private Dictionary PackageMedia(PackageDefinition definition, XElement root) - { - var mediaStreams = new Dictionary(); - - // callback that occurs on each serialized media item - void OnSerializedMedia(IMedia media, XElement xmlMedia) - { - // get the media file path and store that separately in the XML. - // the media file path is different from the URL and is specifically - // extracted using the property editor for this media file and the current media file system. - Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); - if (mediaStream != null && mediaFilePath is not null) - { - xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); - - // add the stream to our outgoing stream - mediaStreams.Add(mediaFilePath, mediaStream); - } - } - - IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); - - var mediaXml = new XElement( - "MediaItems", - medias.Select(media => - { - XElement serializedMedia = _serializer.Serialize( - media, - definition.MediaLoadChildNodes, - OnSerializedMedia); - - return new XElement("MediaSet", serializedMedia); - })); - - root.Add(mediaXml); - - return mediaStreams; - } - - // TODO: Delete this - /// - private XElement? GetMacroXml(int macroId, out IMacro? macro) - { - macro = _macroService.GetById(macroId); - if (macro == null) - return null; - var xml = _serializer.Serialize(macro); - return xml; - } - - /// - /// Converts a umbraco stylesheet to a package xml node - /// - /// The path of the stylesheet. - /// if set to true [include properties]. - /// - private XElement? GetStylesheetXml(string path, bool includeProperties) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - } - - IStylesheet? stylesheet = _fileService.GetStylesheet(path); - if (stylesheet == null) - { - return null; - } - - return _serializer.Serialize(stylesheet, includeProperties); - } - - private void AddDocumentType(IContentType dt, HashSet dtl) - { - if (dt.ParentId > 0) - { - var parent = _contentTypeService.Get(dt.ParentId); - if (parent != null) // could be a container - AddDocumentType(parent, dtl); - } - - if (!dtl.Contains(dt)) - dtl.Add(dt); - } - - private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) - { - if (mediaType.ParentId > 0) - { - var parent = _mediaTypeService.Get(mediaType.ParentId); - if (parent != null) // could be a container - AddMediaType(parent, mediaTypes); - } - - if (!mediaTypes.Contains(mediaType)) - mediaTypes.Add(mediaType); - } - - private static XElement GetPackageInfoXml(PackageDefinition definition) - { - var info = new XElement("info"); - - //Package info - var package = new XElement("package"); - package.Add(new XElement("name", definition.Name)); - info.Add(package); - return info; - } - - private static XDocument CreateCompiledPackageXml(out XElement root) - { - root = new XElement("umbPackage"); - var compiledPackageXml = new XDocument(root); - return compiledPackageXml; - } - - private XDocument EnsureStorage(out string packagesFile) - { - var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); - Directory.CreateDirectory(packagesFolder); - - packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); - if (!File.Exists(packagesFile)) - { - var xml = new XDocument(new XElement("packages")); - xml.Save(packagesFile); - - return xml; - } - - var packagesXml = XDocument.Load(packagesFile); - return packagesXml; - } - - public void DeleteLocalRepositoryFiles() - { - var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); - if (File.Exists(packagesFile)) - { - File.Delete(packagesFile); - } - - var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); - if (Directory.Exists(packagesFolder)) - { - Directory.Delete(packagesFolder); - } } } + + private Dictionary PackageMedia(PackageDefinition definition, XElement root) + { + var mediaStreams = new Dictionary(); + + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) + { + // get the media file path and store that separately in the XML. + // the media file path is different from the URL and is specifically + // extracted using the property editor for this media file and the current media file system. + Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); + if (mediaStream != null && mediaFilePath is not null) + { + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); + + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath, mediaStream); + } + } + + IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); + + var mediaXml = new XElement( + "MediaItems", + medias.Select(media => + { + XElement serializedMedia = _serializer.Serialize( + media, + definition.MediaLoadChildNodes, + OnSerializedMedia); + + return new XElement("MediaSet", serializedMedia); + })); + + root.Add(mediaXml); + + return mediaStreams; + } + + // TODO: Delete this + private XElement? GetMacroXml(int macroId, out IMacro? macro) + { + macro = _macroService.GetById(macroId); + if (macro == null) + { + return null; + } + + XElement xml = _serializer.Serialize(macro); + return xml; + } + + /// + /// Converts a umbraco stylesheet to a package xml node + /// + /// The path of the stylesheet. + /// if set to true [include properties]. + /// + private XElement? GetStylesheetXml(string path, bool includeProperties) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + } + + IStylesheet? stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) + { + return null; + } + + return _serializer.Serialize(stylesheet, includeProperties); + } + + private void AddDocumentType(IContentType dt, HashSet dtl) + { + if (dt.ParentId > 0) + { + IContentType? parent = _contentTypeService.Get(dt.ParentId); + + // could be a container + if (parent != null) + { + AddDocumentType(parent, dtl); + } + } + + if (!dtl.Contains(dt)) + { + dtl.Add(dt); + } + } + + private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + { + if (mediaType.ParentId > 0) + { + IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); + + // could be a container + if (parent != null) + { + AddMediaType(parent, mediaTypes); + } + } + + if (!mediaTypes.Contains(mediaType)) + { + mediaTypes.Add(mediaType); + } + } + + private static XDocument CreateCompiledPackageXml(out XElement root) + { + root = new XElement("umbPackage"); + var compiledPackageXml = new XDocument(root); + return compiledPackageXml; + } + + private XDocument EnsureStorage(out string packagesFile) + { + var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); + Directory.CreateDirectory(packagesFolder); + + packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); + if (!File.Exists(packagesFile)) + { + var xml = new XDocument(new XElement("packages")); + xml.Save(packagesFile); + + return xml; + } + + var packagesXml = XDocument.Load(packagesFile); + return packagesXml; + } } diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index de5b8c04ae..6ca78967b3 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -1,90 +1,90 @@ // ReSharper disable once CheckNamespace -namespace Umbraco.Cms.Core + +namespace Umbraco.Cms.Core; + +public static partial class Constants { - public static partial class Constants + public static class DatabaseSchema { - public static class DatabaseSchema + // TODO: Why aren't all table names with the same prefix? + public const string TableNamePrefix = "umbraco"; + + public static class Tables { - //TODO: Why aren't all table names with the same prefix? - public const string TableNamePrefix = "umbraco"; + public const string Lock = TableNamePrefix + "Lock"; + public const string Log = TableNamePrefix + "Log"; - public static class Tables - { - public const string Lock = TableNamePrefix + "Lock"; - public const string Log = TableNamePrefix + "Log"; + public const string Node = TableNamePrefix + "Node"; + public const string NodeData = /*TableNamePrefix*/ "cms" + "ContentNu"; - public const string Node = TableNamePrefix + "Node"; - public const string NodeData = /*TableNamePrefix*/ "cms" + "ContentNu"; + public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; + public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType"; + public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType"; + public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType"; + public const string DataType = TableNamePrefix + "DataType"; + public const string Template = /*TableNamePrefix*/ "cms" + "Template"; - public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType"; - public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType"; - public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType"; - public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType"; - public const string DataType = TableNamePrefix + "DataType"; - public const string Template = /*TableNamePrefix*/ "cms" + "Template"; + public const string Content = TableNamePrefix + "Content"; + public const string ContentVersion = TableNamePrefix + "ContentVersion"; + public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation"; + public const string ContentVersionCleanupPolicy = TableNamePrefix + "ContentVersionCleanupPolicy"; - public const string Content = TableNamePrefix + "Content"; - public const string ContentVersion = TableNamePrefix + "ContentVersion"; - public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation"; - public const string ContentVersionCleanupPolicy = TableNamePrefix + "ContentVersionCleanupPolicy"; + public const string Document = TableNamePrefix + "Document"; + public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; + public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; + public const string MediaVersion = TableNamePrefix + "MediaVersion"; + public const string ContentSchedule = TableNamePrefix + "ContentSchedule"; - public const string Document = TableNamePrefix + "Document"; - public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; - public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; - public const string MediaVersion = TableNamePrefix + "MediaVersion"; - public const string ContentSchedule = TableNamePrefix + "ContentSchedule"; + public const string PropertyType = /*TableNamePrefix*/ "cms" + "PropertyType"; + public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup"; + public const string PropertyData = TableNamePrefix + "PropertyData"; - public const string PropertyType = /*TableNamePrefix*/ "cms" + "PropertyType"; - public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup"; - public const string PropertyData = TableNamePrefix + "PropertyData"; + public const string RelationType = TableNamePrefix + "RelationType"; + public const string Relation = TableNamePrefix + "Relation"; - public const string RelationType = TableNamePrefix + "RelationType"; - public const string Relation = TableNamePrefix + "Relation"; + public const string Domain = TableNamePrefix + "Domain"; + public const string Language = TableNamePrefix + "Language"; + public const string DictionaryEntry = /*TableNamePrefix*/ "cms" + "Dictionary"; + public const string DictionaryValue = /*TableNamePrefix*/ "cms" + "LanguageText"; - public const string Domain = TableNamePrefix + "Domain"; - public const string Language = TableNamePrefix + "Language"; - public const string DictionaryEntry = /*TableNamePrefix*/ "cms" + "Dictionary"; - public const string DictionaryValue = /*TableNamePrefix*/ "cms" + "LanguageText"; + public const string User = TableNamePrefix + "User"; + public const string UserGroup = TableNamePrefix + "UserGroup"; + public const string UserStartNode = TableNamePrefix + "UserStartNode"; + public const string User2UserGroup = TableNamePrefix + "User2UserGroup"; + public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify"; + public const string UserGroup2App = TableNamePrefix + "UserGroup2App"; + public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node"; + public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission"; + public const string ExternalLogin = TableNamePrefix + "ExternalLogin"; + public const string TwoFactorLogin = TableNamePrefix + "TwoFactorLogin"; + public const string ExternalLoginToken = TableNamePrefix + "ExternalLoginToken"; - public const string User = TableNamePrefix + "User"; - public const string UserGroup = TableNamePrefix + "UserGroup"; - public const string UserStartNode = TableNamePrefix + "UserStartNode"; - public const string User2UserGroup = TableNamePrefix + "User2UserGroup"; - public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify"; - public const string UserGroup2App = TableNamePrefix + "UserGroup2App"; - public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node"; - public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission"; - public const string ExternalLogin = TableNamePrefix + "ExternalLogin"; - public const string TwoFactorLogin = TableNamePrefix + "TwoFactorLogin"; - public const string ExternalLoginToken = TableNamePrefix + "ExternalLoginToken"; + public const string Macro = /*TableNamePrefix*/ "cms" + "Macro"; + public const string MacroProperty = /*TableNamePrefix*/ "cms" + "MacroProperty"; - public const string Macro = /*TableNamePrefix*/ "cms" + "Macro"; - public const string MacroProperty = /*TableNamePrefix*/ "cms" + "MacroProperty"; + public const string Member = /*TableNamePrefix*/ "cms" + "Member"; + public const string MemberPropertyType = /*TableNamePrefix*/ "cms" + "MemberType"; + public const string Member2MemberGroup = /*TableNamePrefix*/ "cms" + "Member2MemberGroup"; - public const string Member = /*TableNamePrefix*/ "cms" + "Member"; - public const string MemberPropertyType = /*TableNamePrefix*/ "cms" + "MemberType"; - public const string Member2MemberGroup = /*TableNamePrefix*/ "cms" + "Member2MemberGroup"; + public const string Access = TableNamePrefix + "Access"; + public const string AccessRule = TableNamePrefix + "AccessRule"; + public const string RedirectUrl = TableNamePrefix + "RedirectUrl"; - public const string Access = TableNamePrefix + "Access"; - public const string AccessRule = TableNamePrefix + "AccessRule"; - public const string RedirectUrl = TableNamePrefix + "RedirectUrl"; + public const string CacheInstruction = TableNamePrefix + "CacheInstruction"; + public const string Server = TableNamePrefix + "Server"; - public const string CacheInstruction = TableNamePrefix + "CacheInstruction"; - public const string Server = TableNamePrefix + "Server"; + public const string Tag = /*TableNamePrefix*/ "cms" + "Tags"; + public const string TagRelationship = /*TableNamePrefix*/ "cms" + "TagRelationship"; - public const string Tag = /*TableNamePrefix*/ "cms" + "Tags"; - public const string TagRelationship = /*TableNamePrefix*/ "cms" + "TagRelationship"; + public const string KeyValue = TableNamePrefix + "KeyValue"; - public const string KeyValue = TableNamePrefix + "KeyValue"; + public const string AuditEntry = TableNamePrefix + "Audit"; + public const string Consent = TableNamePrefix + "Consent"; + public const string UserLogin = TableNamePrefix + "UserLogin"; - public const string AuditEntry = TableNamePrefix + "Audit"; - public const string Consent = TableNamePrefix + "Consent"; - public const string UserLogin = TableNamePrefix + "UserLogin"; + public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; - public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; - - public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; - } + public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; } } } diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs index 3c0b2c4d28..e97f16a663 100644 --- a/src/Umbraco.Core/Persistence/Constants-Locks.cs +++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs @@ -1,75 +1,74 @@ -// ReSharper disable once CheckNamespace +// ReSharper disable once CheckNamespace using Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static partial class Constants { - static partial class Constants + /// + /// Defines lock objects. + /// + public static class Locks { /// - /// Defines lock objects. + /// The lock /// - public static class Locks - { - /// - /// The lock - /// - public const int MainDom = -1000; + public const int MainDom = -1000; - /// - /// All servers. - /// - public const int Servers = -331; + /// + /// All servers. + /// + public const int Servers = -331; - /// - /// All content and media types. - /// - public const int ContentTypes = -332; + /// + /// All content and media types. + /// + public const int ContentTypes = -332; - /// - /// The entire content tree, i.e. all content items. - /// - public const int ContentTree = -333; + /// + /// The entire content tree, i.e. all content items. + /// + public const int ContentTree = -333; - /// - /// The entire media tree, i.e. all media items. - /// - public const int MediaTree = -334; + /// + /// The entire media tree, i.e. all media items. + /// + public const int MediaTree = -334; - /// - /// The entire member tree, i.e. all members. - /// - public const int MemberTree = -335; + /// + /// The entire member tree, i.e. all members. + /// + public const int MemberTree = -335; - /// - /// All media types. - /// - public const int MediaTypes = -336; + /// + /// All media types. + /// + public const int MediaTypes = -336; - /// - /// All member types. - /// - public const int MemberTypes = -337; + /// + /// All member types. + /// + public const int MemberTypes = -337; - /// - /// All domains. - /// - public const int Domains = -338; + /// + /// All domains. + /// + public const int Domains = -338; - /// - /// All key-values. - /// - public const int KeyValues = -339; + /// + /// All key-values. + /// + public const int KeyValues = -339; - /// - /// All languages. - /// - public const int Languages = -340; + /// + /// All languages. + /// + public const int Languages = -340; - /// - /// ScheduledPublishing job. - /// - public const int ScheduledPublishing = -341; - } + /// + /// ScheduledPublishing job. + /// + public const int ScheduledPublishing = -341; } } diff --git a/src/Umbraco.Core/Persistence/IQueryRepository.cs b/src/Umbraco.Core/Persistence/IQueryRepository.cs index 1a8dbaf971..e0e507abc1 100644 --- a/src/Umbraco.Core/Persistence/IQueryRepository.cs +++ b/src/Umbraco.Core/Persistence/IQueryRepository.cs @@ -1,21 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// Defines the base implementation of a querying repository. +/// +public interface IQueryRepository : IRepository { /// - /// Defines the base implementation of a querying repository. + /// Gets entities. /// - public interface IQueryRepository : IRepository - { - /// - /// Gets entities. - /// - IEnumerable Get(IQuery query); + IEnumerable Get(IQuery query); - /// - /// Counts entities. - /// - int Count(IQuery query); - } + /// + /// Counts entities. + /// + int Count(IQuery query); } diff --git a/src/Umbraco.Core/Persistence/IReadRepository.cs b/src/Umbraco.Core/Persistence/IReadRepository.cs index 0f757ae04a..6503019988 100644 --- a/src/Umbraco.Core/Persistence/IReadRepository.cs +++ b/src/Umbraco.Core/Persistence/IReadRepository.cs @@ -1,25 +1,22 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Persistence; -namespace Umbraco.Cms.Core.Persistence +/// +/// Defines the base implementation of a reading repository. +/// +public interface IReadRepository : IRepository { /// - /// Defines the base implementation of a reading repository. + /// Gets an entity. /// - public interface IReadRepository : IRepository - { - /// - /// Gets an entity. - /// - TEntity? Get(TId? id); + TEntity? Get(TId? id); - /// - /// Gets entities. - /// - IEnumerable GetMany(params TId[]? ids); + /// + /// Gets entities. + /// + IEnumerable GetMany(params TId[]? ids); - /// - /// Gets a value indicating whether an entity exists. - /// - bool Exists(TId id); - } + /// + /// Gets a value indicating whether an entity exists. + /// + bool Exists(TId id); } diff --git a/src/Umbraco.Core/Persistence/IReadWriteQueryRepository.cs b/src/Umbraco.Core/Persistence/IReadWriteQueryRepository.cs index b260144de6..40eb92bef6 100644 --- a/src/Umbraco.Core/Persistence/IReadWriteQueryRepository.cs +++ b/src/Umbraco.Core/Persistence/IReadWriteQueryRepository.cs @@ -1,8 +1,9 @@ -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// Defines the base implementation of a reading, writing and querying repository. +/// +public interface IReadWriteQueryRepository : IReadRepository, IWriteRepository, + IQueryRepository { - /// - /// Defines the base implementation of a reading, writing and querying repository. - /// - public interface IReadWriteQueryRepository : IReadRepository, IWriteRepository, IQueryRepository - { } } diff --git a/src/Umbraco.Core/Persistence/IRepository.cs b/src/Umbraco.Core/Persistence/IRepository.cs index f91c4c998b..2629e14c04 100644 --- a/src/Umbraco.Core/Persistence/IRepository.cs +++ b/src/Umbraco.Core/Persistence/IRepository.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// Defines the base implementation of a repository. +/// +public interface IRepository { - /// - /// Defines the base implementation of a repository. - /// - public interface IRepository - { } } diff --git a/src/Umbraco.Core/Persistence/IWriteRepository.cs b/src/Umbraco.Core/Persistence/IWriteRepository.cs index ff766fbe36..26e1548bc6 100644 --- a/src/Umbraco.Core/Persistence/IWriteRepository.cs +++ b/src/Umbraco.Core/Persistence/IWriteRepository.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// Defines the base implementation of a writing repository. +/// +public interface IWriteRepository : IRepository { /// - /// Defines the base implementation of a writing repository. + /// Saves an entity. /// - public interface IWriteRepository : IRepository - { - /// - /// Saves an entity. - /// - void Save(TEntity entity); + void Save(TEntity entity); - /// - /// Deletes an entity. - /// - /// - void Delete(TEntity entity); - } + /// + /// Deletes an entity. + /// + /// + void Delete(TEntity entity); } diff --git a/src/Umbraco.Core/Persistence/Querying/IQuery.cs b/src/Umbraco.Core/Persistence/Querying/IQuery.cs index d2a3b0830f..8803d69fc0 100644 --- a/src/Umbraco.Core/Persistence/Querying/IQuery.cs +++ b/src/Umbraco.Core/Persistence/Querying/IQuery.cs @@ -1,42 +1,39 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Linq.Expressions; -namespace Umbraco.Cms.Core.Persistence.Querying +namespace Umbraco.Cms.Core.Persistence.Querying; + +/// +/// Represents a query for building Linq translatable SQL queries +/// +/// +public interface IQuery { /// - /// Represents a query for building Linq translatable SQL queries + /// Adds a where clause to the query /// - /// - public interface IQuery - { - /// - /// Adds a where clause to the query - /// - /// - /// This instance so calls to this method are chainable - IQuery Where(Expression> predicate); + /// + /// This instance so calls to this method are chainable + IQuery Where(Expression> predicate); - /// - /// Returns all translated where clauses and their sql parameters - /// - /// - IEnumerable> GetWhereClauses(); + /// + /// Returns all translated where clauses and their sql parameters + /// + /// + IEnumerable> GetWhereClauses(); - /// - /// Adds a where-in clause to the query - /// - /// - /// - /// This instance so calls to this method are chainable - IQuery WhereIn(Expression> fieldSelector, IEnumerable? values); + /// + /// Adds a where-in clause to the query + /// + /// + /// + /// This instance so calls to this method are chainable + IQuery WhereIn(Expression> fieldSelector, IEnumerable? values); - /// - /// Adds a set of OR-ed where clauses to the query. - /// - /// - /// This instance so calls to this method are chainable. - IQuery WhereAny(IEnumerable>> predicates); - } + /// + /// Adds a set of OR-ed where clauses to the query. + /// + /// + /// This instance so calls to this method are chainable. + IQuery WhereAny(IEnumerable>> predicates); } diff --git a/src/Umbraco.Core/Persistence/Querying/StringPropertyMatchType.cs b/src/Umbraco.Core/Persistence/Querying/StringPropertyMatchType.cs index 3e48a00d05..fa8e674b97 100644 --- a/src/Umbraco.Core/Persistence/Querying/StringPropertyMatchType.cs +++ b/src/Umbraco.Core/Persistence/Querying/StringPropertyMatchType.cs @@ -1,15 +1,15 @@ -namespace Umbraco.Cms.Core.Persistence.Querying +namespace Umbraco.Cms.Core.Persistence.Querying; + +/// +/// Determines how to match a string property value +/// +public enum StringPropertyMatchType { - /// - /// Determines how to match a string property value - /// - public enum StringPropertyMatchType - { - Exact, - Contains, - StartsWith, - EndsWith, - //Deals with % as wildcard chars in a string - Wildcard - } + Exact, + Contains, + StartsWith, + EndsWith, + + // Deals with % as wildcard chars in a string + Wildcard, } diff --git a/src/Umbraco.Core/Persistence/Querying/ValuePropertyMatchType.cs b/src/Umbraco.Core/Persistence/Querying/ValuePropertyMatchType.cs index 58daf2e577..ab6fd4f938 100644 --- a/src/Umbraco.Core/Persistence/Querying/ValuePropertyMatchType.cs +++ b/src/Umbraco.Core/Persistence/Querying/ValuePropertyMatchType.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Persistence.Querying +namespace Umbraco.Cms.Core.Persistence.Querying; + +/// +/// Determine how to match a number or data value +/// +public enum ValuePropertyMatchType { - /// - /// Determine how to match a number or data value - /// - public enum ValuePropertyMatchType - { - Exact, - GreaterThan, - LessThan, - GreaterThanOrEqualTo, - LessThanOrEqualTo - } + Exact, + GreaterThan, + LessThan, + GreaterThanOrEqualTo, + LessThanOrEqualTo, } diff --git a/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs index 159267c16e..ade100f0d2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Represents a repository for entities. +/// +public interface IAuditEntryRepository : IReadWriteQueryRepository { /// - /// Represents a repository for entities. + /// Gets a page of entries. /// - public interface IAuditEntryRepository : IReadWriteQueryRepository - { - /// - /// Gets a page of entries. - /// - IEnumerable GetPage(long pageIndex, int pageCount, out long records); + IEnumerable GetPage(long pageIndex, int pageCount, out long records); - /// - /// Determines whether the repository is available. - /// - /// During an upgrade, the repository may not be available, until the table has been created. - bool IsAvailable(); - } + /// + /// Determines whether the repository is available. + /// + /// During an upgrade, the repository may not be available, until the table has been created. + bool IsAvailable(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs index 6d28a86b64..acceefef5d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs @@ -1,38 +1,40 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IAuditRepository : IReadRepository, IWriteRepository, + IQueryRepository { - public interface IAuditRepository : IReadRepository, IWriteRepository, IQueryRepository - { - void CleanLogs(int maximumAgeOfLogsInMinutes); + void CleanLogs(int maximumAgeOfLogsInMinutes); - /// - /// Return the audit items as paged result - /// - /// - /// The query coming from the service - /// - /// - /// - /// - /// - /// - /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter - /// so we need to do that here - /// - /// - /// A user supplied custom filter - /// - /// - IEnumerable GetPagedResultsByQuery( - IQuery query, - long pageIndex, int pageSize, out long totalRecords, - Direction orderDirection, - AuditType[]? auditTypeFilter, - IQuery? customFilter); + /// + /// Return the audit items as paged result + /// + /// + /// The query coming from the service + /// + /// + /// + /// + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query + /// or the custom filter + /// so we need to do that here + /// + /// + /// A user supplied custom filter + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, + long pageIndex, + int pageSize, + out long totalRecords, + Direction orderDirection, + AuditType[]? auditTypeFilter, + IQuery? customFilter); - IEnumerable Get(AuditType type, IQuery query); - } + IEnumerable Get(AuditType type, IQuery query); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs index e93f5829a1..f11ddf10e3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ICacheInstructionRepository.cs @@ -1,50 +1,47 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Represents a repository for entities. +/// +public interface ICacheInstructionRepository : IRepository { /// - /// Represents a repository for entities. + /// Gets the count of pending cache instruction records. /// - public interface ICacheInstructionRepository : IRepository - { - /// - /// Gets the count of pending cache instruction records. - /// - int CountAll(); + int CountAll(); - /// - /// Gets the count of pending cache instructions. - /// - int CountPendingInstructions(int lastId); + /// + /// Gets the count of pending cache instructions. + /// + int CountPendingInstructions(int lastId); - /// - /// Gets the most recent cache instruction record Id. - /// - /// - int GetMaxId(); + /// + /// Gets the most recent cache instruction record Id. + /// + /// + int GetMaxId(); - /// - /// Checks to see if a single cache instruction by Id exists. - /// - bool Exists(int id); + /// + /// Checks to see if a single cache instruction by Id exists. + /// + bool Exists(int id); - /// - /// Adds a new cache instruction record. - /// - void Add(CacheInstruction cacheInstruction); + /// + /// Adds a new cache instruction record. + /// + void Add(CacheInstruction cacheInstruction); - /// - /// Gets a collection of cache instructions created later than the provided Id. - /// - /// Last id processed. - /// The maximum number of instructions to retrieve. - IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve); + /// + /// Gets a collection of cache instructions created later than the provided Id. + /// + /// Last id processed. + /// The maximum number of instructions to retrieve. + IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve); - /// - /// Deletes cache instructions older than the provided date. - /// - void DeleteInstructionsOlderThan(DateTime pruneDate); - } + /// + /// Deletes cache instructions older than the provided date. + /// + void DeleteInstructionsOlderThan(DateTime pruneDate); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs index a89ed56285..7fcdb9d2d9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Represents a repository for entities. +/// +public interface IConsentRepository : IReadWriteQueryRepository { /// - /// Represents a repository for entities. + /// Clears the current flag. /// - public interface IConsentRepository : IReadWriteQueryRepository - { - /// - /// Clears the current flag. - /// - void ClearCurrent(string source, string context, string action); - } + void ClearCurrent(string source, string context, string action); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs index b753d35544..1172512228 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs @@ -1,83 +1,85 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Defines the base implementation of a repository for content items. +/// +public interface IContentRepository : IReadWriteQueryRepository + where TEntity : IUmbracoEntity { /// - /// Defines the base implementation of a repository for content items. + /// Gets the recycle bin identifier. /// - public interface IContentRepository : IReadWriteQueryRepository - where TEntity : IUmbracoEntity - { - /// - /// Gets versions. - /// - /// Current version is first, and then versions are ordered with most recent first. - IEnumerable GetAllVersions(int nodeId); + int RecycleBinId { get; } - /// - /// Gets versions. - /// - /// Current version is first, and then versions are ordered with most recent first. - IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take); + /// + /// Gets versions. + /// + /// Current version is first, and then versions are ordered with most recent first. + IEnumerable GetAllVersions(int nodeId); - /// - /// Gets version identifiers. - /// - /// Current version is first, and then versions are ordered with most recent first. - IEnumerable GetVersionIds(int id, int topRows); + /// + /// Gets versions. + /// + /// Current version is first, and then versions are ordered with most recent first. + IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take); - /// - /// Gets a version. - /// - TEntity? GetVersion(int versionId); + /// + /// Gets version identifiers. + /// + /// Current version is first, and then versions are ordered with most recent first. + IEnumerable GetVersionIds(int id, int topRows); - /// - /// Deletes a version. - /// - void DeleteVersion(int versionId); + /// + /// Gets a version. + /// + TEntity? GetVersion(int versionId); - /// - /// Deletes all versions older than a date. - /// - void DeleteVersions(int nodeId, DateTime versionDate); + /// + /// Deletes a version. + /// + void DeleteVersion(int versionId); - /// - /// Gets the recycle bin identifier. - /// - int RecycleBinId { get; } + /// + /// Deletes all versions older than a date. + /// + void DeleteVersions(int nodeId, DateTime versionDate); - /// - /// Gets the recycle bin content. - /// - IEnumerable? GetRecycleBin(); + /// + /// Gets the recycle bin content. + /// + IEnumerable? GetRecycleBin(); - /// - /// Gets the count of content items of a given content type. - /// - int Count(string? contentTypeAlias = null); + /// + /// Gets the count of content items of a given content type. + /// + int Count(string? contentTypeAlias = null); - /// - /// Gets the count of child content items of a given parent content, of a given content type. - /// - int CountChildren(int parentId, string? contentTypeAlias = null); + /// + /// Gets the count of child content items of a given parent content, of a given content type. + /// + int CountChildren(int parentId, string? contentTypeAlias = null); - /// - /// Gets the count of descendant content items of a given parent content, of a given content type. - /// - int CountDescendants(int parentId, string? contentTypeAlias = null); + /// + /// Gets the count of descendant content items of a given parent content, of a given content type. + /// + int CountDescendants(int parentId, string? contentTypeAlias = null); - /// - /// Gets paged content items. - /// - /// Here, can be null but cannot. - IEnumerable GetPage(IQuery? query, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering); + /// + /// Gets paged content items. + /// + /// Here, can be null but cannot. + IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter, + Ordering? ordering); - ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); - } + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeCommonRepository.cs index 7bdfa294c8..5b122d860d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeCommonRepository.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +// TODO +// this should be IContentTypeRepository, and what is IContentTypeRepository at the moment should +// become IDocumentTypeRepository - but since these interfaces are public, that would be breaking + +/// +/// Represents the content types common repository, dealing with document, media and member types. +/// +public interface IContentTypeCommonRepository { - // TODO - // this should be IContentTypeRepository, and what is IContentTypeRepository at the moment should - // become IDocumentTypeRepository - but since these interfaces are public, that would be breaking + /// + /// Gets and cache all types. + /// + IEnumerable? GetAllTypes(); /// - /// Represents the content types common repository, dealing with document, media and member types. + /// Clears the cache. /// - public interface IContentTypeCommonRepository - { - /// - /// Gets and cache all types. - /// - IEnumerable? GetAllTypes(); - - /// - /// Clears the cache. - /// - void ClearCache(); - } + void ClearCache(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs index 148132dc29..77adda5860 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepository.cs @@ -1,35 +1,32 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IContentTypeRepository : IContentTypeRepositoryBase { - public interface IContentTypeRepository : IContentTypeRepositoryBase - { - /// - /// Gets all entities of the specified query - /// - /// - /// An enumerable list of objects - IEnumerable GetByQuery(IQuery query); + /// + /// Gets all entities of the specified query + /// + /// + /// An enumerable list of objects + IEnumerable GetByQuery(IQuery query); - /// - /// Gets all property type aliases. - /// - /// - IEnumerable GetAllPropertyTypeAliases(); + /// + /// Gets all property type aliases. + /// + /// + IEnumerable GetAllPropertyTypeAliases(); - /// - /// Gets all content type aliases - /// - /// - /// If this list is empty, it will return all content type aliases for media, members and content, otherwise - /// it will only return content type aliases for the object types specified - /// - /// - IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); - IEnumerable GetAllContentTypeIds(string[] aliases); - } + IEnumerable GetAllContentTypeIds(string[] aliases); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 2a427da9dd..e90c70e89d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -1,45 +1,42 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IContentTypeRepositoryBase : IReadWriteQueryRepository, IReadRepository + where TItem : IContentTypeComposition { - public interface IContentTypeRepositoryBase : IReadWriteQueryRepository, IReadRepository - where TItem : IContentTypeComposition - { - TItem? Get(string alias); - IEnumerable> Move(TItem moving, EntityContainer container); + TItem? Get(string alias); - /// - /// Derives a unique alias from an existing alias. - /// - /// The original alias. - /// The original alias with a number appended to it, so that it is unique. - /// Unique across all content, media and member types. - string GetUniqueAlias(string alias); + IEnumerable> Move(TItem moving, EntityContainer container); + /// + /// Derives a unique alias from an existing alias. + /// + /// The original alias. + /// The original alias with a number appended to it, so that it is unique. + /// Unique across all content, media and member types. + string GetUniqueAlias(string alias); - /// - /// Gets a value indicating whether there is a list view content item in the path. - /// - /// - /// - bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(string contentPath); - /// - /// Gets a value indicating whether there is a list view content item in the path. - /// - /// - /// - bool HasContainerInPath(params int[] ids); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); - /// - /// Returns true or false depending on whether content nodes have been created based on the provided content type id. - /// - bool HasContentNodes(int id); - } + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + bool HasContentNodes(int id); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs index 3e19c08f99..69caeb8038 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeContainerRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDataTypeContainerRepository : IEntityContainerRepository { - public interface IDataTypeContainerRepository : IEntityContainerRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs index e9063416af..060d2f2e1d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs @@ -1,18 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public interface IDataTypeRepository : IReadWriteQueryRepository - { - IEnumerable> Move(IDataType toMove, EntityContainer? container); +namespace Umbraco.Cms.Core.Persistence.Repositories; - /// - /// Returns a dictionary of content type s and the property type aliases that use a - /// - /// - /// - IReadOnlyDictionary> FindUsages(int id); - } +public interface IDataTypeRepository : IReadWriteQueryRepository +{ + IEnumerable> Move(IDataType toMove, EntityContainer? container); + + /// + /// Returns a dictionary of content type s and the property type aliases that use a + /// + /// + /// + /// + IReadOnlyDictionary> FindUsages(int id); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs index 555624b1a0..db2347e925 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDictionaryRepository : IReadWriteQueryRepository { - public interface IDictionaryRepository : IReadWriteQueryRepository - { - IDictionaryItem? Get(Guid uniqueId); - IDictionaryItem? Get(string key); - IEnumerable GetDictionaryItemDescendants(Guid? parentId); - Dictionary GetDictionaryItemKeyMap(); - } + IDictionaryItem? Get(Guid uniqueId); + + IDictionaryItem? Get(string key); + + IEnumerable GetDictionaryItemDescendants(Guid? parentId); + + Dictionary GetDictionaryItemKeyMap(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs index e5e6e0f418..12857f0588 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentBlueprintRepository : IDocumentRepository { - public interface IDocumentBlueprintRepository : IDocumentRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index e0b7f234ec..15312ccbf2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -1,96 +1,98 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentRepository : IContentRepository, IReadRepository { - public interface IDocumentRepository : IContentRepository, IReadRepository - { - /// - /// Gets publish/unpublish schedule for a content node. - /// - /// - /// - ContentScheduleCollection GetContentSchedule(int contentId); + /// + /// Gets publish/unpublish schedule for a content node. + /// + /// + /// + /// + /// + ContentScheduleCollection GetContentSchedule(int contentId); - /// - /// Persists publish/unpublish schedule for a content node. - /// - /// - /// - void PersistContentSchedule(IContent content, ContentScheduleCollection schedule); + /// + /// Persists publish/unpublish schedule for a content node. + /// + /// + /// + void PersistContentSchedule(IContent content, ContentScheduleCollection schedule); - /// - /// Clears the publishing schedule for all entries having an a date before (lower than, or equal to) a specified date. - /// - void ClearSchedule(DateTime date); + /// + /// Clears the publishing schedule for all entries having an a date before (lower than, or equal to) a specified date. + /// + void ClearSchedule(DateTime date); - void ClearSchedule(DateTime date, ContentScheduleAction action); + void ClearSchedule(DateTime date, ContentScheduleAction action); - bool HasContentForExpiration(DateTime date); - bool HasContentForRelease(DateTime date); + bool HasContentForExpiration(DateTime date); - /// - /// Gets objects having an expiration date before (lower than, or equal to) a specified date. - /// - /// - /// The content returned from this method may be culture variant, in which case the resulting should be queried - /// for which culture(s) have been scheduled. - /// - IEnumerable GetContentForExpiration(DateTime date); + bool HasContentForRelease(DateTime date); - /// - /// Gets objects having a release date before (lower than, or equal to) a specified date. - /// - /// - /// The content returned from this method may be culture variant, in which case the resulting should be queried - /// for which culture(s) have been scheduled. - /// - IEnumerable GetContentForRelease(DateTime date); + /// + /// Gets objects having an expiration date before (lower than, or equal to) a specified date. + /// + /// + /// The content returned from this method may be culture variant, in which case the resulting + /// should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForExpiration(DateTime date); - /// - /// Get the count of published items - /// - /// - /// - /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter - /// - int CountPublished(string? contentTypeAlias = null); + /// + /// Gets objects having a release date before (lower than, or equal to) a specified date. + /// + /// + /// The content returned from this method may be culture variant, in which case the resulting + /// should be queried + /// for which culture(s) have been scheduled. + /// + IEnumerable GetContentForRelease(DateTime date); - bool IsPathPublished(IContent? content); + /// + /// Get the count of published items + /// + /// + /// + /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter + /// + int CountPublished(string? contentTypeAlias = null); - /// - /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. - /// - /// - void ReplaceContentPermissions(EntityPermissionSet permissionSet); + bool IsPathPublished(IContent? content); - /// - /// Assigns a single permission to the current content item for the specified user group ids - /// - /// - /// - /// - void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// + void ReplaceContentPermissions(EntityPermissionSet permissionSet); - /// - /// Gets the explicit list of permissions for the content item - /// - /// - /// - EntityPermissionCollection GetPermissionsForEntity(int entityId); + /// + /// Assigns a single permission to the current content item for the specified user group ids + /// + /// + /// + /// + void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); - /// - /// Used to add/update a permission for a content item - /// - /// - void AddOrUpdatePermissions(ContentPermissionSet permission); + /// + /// Gets the explicit list of permissions for the content item + /// + /// + /// + EntityPermissionCollection GetPermissionsForEntity(int entityId); - /// - /// Returns true if there is any content in the recycle bin - /// - bool RecycleBinSmells(); - } + /// + /// Used to add/update a permission for a content item + /// + /// + void AddOrUpdatePermissions(ContentPermissionSet permission); + + /// + /// Returns true if there is any content in the recycle bin + /// + bool RecycleBinSmells(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentTypeContainerRepository.cs index 53fd62fdbe..ed604ec165 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentTypeContainerRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentTypeContainerRepository : IEntityContainerRepository { - public interface IDocumentTypeContainerRepository : IEntityContainerRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs index ee46db3690..7526d83cd0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs @@ -1,38 +1,36 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentVersionRepository : IRepository { - public interface IDocumentVersionRepository : IRepository - { - /// - /// Gets a list of all historic content versions. - /// - public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup(); + /// + /// Gets a list of all historic content versions. + /// + public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup(); - /// - /// Gets cleanup policy override settings per content type. - /// - public IReadOnlyCollection? GetCleanupPolicies(); + /// + /// Gets cleanup policy override settings per content type. + /// + public IReadOnlyCollection? GetCleanupPolicies(); - /// - /// Gets paginated content versions for given content id paginated. - /// - public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null); + /// + /// Gets paginated content versions for given content id paginated. + /// + public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null); - /// - /// Deletes multiple content versions by ID. - /// - void DeleteVersions(IEnumerable versionIds); + /// + /// Deletes multiple content versions by ID. + /// + void DeleteVersions(IEnumerable versionIds); - /// - /// Updates the prevent cleanup flag on a content version. - /// - void SetPreventCleanup(int versionId, bool preventCleanup); + /// + /// Updates the prevent cleanup flag on a content version. + /// + void SetPreventCleanup(int versionId, bool preventCleanup); - /// - /// Gets the content version metadata for a specific version. - /// - ContentVersionMeta? Get(int versionId); - } + /// + /// Gets the content version metadata for a specific version. + /// + ContentVersionMeta? Get(int versionId); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDomainRepository.cs index a24b76f90a..18b2ef1f8e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDomainRepository.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDomainRepository : IReadWriteQueryRepository { - public interface IDomainRepository : IReadWriteQueryRepository - { - IDomain? GetByName(string domainName); - bool Exists(string domainName); - IEnumerable GetAll(bool includeWildcards); - IEnumerable GetAssignedDomains(int contentId, bool includeWildcards); - } + IDomain? GetByName(string domainName); + + bool Exists(string domainName); + + IEnumerable GetAll(bool includeWildcards); + + IEnumerable GetAssignedDomains(int contentId, bool includeWildcards); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs index 6b8ece1bfd..3e2ae8c7b5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityContainerRepository.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public interface IEntityContainerRepository : IReadRepository, IWriteRepository - { - EntityContainer? Get(Guid id); +namespace Umbraco.Cms.Core.Persistence.Repositories; - IEnumerable Get(string name, int level); - } +public interface IEntityContainerRepository : IReadRepository, IWriteRepository +{ + EntityContainer? Get(Guid id); + + IEnumerable Get(string name, int level); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 8eeab0b834..ff7c8f12d9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -1,59 +1,70 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IEntityRepository : IRepository { - public interface IEntityRepository : IRepository - { - IEntitySlim? Get(int id); - IEntitySlim? Get(Guid key); - IEntitySlim? Get(int id, Guid objectTypeId); - IEntitySlim? Get(Guid key, Guid objectTypeId); + IEntitySlim? Get(int id); - IEnumerable GetAll(Guid objectType, params int[] ids); - IEnumerable GetAll(Guid objectType, params Guid[] keys); + IEntitySlim? Get(Guid key); - /// - /// Gets entities for a query - /// - /// - /// - IEnumerable GetByQuery(IQuery query); + IEntitySlim? Get(int id, Guid objectTypeId); - /// - /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized - /// - /// - /// - /// - IEnumerable GetByQuery(IQuery query, Guid objectType); + IEntitySlim? Get(Guid key, Guid objectTypeId); - UmbracoObjectTypes GetObjectType(int id); - UmbracoObjectTypes GetObjectType(Guid key); - int ReserveId(Guid key); + IEnumerable GetAll(Guid objectType, params int[] ids); - IEnumerable GetAllPaths(Guid objectType, params int[]? ids); - IEnumerable GetAllPaths(Guid objectType, params Guid[] keys); + IEnumerable GetAll(Guid objectType, params Guid[] keys); - bool Exists(int id); - bool Exists(Guid key); + /// + /// Gets entities for a query + /// + /// + /// + IEnumerable GetByQuery(IQuery query); - /// - /// Gets paged entities for a query and a specific object type - /// - /// - /// - /// - /// - /// - /// - /// - /// - IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering); - } + /// + /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized + /// + /// + /// + /// + IEnumerable GetByQuery(IQuery query, Guid objectType); + + UmbracoObjectTypes GetObjectType(int id); + + UmbracoObjectTypes GetObjectType(Guid key); + + int ReserveId(Guid key); + + IEnumerable GetAllPaths(Guid objectType, params int[]? ids); + + IEnumerable GetAllPaths(Guid objectType, params Guid[] keys); + + bool Exists(int id); + + bool Exists(Guid key); + + /// + /// Gets paged entities for a query and a specific object type + /// + /// + /// + /// + /// + /// + /// + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, + Guid objectType, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter, + Ordering? ordering); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs index 7d9594a3c6..6d7370768c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs @@ -1,29 +1,26 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IExternalLoginRepository : IReadWriteQueryRepository, + IQueryRepository { + /// + /// Replaces all external login providers for the user + /// + /// + /// + [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] + void Save(int userId, IEnumerable logins); - public interface IExternalLoginRepository : IReadWriteQueryRepository, IQueryRepository - { + /// + /// Replaces all external login provider tokens for the providers specified for the user + /// + /// + /// + [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] + void Save(int userId, IEnumerable tokens); - /// - /// Replaces all external login providers for the user - /// - /// - /// - [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] - void Save(int userId, IEnumerable logins); - - /// - /// Replaces all external login provider tokens for the providers specified for the user - /// - /// - /// - [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] - void Save(int userId, IEnumerable tokens); - [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] - void DeleteUserLogins(int memberId); - } + [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] + void DeleteUserLogins(int memberId); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs index 0a4b9e76cf..ec9a79530c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Repository for external logins with Guid as key, so it can be shared for members and users +/// +public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository, + IQueryRepository { + /// + /// Replaces all external login providers for the user/member key + /// + void Save(Guid userOrMemberKey, IEnumerable logins); /// - /// Repository for external logins with Guid as key, so it can be shared for members and users + /// Replaces all external login provider tokens for the providers specified for the user/member key /// - public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository, IQueryRepository - { - /// - /// Replaces all external login providers for the user/member key - /// - void Save(Guid userOrMemberKey, IEnumerable logins); + void Save(Guid userOrMemberKey, IEnumerable tokens); - /// - /// Replaces all external login provider tokens for the providers specified for the user/member key - /// - void Save(Guid userOrMemberKey, IEnumerable tokens); - - /// - /// Deletes all external logins for the specified the user/member key - /// - void DeleteUserLogins(Guid userOrMemberKey); - } + /// + /// Deletes all external logins for the specified the user/member key + /// + void DeleteUserLogins(Guid userOrMemberKey); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IFileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IFileRepository.cs index ce76086ed2..53e1bb4074 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IFileRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IFileRepository.cs @@ -1,13 +1,10 @@ -using System.IO; +namespace Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Persistence.Repositories +public interface IFileRepository { - public interface IFileRepository - { - Stream GetFileContentStream(string filepath); + Stream GetFileContentStream(string filepath); - void SetFileContent(string filepath, Stream content); + void SetFileContent(string filepath, Stream content); - long GetFileSize(string filepath); - } + long GetFileSize(string filepath); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs index 77c2f9d40b..9914e49b26 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public interface IFileWithFoldersRepository - { - void AddFolder(string folderPath); +namespace Umbraco.Cms.Core.Persistence.Repositories; - void DeleteFolder(string folderPath); - } +public interface IFileWithFoldersRepository +{ + void AddFolder(string folderPath); + + void DeleteFolder(string folderPath); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IIdKeyMapRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IIdKeyMapRepository.cs index b2c7bc9aa1..6520644a7f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IIdKeyMapRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IIdKeyMapRepository.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Persistence.Repositories; @@ -6,5 +5,6 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; public interface IIdKeyMapRepository { int? GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType); + Guid? GetIdForKey(int id, UmbracoObjectTypes umbracoObjectType); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs index 5dc7ab0555..f12bd612fc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs @@ -1,9 +1,6 @@ -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Persistence.Repositories +public interface IInstallationRepository { - public interface IInstallationRepository - { - Task SaveInstallLogAsync(InstallLog installLog); - } + Task SaveInstallLogAsync(InstallLog installLog); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs index c9ee7a9d25..c9792f009d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IKeyValueRepository : IReadRepository, IWriteRepository { - public interface IKeyValueRepository : IReadRepository, IWriteRepository - { - /// - /// Returns key/value pairs for all keys with the specified prefix. - /// - /// - /// - IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix); - } + /// + /// Returns key/value pairs for all keys with the specified prefix. + /// + /// + /// + IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs index 1be32de989..e7fff03bd7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs @@ -1,41 +1,40 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ILanguageRepository : IReadWriteQueryRepository { - public interface ILanguageRepository : IReadWriteQueryRepository - { - ILanguage? GetByIsoCode(string isoCode); + ILanguage? GetByIsoCode(string isoCode); - /// - /// Gets a language identifier from its ISO code. - /// - /// - /// This can be optimized and bypass all deep cloning. - /// - int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true); + /// + /// Gets a language identifier from its ISO code. + /// + /// + /// This can be optimized and bypass all deep cloning. + /// + int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true); - /// - /// Gets a language ISO code from its identifier. - /// - /// - /// This can be optimized and bypass all deep cloning. - /// - string? GetIsoCodeById(int? id, bool throwOnNotFound = true); + /// + /// Gets a language ISO code from its identifier. + /// + /// + /// This can be optimized and bypass all deep cloning. + /// + string? GetIsoCodeById(int? id, bool throwOnNotFound = true); - /// - /// Gets the default language ISO code. - /// - /// - /// This can be optimized and bypass all deep cloning. - /// - string GetDefaultIsoCode(); + /// + /// Gets the default language ISO code. + /// + /// + /// This can be optimized and bypass all deep cloning. + /// + string GetDefaultIsoCode(); - /// - /// Gets the default language identifier. - /// - /// - /// This can be optimized and bypass all deep cloning. - /// - int? GetDefaultId(); - } + /// + /// Gets the default language identifier. + /// + /// + /// This can be optimized and bypass all deep cloning. + /// + int? GetDefaultId(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs index 8e3d779b9d..0d1da11c9d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ILogViewerQueryRepository.cs @@ -1,9 +1,8 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ILogViewerQueryRepository : IReadWriteQueryRepository { - public interface ILogViewerQueryRepository : IReadWriteQueryRepository - { - ILogViewerQuery? GetByName(string name); - } + ILogViewerQuery? GetByName(string name); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMacroRepository.cs index 44ab86b80a..9d2fe0ecbf 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMacroRepository.cs @@ -1,12 +1,8 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMacroRepository : IReadWriteQueryRepository, IReadRepository { - public interface IMacroRepository : IReadWriteQueryRepository, IReadRepository - { - - //IEnumerable GetAll(params string[] aliases); - - } + // IEnumerable GetAll(params string[] aliases); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs index 46705d0ded..48ead78759 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - [Obsolete("This interface will be merged with IMacroRepository in Umbraco 11")] - public interface IMacroWithAliasRepository : IMacroRepository - { - IMacro? GetByAlias(string alias); +namespace Umbraco.Cms.Core.Persistence.Repositories; - IEnumerable GetAllByAlias(string[] aliases); - } +[Obsolete("This interface will be merged with IMacroRepository in Umbraco 11")] +public interface IMacroWithAliasRepository : IMacroRepository +{ + IMacro? GetByAlias(string alias); + + IEnumerable GetAllByAlias(string[] aliases); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs index ad268c6292..d51f031071 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs @@ -1,11 +1,10 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMediaRepository : IContentRepository, IReadRepository { - public interface IMediaRepository : IContentRepository, IReadRepository - { - IMedia? GetMediaByPath(string mediaPath); - bool RecycleBinSmells(); - } + IMedia? GetMediaByPath(string mediaPath); + + bool RecycleBinSmells(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeContainerRepository.cs index cf2c181d5f..fe8c798915 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMediaTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeContainerRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMediaTypeContainerRepository : IEntityContainerRepository { - public interface IMediaTypeContainerRepository : IEntityContainerRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs index 2a1168ae57..ac06431ee8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMediaTypeRepository.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMediaTypeRepository : IContentTypeRepositoryBase { - public interface IMediaTypeRepository : IContentTypeRepositoryBase - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs index a7187ec1ca..fc12afe1d3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs @@ -1,51 +1,46 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMemberGroupRepository : IReadWriteQueryRepository { - public interface IMemberGroupRepository : IReadWriteQueryRepository - { - /// - /// Gets a member group by it's uniqueId - /// - /// - /// - IMemberGroup? Get(Guid uniqueId); + /// + /// Gets a member group by it's uniqueId + /// + /// + /// + IMemberGroup? Get(Guid uniqueId); - /// - /// Gets a member group by it's name - /// - /// - /// - IMemberGroup? GetByName(string? name); + /// + /// Gets a member group by it's name + /// + /// + /// + IMemberGroup? GetByName(string? name); - /// - /// Creates the new member group if it doesn't already exist - /// - /// - IMemberGroup? CreateIfNotExists(string roleName); + /// + /// Creates the new member group if it doesn't already exist + /// + /// + IMemberGroup? CreateIfNotExists(string roleName); - /// - /// Returns the member groups for a given member - /// - /// - /// - IEnumerable GetMemberGroupsForMember(int memberId); + /// + /// Returns the member groups for a given member + /// + /// + /// + IEnumerable GetMemberGroupsForMember(int memberId); - /// - /// Returns the member groups for a given member - /// - /// - /// - IEnumerable GetMemberGroupsForMember(string? username); + /// + /// Returns the member groups for a given member + /// + /// + /// + IEnumerable GetMemberGroupsForMember(string? username); - void ReplaceRoles(int[] memberIds, string[] roleNames); + void ReplaceRoles(int[] memberIds, string[] roleNames); - void AssignRoles(int[] memberIds, string[] roleNames); + void AssignRoles(int[] memberIds, string[] roleNames); - void DissociateRoles(int[] memberIds, string[] roleNames); - - - } + void DissociateRoles(int[] memberIds, string[] roleNames); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs index 28a89ff43a..58475f802d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs @@ -1,57 +1,57 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMemberRepository : IContentRepository { - public interface IMemberRepository : IContentRepository - { - int[] GetMemberIds(string[] names); + int[] GetMemberIds(string[] names); - IMember? GetByUsername(string? username); + IMember? GetByUsername(string? username); - /// - /// Finds members in a given role - /// - /// - /// - /// - /// - IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + /// + /// Finds members in a given role + /// + /// + /// + /// + /// + IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - /// - /// Get all members in a specific group - /// - /// - /// - IEnumerable GetByMemberGroup(string groupName); + /// + /// Get all members in a specific group + /// + /// + /// + IEnumerable GetByMemberGroup(string groupName); - /// - /// Checks if a member with the username exists - /// - /// - /// - bool Exists(string username); + /// + /// Checks if a member with the username exists + /// + /// + /// + bool Exists(string username); - /// - /// Gets the count of items based on a complex query - /// - /// - /// - int GetCountByQuery(IQuery? query); + /// + /// Gets the count of items based on a complex query + /// + /// + /// + int GetCountByQuery(IQuery? query); - /// - /// Sets a members last login date based on their username - /// - /// - /// - /// - /// This is a specialized method because whenever a member logs in, the membership provider requires us to set the 'online' which requires - /// updating their login date. This operation must be fast and cannot use database locks which is fine if we are only executing a single query - /// for this data since there won't be any other data contention issues. - /// - [Obsolete("This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")] - void SetLastLogin(string username, DateTime date); - } + /// + /// Sets a members last login date based on their username + /// + /// + /// + /// + /// This is a specialized method because whenever a member logs in, the membership provider requires us to set the + /// 'online' which requires + /// updating their login date. This operation must be fast and cannot use database locks which is fine if we are only + /// executing a single query + /// for this data since there won't be any other data contention issues. + /// + [Obsolete( + "This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")] + void SetLastLogin(string username, DateTime date); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberTypeContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeContainerRepository.cs index 1ccf3e756c..255e872206 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberTypeContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeContainerRepository.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMemberTypeContainerRepository : IEntityContainerRepository { - public interface IMemberTypeContainerRepository : IEntityContainerRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs index 0b31f0ba46..f9cd35534a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberTypeRepository.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IMemberTypeRepository : IContentTypeRepositoryBase { - public interface IMemberTypeRepository : IContentTypeRepositoryBase - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs index 4ae191fa72..5f93a912fc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs @@ -1,9 +1,8 @@ -using System; - namespace Umbraco.Cms.Core.Persistence.Repositories; public interface INodeCountRepository { int GetNodeCount(Guid nodeType); + int GetMediaCount(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/INotificationsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INotificationsRepository.cs index be1a00a130..5a3f63f8cb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/INotificationsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/INotificationsRepository.cs @@ -1,20 +1,24 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface INotificationsRepository : IRepository { - public interface INotificationsRepository : IRepository - { - Notification CreateNotification(IUser user, IEntity entity, string action); - int DeleteNotifications(IUser user); - int DeleteNotifications(IEntity entity); - int DeleteNotifications(IUser user, IEntity entity); - IEnumerable? GetEntityNotifications(IEntity entity); - IEnumerable? GetUserNotifications(IUser user); - IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType); - IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions); - } + Notification CreateNotification(IUser user, IEntity entity, string action); + + int DeleteNotifications(IUser user); + + int DeleteNotifications(IEntity entity); + + int DeleteNotifications(IUser user, IEntity entity); + + IEnumerable? GetEntityNotifications(IEntity entity); + + IEnumerable? GetUserNotifications(IUser user); + + IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType); + + IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IPartialViewMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPartialViewMacroRepository.cs index c731d39780..ba6d24c2d8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IPartialViewMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IPartialViewMacroRepository.cs @@ -1,9 +1,9 @@ -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +// this only exists to differentiate with IPartialViewRepository in IoC +// without resorting to constants, names, whatever - and IPartialViewRepository +// is implemented by PartialViewRepository and IPartialViewMacroRepository by +// PartialViewMacroRepository - just to inject the proper filesystem. +public interface IPartialViewMacroRepository : IPartialViewRepository { - // this only exists to differentiate with IPartialViewRepository in IoC - // without resorting to constants, names, whatever - and IPartialViewRepository - // is implemented by PartialViewRepository and IPartialViewMacroRepository by - // PartialViewMacroRepository - just to inject the proper filesystem. - public interface IPartialViewMacroRepository : IPartialViewRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IPartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPartialViewRepository.cs index a8a84079fa..72b8fa2af0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IPartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IPartialViewRepository.cs @@ -1,8 +1,8 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IPartialViewRepository : IReadRepository, IWriteRepository, + IFileRepository, IFileWithFoldersRepository { - public interface IPartialViewRepository : IReadRepository, IWriteRepository, IFileRepository, IFileWithFoldersRepository - { - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs index 2190782d3b..84ef0e92f5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IPublicAccessRepository.cs @@ -1,8 +1,7 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IPublicAccessRepository : IReadWriteQueryRepository { - public interface IPublicAccessRepository : IReadWriteQueryRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs index 17be5b3856..b6393dfcc0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs @@ -1,89 +1,86 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +/// +/// Defines the repository. +/// +public interface IRedirectUrlRepository : IReadWriteQueryRepository { /// - /// Defines the repository. + /// Gets a redirect URL. /// - public interface IRedirectUrlRepository : IReadWriteQueryRepository - { - /// - /// Gets a redirect URL. - /// - /// The Umbraco redirect URL route. - /// The content unique key. - /// The culture. - /// - IRedirectUrl? Get(string url, Guid contentKey, string? culture); + /// The Umbraco redirect URL route. + /// The content unique key. + /// The culture. + /// + IRedirectUrl? Get(string url, Guid contentKey, string? culture); - /// - /// Deletes a redirect URL. - /// - /// The redirect URL identifier. - void Delete(Guid id); + /// + /// Deletes a redirect URL. + /// + /// The redirect URL identifier. + void Delete(Guid id); - /// - /// Deletes all redirect URLs. - /// - void DeleteAll(); + /// + /// Deletes all redirect URLs. + /// + void DeleteAll(); - /// - /// Deletes all redirect URLs for a given content. - /// - /// The content unique key. - void DeleteContentUrls(Guid contentKey); + /// + /// Deletes all redirect URLs for a given content. + /// + /// The content unique key. + void DeleteContentUrls(Guid contentKey); - /// - /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. - /// - /// The Umbraco redirect URL route. - /// The most recent redirect URL corresponding to the route. - IRedirectUrl? GetMostRecentUrl(string url); + /// + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The most recent redirect URL corresponding to the route. + IRedirectUrl? GetMostRecentUrl(string url); - /// - /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. - /// - /// The Umbraco redirect URL route. - /// The culture the domain is associated with - /// The most recent redirect URL corresponding to the route. - IRedirectUrl? GetMostRecentUrl(string url, string culture); + /// + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The culture the domain is associated with + /// The most recent redirect URL corresponding to the route. + IRedirectUrl? GetMostRecentUrl(string url, string culture); - /// - /// Gets all redirect URLs for a content item. - /// - /// The content unique key. - /// All redirect URLs for the content item. - IEnumerable GetContentUrls(Guid contentKey); + /// + /// Gets all redirect URLs for a content item. + /// + /// The content unique key. + /// All redirect URLs for the content item. + IEnumerable GetContentUrls(Guid contentKey); - /// - /// Gets all redirect URLs. - /// - /// The page index. - /// The page size. - /// The total count of redirect URLs. - /// The redirect URLs. - IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total); + /// + /// Gets all redirect URLs. + /// + /// The page index. + /// The page size. + /// The total count of redirect URLs. + /// The redirect URLs. + IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total); - /// - /// Gets all redirect URLs below a given content item. - /// - /// The content unique identifier. - /// The page index. - /// The page size. - /// The total count of redirect URLs. - /// The redirect URLs. - IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total); + /// + /// Gets all redirect URLs below a given content item. + /// + /// The content unique identifier. + /// The page index. + /// The page size. + /// The total count of redirect URLs. + /// The redirect URLs. + IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total); - /// - /// Searches for all redirect URLs that contain a given search term in their URL property. - /// - /// The term to search for. - /// The page index. - /// The page size. - /// The total count of redirect URLs. - /// The redirect URLs. - IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total); - } + /// + /// Searches for all redirect URLs that contain a given search term in their URL property. + /// + /// The term to search for. + /// The page index. + /// The page size. + /// The total count of redirect URLs. + /// The redirect URLs. + IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 0165b9eb39..8077a80dc1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -1,39 +1,36 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IRelationRepository : IReadWriteQueryRepository { - public interface IRelationRepository : IReadWriteQueryRepository - { - IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering); + IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering); - /// - /// Persist multiple at once - /// - /// - void Save(IEnumerable relations); + /// + /// Persist multiple at once + /// + /// + void Save(IEnumerable relations); - /// - /// Persist multiple at once but Ids are not returned on created relations - /// - /// - void SaveBulk(IEnumerable relations); + /// + /// Persist multiple at once but Ids are not returned on created relations + /// + /// + void SaveBulk(IEnumerable relations); - /// - /// Deletes all relations for a parent for any specified relation type alias - /// - /// - /// - /// A list of relation types to match for deletion, if none are specified then all relations for this parent id are deleted - /// - void DeleteByParent(int parentId, params string[] relationTypeAliases); + /// + /// Deletes all relations for a parent for any specified relation type alias + /// + /// + /// + /// A list of relation types to match for deletion, if none are specified then all relations for this parent id are deleted. + /// + void DeleteByParent(int parentId, params string[] relationTypeAliases); - IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); + IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); - IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); - } + IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationTypeRepository.cs index 26dfe4acba..19929ee83f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationTypeRepository.cs @@ -1,8 +1,8 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IRelationTypeRepository : IReadWriteQueryRepository, + IReadRepository { - public interface IRelationTypeRepository : IReadWriteQueryRepository, IReadRepository - { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IScriptRepository.cs index 604e1da8d2..f0cfe94902 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IScriptRepository.cs @@ -1,9 +1,8 @@ -using System.IO; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IScriptRepository : IReadRepository, IWriteRepository, IFileRepository, + IFileWithFoldersRepository { - public interface IScriptRepository : IReadRepository, IWriteRepository, IFileRepository, IFileWithFoldersRepository - { - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IServerRegistrationRepository.cs index af3555160e..5593dec09a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IServerRegistrationRepository.cs @@ -1,12 +1,10 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public interface IServerRegistrationRepository : IReadWriteQueryRepository - { - void DeactiveStaleServers(TimeSpan staleTimeout); +namespace Umbraco.Cms.Core.Persistence.Repositories; - void ClearCache(); - } +public interface IServerRegistrationRepository : IReadWriteQueryRepository +{ + void DeactiveStaleServers(TimeSpan staleTimeout); + + void ClearCache(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs index dcdb5debe7..29f132a74a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IStylesheetRepository.cs @@ -1,8 +1,8 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IStylesheetRepository : IReadRepository, IWriteRepository, + IFileRepository, IFileWithFoldersRepository { - public interface IStylesheetRepository : IReadRepository, IWriteRepository, IFileRepository, IFileWithFoldersRepository - { - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs index e2fa2e4406..35c134adb3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITagRepository.cs @@ -1,95 +1,98 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ITagRepository : IReadWriteQueryRepository { - public interface ITagRepository : IReadWriteQueryRepository - { - #region Assign and Remove Tags + #region Assign and Remove Tags - /// - /// Assign tags to a content property. - /// - /// The identifier of the content item. - /// The identifier of the property type. - /// The tags to assign. - /// A value indicating whether to replace already assigned tags. - /// - /// When is false, the tags specified in are added to those already assigned. - /// When is empty and is true, all assigned tags are removed. - /// - // TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it - void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true); + /// + /// Assign tags to a content property. + /// + /// The identifier of the content item. + /// The identifier of the property type. + /// The tags to assign. + /// A value indicating whether to replace already assigned tags. + /// + /// + /// When is false, the tags specified in are added to + /// those already assigned. + /// + /// + /// When is empty and is true, all assigned tags are + /// removed. + /// + /// + // TODO: replaceTags is used as 'false' in tests exclusively - should get rid of it + void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true); - /// - /// Removes assigned tags from a content property. - /// - /// The identifier of the content item. - /// The identifier of the property type. - /// The tags to remove. - void Remove(int contentId, int propertyTypeId, IEnumerable tags); + /// + /// Removes assigned tags from a content property. + /// + /// The identifier of the content item. + /// The identifier of the property type. + /// The tags to remove. + void Remove(int contentId, int propertyTypeId, IEnumerable tags); - /// - /// Removes all assigned tags from a content item. - /// - /// The identifier of the content item. - void RemoveAll(int contentId); + /// + /// Removes all assigned tags from a content item. + /// + /// The identifier of the content item. + void RemoveAll(int contentId); - /// - /// Removes all assigned tags from a content property. - /// - /// The identifier of the content item. - /// The identifier of the property type. - void RemoveAll(int contentId, int propertyTypeId); + /// + /// Removes all assigned tags from a content property. + /// + /// The identifier of the content item. + /// The identifier of the property type. + void RemoveAll(int contentId, int propertyTypeId); - #endregion + #endregion - #region Queries + #region Queries - /// - /// Gets a tagged entity. - /// - TaggedEntity? GetTaggedEntityByKey(Guid key); + /// + /// Gets a tagged entity. + /// + TaggedEntity? GetTaggedEntityByKey(Guid key); - /// - /// Gets a tagged entity. - /// - TaggedEntity? GetTaggedEntityById(int id); + /// + /// Gets a tagged entity. + /// + TaggedEntity? GetTaggedEntityById(int id); - /// Gets all entities of a type, tagged with any tag in the specified group. - IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string? culture = null); + /// Gets all entities of a type, tagged with any tag in the specified group. + IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string? culture = null); - /// - /// Gets all entities of a type, tagged with the specified tag. - /// - IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string? group = null, string? culture = null); + /// + /// Gets all entities of a type, tagged with the specified tag. + /// + IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string? group = null, string? culture = null); - /// - /// Gets all tags for an entity type. - /// - IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, string? culture = null); + /// + /// Gets all tags for an entity type. + /// + IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity. - /// - IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity. + /// + IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity. - /// - IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity. + /// + IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity via a property. - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity via a property. - /// - IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null); - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs index 185973623c..5c5881ef7a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ITemplateRepository : IReadWriteQueryRepository, IFileRepository { - public interface ITemplateRepository : IReadWriteQueryRepository, IFileRepository - { - ITemplate? Get(string? alias); + ITemplate? Get(string? alias); - IEnumerable GetAll(params string[] aliases); + IEnumerable GetAll(params string[] aliases); - IEnumerable GetChildren(int masterTemplateId); + IEnumerable GetChildren(int masterTemplateId); - IEnumerable GetDescendants(int masterTemplateId); - } + IEnumerable GetDescendants(int masterTemplateId); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs index e6ca8eaa50..a69722c04a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs @@ -1,42 +1,49 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ITrackedReferencesRepository { - public interface ITrackedReferencesRepository - { - /// - /// Gets a page of items which are in relation with the current item. - /// Basically, shows the items which depend on the current item. - /// - /// The identifier of the entity to retrieve relations for. - /// The page index. - /// The page size. - /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). - /// The total count of the items with reference to the current item. - /// An enumerable list of objects. - IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); + /// + /// Gets a page of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + /// The identifier of the entity to retrieve relations for. + /// The page index. + /// The page size. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of the items with reference to the current item. + /// An enumerable list of objects. + IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); - /// - /// Gets a page of items used in any kind of relation from selected integer ids. - /// - /// The identifiers of the entities to check for relations. - /// The page index. - /// The page size. - /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). - /// The total count of the items in any kind of relation. - /// An enumerable list of objects. - IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); + /// + /// Gets a page of items used in any kind of relation from selected integer ids. + /// + /// The identifiers of the entities to check for relations. + /// The page index. + /// The page size. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of the items in any kind of relation. + /// An enumerable list of objects. + IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); - /// - /// Gets a page of the descending items that have any references, given a parent id. - /// - /// The unique identifier of the parent to retrieve descendants for. - /// The page index. - /// The page size. - /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). - /// The total count of descending items. - /// An enumerable list of objects. - IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); - } + /// + /// Gets a page of the descending items that have any references, given a parent id. + /// + /// The unique identifier of the parent to retrieve descendants for. + /// The page index. + /// The page size. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of descending items. + /// An enumerable list of objects. + IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs index 63622f8e82..31a279eb62 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface ITwoFactorLoginRepository : IReadRepository, IWriteRepository { - public interface ITwoFactorLoginRepository: IReadRepository, IWriteRepository - { - Task DeleteUserLoginsAsync(Guid userOrMemberKey); - Task DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName); + Task DeleteUserLoginsAsync(Guid userOrMemberKey); - Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey); - } + Task DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName); + Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs index d64f177f14..7a0d8b6f74 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs @@ -1,10 +1,8 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IUpgradeCheckRepository { - public interface IUpgradeCheckRepository - { - Task CheckUpgradeAsync(SemVersion version); - } + Task CheckUpgradeAsync(SemVersion version); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs index d5cf6fd762..0959019af2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs @@ -1,59 +1,63 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IUserGroupRepository : IReadWriteQueryRepository { - public interface IUserGroupRepository : IReadWriteQueryRepository - { - /// - /// Gets a group by it's alias - /// - /// - /// - IUserGroup? Get(string alias); + /// + /// Gets a group by it's alias + /// + /// + /// + IUserGroup? Get(string alias); - /// - /// This is useful when an entire section is removed from config - /// - /// - IEnumerable GetGroupsAssignedToSection(string sectionAlias); + /// + /// This is useful when an entire section is removed from config + /// + /// + IEnumerable GetGroupsAssignedToSection(string sectionAlias); - /// - /// Used to add or update a user group and assign users to it - /// - /// - /// - void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds); + /// + /// Used to add or update a user group and assign users to it + /// + /// + /// + void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds); - /// - /// Gets explicitly defined permissions for the group for specified entities - /// - /// - /// Array of entity Ids, if empty will return permissions for the group for all entities - EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds); + /// + /// Gets explicitly defined permissions for the group for specified entities + /// + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds); - /// - /// Gets explicit and default permissions (if requested) permissions for the group for specified entities - /// - /// - /// If true will include the group's default permissions if no permissions are explicitly assigned - /// Array of entity Ids, if empty will return permissions for the group for all entities - EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds); + /// + /// Gets explicit and default permissions (if requested) permissions for the group for specified entities + /// + /// + /// + /// If true will include the group's default permissions if no permissions are + /// explicitly assigned + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds); - /// - /// Replaces the same permission set for a single group to any number of entities - /// - /// Id of group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. - void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds); + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// + /// Specify the nodes to replace permissions for. If nothing is specified all permissions are + /// removed. + /// + void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds); - /// - /// Assigns the same permission set for a single group to any number of entities - /// - /// Id of group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for - void AssignGroupPermission(int groupId, char permission, params int[] entityIds); - } + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for + void AssignGroupPermission(int groupId, char permission, params int[] entityIds); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs index 8357729f38..893a3c248e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs @@ -1,112 +1,121 @@ -using System; -using System.Collections.Generic; using System.Linq.Expressions; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IUserRepository : IReadWriteQueryRepository { - public interface IUserRepository : IReadWriteQueryRepository - { - /// - /// Gets the count of items based on a complex query - /// - /// - /// - int GetCountByQuery(IQuery? query); + /// + /// Gets the count of items based on a complex query + /// + /// + /// + int GetCountByQuery(IQuery? query); - /// - /// Checks if a user with the username exists - /// - /// - /// - [Obsolete("This method will be removed in future versions. Please use ExistsByUserName instead.")] - bool Exists(string username); + /// + /// Checks if a user with the username exists + /// + /// + /// + [Obsolete("This method will be removed in future versions. Please use ExistsByUserName instead.")] + bool Exists(string username); - /// - /// Checks if a user with the username exists - /// - /// - /// - bool ExistsByUserName(string username); + /// + /// Checks if a user with the username exists + /// + /// + /// + bool ExistsByUserName(string username); + /// + /// Checks if a user with the login exists + /// + /// + /// + bool ExistsByLogin(string login); - /// - /// Checks if a user with the login exists - /// - /// - /// - bool ExistsByLogin(string login); + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + IEnumerable GetAllInGroup(int groupId); - /// - /// Gets a list of objects associated with a given group - /// - /// Id of group - IEnumerable GetAllInGroup(int groupId); + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + IEnumerable GetAllNotInGroup(int groupId); - /// - /// Gets a list of objects not associated with a given group - /// - /// Id of group - IEnumerable GetAllNotInGroup(int groupId); + /// + /// Gets paged user results + /// + /// + /// + /// + /// + /// + /// + /// + /// A filter to only include user that belong to these user groups + /// + /// + /// A filter to only include users that do not belong to these user groups + /// + /// Optional parameter to filter by specified user state + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, + Expression> orderBy, + Direction orderDirection = Direction.Ascending, + string[]? includeUserGroups = null, + string[]? excludeUserGroups = null, + UserState[]? userState = null, + IQuery? filter = null); - /// - /// Gets paged user results - /// - /// - /// - /// - /// - /// - /// - /// - /// A filter to only include user that belong to these user groups - /// - /// - /// A filter to only include users that do not belong to these user groups - /// - /// Optional parameter to filter by specified user state - /// - /// - IEnumerable GetPagedResultsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, - Expression> orderBy, Direction orderDirection = Direction.Ascending, - string[]? includeUserGroups = null, string[]? excludeUserGroups = null, UserState[]? userState = null, - IQuery? filter = null); + /// + /// Returns a user by username + /// + /// + /// + /// This is only used for a shim in order to upgrade to 7.7 + /// + /// + /// A non cached instance + /// + IUser? GetByUsername(string username, bool includeSecurityData); - /// - /// Returns a user by username - /// - /// - /// - /// This is only used for a shim in order to upgrade to 7.7 - /// - /// - /// A non cached instance - /// - IUser? GetByUsername(string username, bool includeSecurityData); + /// + /// Returns a user by id + /// + /// + /// + /// This is only used for a shim in order to upgrade to 7.7 + /// + /// + /// A non cached instance + /// + IUser? Get(int? id, bool includeSecurityData); - /// - /// Returns a user by id - /// - /// - /// - /// This is only used for a shim in order to upgrade to 7.7 - /// - /// - /// A non cached instance - /// - IUser? Get(int? id, bool includeSecurityData); + IProfile? GetProfile(string username); - IProfile? GetProfile(string username); - IProfile? GetProfile(int id); - IDictionary GetUserStates(); + IProfile? GetProfile(int id); - Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true); - bool ValidateLoginSession(int userId, Guid sessionId); - int ClearLoginSessions(int userId); - int ClearLoginSessions(TimeSpan timespan); - void ClearLoginSession(Guid sessionId); + IDictionary GetUserStates(); - IEnumerable GetNextUsers(int id, int count); - } + Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true); + + bool ValidateLoginSession(int userId, Guid sessionId); + + int ClearLoginSessions(int userId); + + int ClearLoginSessions(TimeSpan timespan); + + void ClearLoginSession(Guid sessionId); + + IEnumerable GetNextUsers(int id, int count); } diff --git a/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs index cd3e31559b..c30015a7a0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs @@ -1,35 +1,33 @@ -using System.Net.Http; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Persistence.Repositories -{ - public class InstallationRepository : IInstallationRepository - { - private readonly IJsonSerializer _jsonSerializer; - private static HttpClient? _httpClient; - private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; +namespace Umbraco.Cms.Core.Persistence.Repositories; - public InstallationRepository(IJsonSerializer jsonSerializer) +public class InstallationRepository : IInstallationRepository +{ + private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; + private static HttpClient? _httpClient; + private readonly IJsonSerializer _jsonSerializer; + + public InstallationRepository(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + public async Task SaveInstallLogAsync(InstallLog installLog) + { + try { - _jsonSerializer = jsonSerializer; + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } + + var content = new StringContent(_jsonSerializer.Serialize(installLog), Encoding.UTF8, "application/json"); + + await _httpClient.PostAsync(RestApiInstallUrl, content); } - public async Task SaveInstallLogAsync(InstallLog installLog) + // this occurs if the server for Our is down or cannot be reached + catch (HttpRequestException) { - try - { - if (_httpClient == null) - _httpClient = new HttpClient(); - - var content = new StringContent(_jsonSerializer.Serialize(installLog), Encoding.UTF8, "application/json"); - - await _httpClient.PostAsync(RestApiInstallUrl, content); - } - // this occurs if the server for Our is down or cannot be reached - catch (HttpRequestException) - { } } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs index db0ebd7be5..a6b6c16aa5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheKeys.cs @@ -1,37 +1,31 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Core.Persistence.Repositories +/// +/// Provides cache keys for repositories. +/// +public static class RepositoryCacheKeys { - /// - /// Provides cache keys for repositories. - /// - public static class RepositoryCacheKeys + // used to cache keys so we don't keep allocating strings + private static readonly Dictionary Keys = new(); + + public static string GetKey() { - // used to cache keys so we don't keep allocating strings - private static readonly Dictionary s_keys = new Dictionary(); + Type type = typeof(T); + return Keys.TryGetValue(type, out var key) ? key : Keys[type] = "uRepo_" + type.Name + "_"; + } - public static string GetKey() + public static string GetKey(TId? id) + { + if (EqualityComparer.Default.Equals(id, default)) { - Type type = typeof(T); - return s_keys.TryGetValue(type, out var key) ? key : (s_keys[type] = "uRepo_" + type.Name + "_"); + return string.Empty; } - public static string GetKey(TId? id) + if (typeof(TId).IsValueType) { - if (EqualityComparer.Default.Equals(id, default)) - { - return string.Empty; - } - - if (typeof(TId).IsValueType) - { - return GetKey() + id; - } - else - { - return GetKey() + id?.ToString()?.ToUpperInvariant(); - } + return GetKey() + id; } + + return GetKey() + id?.ToString()?.ToUpperInvariant(); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs index c36156e54b..4d4e642d9d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs @@ -1,59 +1,58 @@ -using System; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Persistence.Repositories +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public class UpgradeCheckRepository : IUpgradeCheckRepository { - public class UpgradeCheckRepository : IUpgradeCheckRepository + private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; + private static HttpClient? _httpClient; + private readonly IJsonSerializer _jsonSerializer; + + public UpgradeCheckRepository(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + public async Task CheckUpgradeAsync(SemVersion version) { - private readonly IJsonSerializer _jsonSerializer; - private static HttpClient? _httpClient; - private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; - - public UpgradeCheckRepository(IJsonSerializer jsonSerializer) + try { - _jsonSerializer = jsonSerializer; + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } + + var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); + + _httpClient.Timeout = TimeSpan.FromSeconds(1); + HttpResponseMessage task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content); + var json = await task.Content.ReadAsStringAsync(); + UpgradeResult? result = _jsonSerializer.Deserialize(json); + + return result ?? new UpgradeResult("None", string.Empty, string.Empty); } - - public async Task CheckUpgradeAsync(SemVersion version) + catch (HttpRequestException) { - try - { - if (_httpClient == null) - _httpClient = new HttpClient(); - - var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); - - _httpClient.Timeout = TimeSpan.FromSeconds(1); - var task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl,content); - var json = await task.Content.ReadAsStringAsync(); - var result = _jsonSerializer.Deserialize(json); - - return result ?? new UpgradeResult("None", "", ""); - } - catch (HttpRequestException) - { - // this occurs if the server for Our is down or cannot be reached - return new UpgradeResult("None", "", ""); - } - } - private class CheckUpgradeDto - { - public CheckUpgradeDto(SemVersion version) - { - VersionMajor = version.Major; - VersionMinor = version.Minor; - VersionPatch = version.Patch; - VersionComment = version.Prerelease; - } - - public int VersionMajor { get; } - public int VersionMinor { get; } - public int VersionPatch { get; } - public string VersionComment { get; } + // this occurs if the server for Our is down or cannot be reached + return new UpgradeResult("None", string.Empty, string.Empty); } } + + private class CheckUpgradeDto + { + public CheckUpgradeDto(SemVersion version) + { + VersionMajor = version.Major; + VersionMinor = version.Minor; + VersionPatch = version.Patch; + VersionComment = version.Prerelease; + } + + public int VersionMajor { get; } + + public int VersionMinor { get; } + + public int VersionPatch { get; } + + public string VersionComment { get; } + } } diff --git a/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs b/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs index 8eb27f1a81..20db5106d7 100644 --- a/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlExpressionExtensions.cs @@ -1,49 +1,48 @@ -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +/// +/// String extension methods used specifically to translate into SQL +/// +public static class SqlExpressionExtensions { /// - /// String extension methods used specifically to translate into SQL + /// Indicates whether two nullable values are equal, substituting a fallback value for nulls. /// - public static class SqlExpressionExtensions + /// The nullable type. + /// The value to compare. + /// The value to compare to. + /// The value to use when any value is null. + /// Do not use outside of Sql expressions. + // see usage in ExpressionVisitorBase + public static bool SqlNullableEquals(this T? value, T? other, T fallbackValue) + where T : struct => + (value ?? fallbackValue).Equals(other ?? fallbackValue); + + public static bool SqlIn(this IEnumerable collection, T item) => collection.Contains(item); + + public static bool SqlWildcard(this string str, string txt, TextColumnType columnType) { - /// - /// Indicates whether two nullable values are equal, substituting a fallback value for nulls. - /// - /// The nullable type. - /// The value to compare. - /// The value to compare to. - /// The value to use when any value is null. - /// Do not use outside of Sql expressions. - // see usage in ExpressionVisitorBase - public static bool SqlNullableEquals(this T? value, T? other, T fallbackValue) - where T : struct - { - return (value ?? fallbackValue).Equals(other ?? fallbackValue); - } + var wildcardmatch = new Regex("^" + Regex.Escape(txt). - public static bool SqlIn(this IEnumerable collection, T item) => collection.Contains(item); + // deal with any wildcard chars % + Replace(@"\%", ".*") + "$"); - public static bool SqlWildcard(this string str, string txt, TextColumnType columnType) - { - var wildcardmatch = new Regex("^" + Regex.Escape(txt). - //deal with any wildcard chars % - Replace(@"\%", ".*") + "$"); - - return wildcardmatch.IsMatch(str); - } + return wildcardmatch.IsMatch(str); + } #pragma warning disable IDE0060 // Remove unused parameter - public static bool SqlContains(this string str, string txt, TextColumnType columnType) => str.InvariantContains(txt); + public static bool SqlContains(this string str, string txt, TextColumnType columnType) => + str.InvariantContains(txt); - public static bool SqlEquals(this string str, string txt, TextColumnType columnType) => str.InvariantEquals(txt); + public static bool SqlEquals(this string str, string txt, TextColumnType columnType) => str.InvariantEquals(txt); - public static bool SqlStartsWith(this string? str, string txt, TextColumnType columnType) => str?.InvariantStartsWith(txt) ?? false; + public static bool SqlStartsWith(this string? str, string txt, TextColumnType columnType) => + str?.InvariantStartsWith(txt) ?? false; - public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) => str.InvariantEndsWith(txt); + public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) => + str.InvariantEndsWith(txt); #pragma warning restore IDE0060 // Remove unused parameter - } } diff --git a/src/Umbraco.Core/Persistence/SqlExtensionsStatics.cs b/src/Umbraco.Core/Persistence/SqlExtensionsStatics.cs index d0f32fb971..506e516447 100644 --- a/src/Umbraco.Core/Persistence/SqlExtensionsStatics.cs +++ b/src/Umbraco.Core/Persistence/SqlExtensionsStatics.cs @@ -1,45 +1,44 @@ -using System; +namespace Umbraco.Cms.Core.Persistence; -namespace Umbraco.Cms.Core.Persistence +/// +/// Provides a mean to express aliases in SELECT Sql statements. +/// +/// +/// +/// First register with using static Umbraco.Core.Persistence.NPocoSqlExtensions.Aliaser, +/// then use eg Sql{Foo}(x => Alias(x.Id, "id")). +/// +/// +public static class SqlExtensionsStatics { /// - /// Provides a mean to express aliases in SELECT Sql statements. + /// Aliases a field. /// - /// - /// First register with using static Umbraco.Core.Persistence.NPocoSqlExtensions.Aliaser, - /// then use eg Sql{Foo}(x => Alias(x.Id, "id")). - /// - public static class SqlExtensionsStatics - { - /// - /// Aliases a field. - /// - /// The field to alias. - /// The alias. - public static object? Alias(object? field, string alias) => field; + /// The field to alias. + /// The alias. + public static object? Alias(object? field, string alias) => field; - /// - /// Produces Sql text. - /// - /// The name of the field. - /// A function producing Sql text. - public static T? SqlText(string field, Func expr) => default; + /// + /// Produces Sql text. + /// + /// The name of the field. + /// A function producing Sql text. + public static T? SqlText(string field, Func expr) => default; - /// - /// Produces Sql text. - /// - /// The name of the first field. - /// The name of the second field. - /// A function producing Sql text. - public static T? SqlText(string field1, string field2, Func expr) => default; + /// + /// Produces Sql text. + /// + /// The name of the first field. + /// The name of the second field. + /// A function producing Sql text. + public static T? SqlText(string field1, string field2, Func expr) => default; - /// - /// Produces Sql text. - /// - /// The name of the first field. - /// The name of the second field. - /// The name of the third field. - /// A function producing Sql text. - public static T? SqlText(string field1, string field2, string field3, Func expr) => default; - } + /// + /// Produces Sql text. + /// + /// The name of the first field. + /// The name of the second field. + /// The name of the third field. + /// A function producing Sql text. + public static T? SqlText(string field1, string field2, string field3, Func expr) => default; } diff --git a/src/Umbraco.Core/Persistence/TextColumnType.cs b/src/Umbraco.Core/Persistence/TextColumnType.cs index dc0b8d56bd..9e3a4dd71b 100644 --- a/src/Umbraco.Core/Persistence/TextColumnType.cs +++ b/src/Umbraco.Core/Persistence/TextColumnType.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Persistence +namespace Umbraco.Cms.Core.Persistence; + +public enum TextColumnType { - public enum TextColumnType - { - NVarchar, - NText - } + NVarchar, + NText, } diff --git a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs index c799a00df6..5e038f0e76 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs @@ -1,71 +1,68 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// The configuration object for the Block List editor +/// +public class BlockListConfiguration { - /// - /// The configuration object for the Block List editor - /// - public class BlockListConfiguration + [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")] + public BlockConfiguration[] Blocks { get; set; } = null!; + + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] + public NumberRange ValidationLimit { get; set; } = new(); + + [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views or labels using custom expression.")] + public bool UseLiveEditing { get; set; } + + [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] + public bool UseInlineEditingAsDefault { get; set; } + + [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] + public string? MaxPropertyWidth { get; set; } + + [DataContract] + public class BlockConfiguration { - [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")] - public BlockConfiguration[] Blocks { get; set; } = null!; + [DataMember(Name = "backgroundColor")] + public string? BackgroundColor { get; set; } - [DataContract] - public class BlockConfiguration - { + [DataMember(Name = "iconColor")] + public string? IconColor { get; set; } - [DataMember(Name ="backgroundColor")] - public string? BackgroundColor { get; set; } + [DataMember(Name = "thumbnail")] + public string? Thumbnail { get; set; } - [DataMember(Name ="iconColor")] - public string? IconColor { get; set; } + [DataMember(Name = "contentElementTypeKey")] + public Guid ContentElementTypeKey { get; set; } - [DataMember(Name ="thumbnail")] - public string? Thumbnail { get; set; } + [DataMember(Name = "settingsElementTypeKey")] + public Guid? SettingsElementTypeKey { get; set; } - [DataMember(Name ="contentElementTypeKey")] - public Guid ContentElementTypeKey { get; set; } + [DataMember(Name = "view")] + public string? View { get; set; } - [DataMember(Name ="settingsElementTypeKey")] - public Guid? SettingsElementTypeKey { get; set; } + [DataMember(Name = "stylesheet")] + public string? Stylesheet { get; set; } - [DataMember(Name ="view")] - public string? View { get; set; } + [DataMember(Name = "label")] + public string? Label { get; set; } - [DataMember(Name ="stylesheet")] - public string? Stylesheet { get; set; } + [DataMember(Name = "editorSize")] + public string? EditorSize { get; set; } - [DataMember(Name ="label")] - public string? Label { get; set; } + [DataMember(Name = "forceHideContentEditorInOverlay")] + public bool ForceHideContentEditorInOverlay { get; set; } + } - [DataMember(Name ="editorSize")] - public string? EditorSize { get; set; } + [DataContract] + public class NumberRange + { + [DataMember(Name = "min")] + public int? Min { get; set; } - [DataMember(Name ="forceHideContentEditorInOverlay")] - public bool ForceHideContentEditorInOverlay { get; set; } - } - - [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] - public NumberRange ValidationLimit { get; set; } = new NumberRange(); - - [DataContract] - public class NumberRange - { - [DataMember(Name ="min")] - public int? Min { get; set; } - - [DataMember(Name ="max")] - public int? Max { get; set; } - } - - [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views or labels using custom expression.")] - public bool UseLiveEditing { get; set; } - - [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] - public bool UseInlineEditingAsDefault { get; set; } - - [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] - public string? MaxPropertyWidth { get; set; } + [DataMember(Name = "max")] + public int? Max { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs index 80350bb350..02fc30d68b 100644 --- a/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ColorPickerConfiguration.cs @@ -1,11 +1,14 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the color picker value editor. +/// +public class ColorPickerConfiguration : ValueListConfiguration { - /// - /// Represents the configuration for the color picker value editor. - /// - public class ColorPickerConfiguration : ValueListConfiguration - { - [ConfigurationField("useLabel", "Include labels?", "boolean", Description = "Stores colors as a Json object containing both the color hex string and label, rather than just the hex string.")] - public bool UseLabel { get; set; } - } + [ConfigurationField( + "useLabel", + "Include labels?", + "boolean", + Description = "Stores colors as a Json object containing both the color hex string and label, rather than just the hex string.")] + public bool UseLabel { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index 89d19c5115..25aeb93418 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -1,137 +1,149 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Cms.Core.Serialization; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a data type configuration editor. +/// +[DataContract] +public class ConfigurationEditor : IConfigurationEditor { + private IDictionary _defaultConfiguration; + /// - /// Represents a data type configuration editor. + /// Initializes a new instance of the class. /// - [DataContract] - public class ConfigurationEditor : IConfigurationEditor + public ConfigurationEditor() { - private IDictionary _defaultConfiguration; - - /// - /// Initializes a new instance of the class. - /// - public ConfigurationEditor() - { - Fields = new List(); - _defaultConfiguration = new Dictionary(); - } - - /// - /// Initializes a new instance of the class. - /// - protected ConfigurationEditor(List fields) - { - Fields = fields; - _defaultConfiguration = new Dictionary(); - } - - /// - /// Gets the fields. - /// - [DataMember(Name = "fields")] - public List Fields { get; } - - /// - /// Gets a field by its property name. - /// - /// Can be used in constructors to add infos to a field that has been defined - /// by a property marked with the . - protected ConfigurationField Field(string name) - => Fields.First(x => x.PropertyName == name); - - /// - /// Gets the configuration as a typed object. - /// - public static TConfiguration? ConfigurationAs(object? obj) - { - if (obj == null) return default; - if (obj is TConfiguration configuration) return configuration; - throw new InvalidCastException( - $"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}."); - } - - /// - /// Converts a configuration object into a serialized database value. - /// - public static string? ToDatabase(object? configuration, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - => configuration == null ? null : configurationEditorJsonSerializer.Serialize(configuration); - - /// - [DataMember(Name = "defaultConfig")] - public virtual IDictionary DefaultConfiguration - { - get => _defaultConfiguration; - set => _defaultConfiguration = value; - } - - /// - public virtual object? DefaultConfigurationObject => DefaultConfiguration; - - /// - public virtual bool IsConfiguration(object obj) => obj is IDictionary; - - - /// - public virtual object FromDatabase(string? configurationJson, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - => string.IsNullOrWhiteSpace(configurationJson) - ? new Dictionary() - : configurationEditorJsonSerializer.Deserialize>(configurationJson)!; - - /// - public virtual object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) - { - // by default, return the posted dictionary - // but only keep entries that have a non-null/empty value - // rest will fall back to default during ToConfigurationEditor() - - var keys = editorValues?.Where(x => - x.Value == null || x.Value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) - .Select(x => x.Key).ToList(); - - if (keys is not null) - { - foreach (var key in keys) - { - editorValues?.Remove(key); - } - } - - return editorValues; - } - - /// - public virtual IDictionary ToConfigurationEditor(object? configuration) - { - // editors that do not override ToEditor/FromEditor have their configuration - // as a dictionary of and, by default, we merge their default - // configuration with their current configuration - - if (configuration == null) - configuration = new Dictionary(); - - if (!(configuration is IDictionary c)) - throw new ArgumentException( - $"Expecting a {typeof(Dictionary).Name} instance but got {configuration.GetType().Name}.", - nameof(configuration)); - - // clone the default configuration, and apply the current configuration values - var d = new Dictionary(DefaultConfiguration); - foreach (var (key, value) in c) - d[key] = value; - return d; - } - - /// - public virtual IDictionary ToValueEditor(object? configuration) - => ToConfigurationEditor(configuration); - + Fields = new List(); + _defaultConfiguration = new Dictionary(); } + + /// + /// Initializes a new instance of the class. + /// + protected ConfigurationEditor(List fields) + { + Fields = fields; + _defaultConfiguration = new Dictionary(); + } + + /// + /// Gets the fields. + /// + [DataMember(Name = "fields")] + public List Fields { get; } + + /// + [DataMember(Name = "defaultConfig")] + public virtual IDictionary DefaultConfiguration + { + get => _defaultConfiguration; + set => _defaultConfiguration = value; + } + + /// + public virtual object? DefaultConfigurationObject => DefaultConfiguration; + + /// + /// Converts a configuration object into a serialized database value. + /// + public static string? ToDatabase( + object? configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + => configuration == null ? null : configurationEditorJsonSerializer.Serialize(configuration); + + /// + /// Gets the configuration as a typed object. + /// + public static TConfiguration? ConfigurationAs(object? obj) + { + if (obj == null) + { + return default; + } + + if (obj is TConfiguration configuration) + { + return configuration; + } + + throw new InvalidCastException( + $"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}."); + } + + /// + public virtual bool IsConfiguration(object obj) => obj is IDictionary; + + /// + public virtual object FromDatabase( + string? configurationJson, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + => string.IsNullOrWhiteSpace(configurationJson) + ? new Dictionary() + : configurationEditorJsonSerializer.Deserialize>(configurationJson)!; + + /// + public virtual object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) + { + // by default, return the posted dictionary + // but only keep entries that have a non-null/empty value + // rest will fall back to default during ToConfigurationEditor() + var keys = editorValues?.Where(x => + x.Value == null || (x.Value is string stringValue && string.IsNullOrWhiteSpace(stringValue))) + .Select(x => x.Key).ToList(); + + if (keys is not null) + { + foreach (var key in keys) + { + editorValues?.Remove(key); + } + } + + return editorValues; + } + + /// + public virtual IDictionary ToConfigurationEditor(object? configuration) + { + // editors that do not override ToEditor/FromEditor have their configuration + // as a dictionary of and, by default, we merge their default + // configuration with their current configuration + if (configuration == null) + { + configuration = new Dictionary(); + } + + if (!(configuration is IDictionary c)) + { + throw new ArgumentException( + $"Expecting a {typeof(Dictionary).Name} instance but got {configuration.GetType().Name}.", + nameof(configuration)); + } + + // clone the default configuration, and apply the current configuration values + var d = new Dictionary(DefaultConfiguration); + foreach ((string key, object value) in c) + { + d[key] = value; + } + + return d; + } + + /// + public virtual IDictionary ToValueEditor(object? configuration) + => ToConfigurationEditor(configuration); + + /// + /// Gets a field by its property name. + /// + /// + /// Can be used in constructors to add infos to a field that has been defined + /// by a property marked with the . + /// + protected ConfigurationField Field(string name) + => Fields.First(x => x.PropertyName == name); } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs index fa2427a048..6d64bc2d19 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; @@ -12,151 +10,178 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a data type configuration editor with a typed configuration. +/// +public abstract class ConfigurationEditor : ConfigurationEditor + where TConfiguration : new() { - /// - /// Represents a data type configuration editor with a typed configuration. - /// - public abstract class ConfigurationEditor : ConfigurationEditor - where TConfiguration : new() - { - private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IEditorConfigurationParser _editorConfigurationParser; - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - protected ConfigurationEditor(IIOHelper ioHelper) + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + protected ConfigurationEditor(IIOHelper ioHelper) : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + { + } - /// - /// Initializes a new instance of the class. - /// - protected ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + /// + /// Initializes a new instance of the class. + /// + protected ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(DiscoverFields(ioHelper)) => - _editorConfigurationParser = editorConfigurationParser; + _editorConfigurationParser = editorConfigurationParser; - /// - /// Discovers fields from configuration properties marked with the field attribute. - /// - private static List DiscoverFields(IIOHelper ioHelper) + /// + public override IDictionary DefaultConfiguration => + ToConfigurationEditor(DefaultConfigurationObject); + + /// + public override object DefaultConfigurationObject => new TConfiguration(); + + /// + public override bool IsConfiguration(object obj) + => obj is TConfiguration; + + /// + public override object FromDatabase( + string? configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + { + try { - var fields = new List(); - var properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration)); - - foreach (var property in properties) + if (string.IsNullOrWhiteSpace(configuration)) { - var attribute = property.GetCustomAttribute(false); - if (attribute == null) continue; - - ConfigurationField field; - - var attributeView = ioHelper.ResolveRelativeOrVirtualUrl(attribute.View); - // if the field does not have its own type, use the base type - if (attribute.Type == null) - { - field = new ConfigurationField - { - // if the key is empty then use the property name - Key = string.IsNullOrWhiteSpace(attribute.Key) ? property.Name : attribute.Key, - Name = attribute.Name, - PropertyName = property.Name, - PropertyType = property.PropertyType, - Description = attribute.Description, - HideLabel = attribute.HideLabel, - View = attributeView - }; - - fields.Add(field); - continue; - } - - // if the field has its own type, instantiate it - try - { - field = (ConfigurationField) Activator.CreateInstance(attribute.Type)!; - } - catch (Exception ex) - { - throw new Exception($"Failed to create an instance of type \"{attribute.Type}\" for property \"{property.Name}\" of configuration \"{typeof(TConfiguration).Name}\" (see inner exception).", ex); - } - - // then add it, and overwrite values if they are assigned in the attribute - fields.Add(field); - - field.PropertyName = property.Name; - field.PropertyType = property.PropertyType; - - if (!string.IsNullOrWhiteSpace(attribute.Key)) - field.Key = attribute.Key; - - // if the key is still empty then use the property name - if (string.IsNullOrWhiteSpace(field.Key)) - field.Key = property.Name; - - if (!string.IsNullOrWhiteSpace(attribute.Name)) - field.Name = attribute.Name; - - if (!string.IsNullOrWhiteSpace(attribute.View)) - field.View = attributeView; - - if (!string.IsNullOrWhiteSpace(attribute.Description)) - field.Description = attribute.Description; - - if (attribute.HideLabelSettable.HasValue) - field.HideLabel = attribute.HideLabel; + return new TConfiguration(); } - return fields; + return configurationEditorJsonSerializer.Deserialize(configuration)!; } - - /// - public override IDictionary DefaultConfiguration => ToConfigurationEditor(DefaultConfigurationObject); - - /// - public override object DefaultConfigurationObject => new TConfiguration(); - - /// - public override bool IsConfiguration(object obj) - => obj is TConfiguration; - - /// - public override object FromDatabase(string? configuration, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + catch (Exception e) { + throw new InvalidOperationException( + $"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", + e); + } + } + + /// + public sealed override object? FromConfigurationEditor( + IDictionary? editorValues, + object? configuration) => FromConfigurationEditor(editorValues, (TConfiguration?)configuration); + + /// + /// Converts the configuration posted by the editor. + /// + /// The configuration object posted by the editor. + /// The current configuration object. + public virtual TConfiguration? FromConfigurationEditor( + IDictionary? editorValues, + TConfiguration? configuration) => + _editorConfigurationParser.ParseFromConfigurationEditor(editorValues, Fields); + + /// + public sealed override IDictionary ToConfigurationEditor(object? configuration) => + ToConfigurationEditor((TConfiguration?)configuration); + + /// + /// Converts configuration values to values for the editor. + /// + /// The configuration. + public virtual Dictionary ToConfigurationEditor(TConfiguration? configuration) => + _editorConfigurationParser.ParseToConfigurationEditor(configuration); + + /// + /// Discovers fields from configuration properties marked with the field attribute. + /// + private static List DiscoverFields(IIOHelper ioHelper) + { + var fields = new List(); + PropertyInfo[] properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration)); + + foreach (PropertyInfo property in properties) + { + ConfigurationFieldAttribute? attribute = property.GetCustomAttribute(false); + if (attribute == null) + { + continue; + } + + ConfigurationField field; + + var attributeView = ioHelper.ResolveRelativeOrVirtualUrl(attribute.View); + + // if the field does not have its own type, use the base type + if (attribute.Type == null) + { + field = new ConfigurationField + { + // if the key is empty then use the property name + Key = string.IsNullOrWhiteSpace(attribute.Key) ? property.Name : attribute.Key, + Name = attribute.Name, + PropertyName = property.Name, + PropertyType = property.PropertyType, + Description = attribute.Description, + HideLabel = attribute.HideLabel, + View = attributeView, + }; + + fields.Add(field); + continue; + } + + // if the field has its own type, instantiate it try { - if (string.IsNullOrWhiteSpace(configuration)) return new TConfiguration(); - return configurationEditorJsonSerializer.Deserialize(configuration)!; + field = (ConfigurationField)Activator.CreateInstance(attribute.Type)!; } - catch (Exception e) + catch (Exception ex) { - throw new InvalidOperationException($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e); + throw new Exception( + $"Failed to create an instance of type \"{attribute.Type}\" for property \"{property.Name}\" of configuration \"{typeof(TConfiguration).Name}\" (see inner exception).", + ex); + } + + // then add it, and overwrite values if they are assigned in the attribute + fields.Add(field); + + field.PropertyName = property.Name; + field.PropertyType = property.PropertyType; + + if (!string.IsNullOrWhiteSpace(attribute.Key)) + { + field.Key = attribute.Key; + } + + // if the key is still empty then use the property name + if (string.IsNullOrWhiteSpace(field.Key)) + { + field.Key = property.Name; + } + + if (!string.IsNullOrWhiteSpace(attribute.Name)) + { + field.Name = attribute.Name; + } + + if (!string.IsNullOrWhiteSpace(attribute.View)) + { + field.View = attributeView; + } + + if (!string.IsNullOrWhiteSpace(attribute.Description)) + { + field.Description = attribute.Description; + } + + if (attribute.HideLabelSettable.HasValue) + { + field.HideLabel = attribute.HideLabel; } } - /// - public sealed override object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) - { - return FromConfigurationEditor(editorValues, (TConfiguration?) configuration); - } - - /// - /// Converts the configuration posted by the editor. - /// - /// The configuration object posted by the editor. - /// The current configuration object. - public virtual TConfiguration? FromConfigurationEditor(IDictionary? editorValues, TConfiguration? configuration) => _editorConfigurationParser.ParseFromConfigurationEditor(editorValues, Fields); - - /// - public sealed override IDictionary ToConfigurationEditor(object? configuration) - { - return ToConfigurationEditor((TConfiguration?) configuration); - } - - /// - /// Converts configuration values to values for the editor. - /// - /// The configuration. - public virtual Dictionary ToConfigurationEditor(TConfiguration? configuration) => _editorConfigurationParser.ParseToConfigurationEditor(configuration); + return fields; } } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationField.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationField.cs index 0e679f9dc1..40bd0c0ca9 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationField.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationField.cs @@ -1,106 +1,109 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; +using System.Runtime.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a datatype configuration field for editing. +/// +[DataContract] +public class ConfigurationField { + private readonly string? _view; + /// - /// Represents a datatype configuration field for editing. + /// Initializes a new instance of the class. /// - [DataContract] - public class ConfigurationField + public ConfigurationField() + : this(new List()) { - private string? _view; + } - /// - /// Initializes a new instance of the class. - /// - public ConfigurationField() - : this(new List()) - { } + /// + /// Initializes a new instance of the class. + /// + public ConfigurationField(params IValueValidator[] validators) + : this(validators.ToList()) + { + } - /// - /// Initializes a new instance of the class. - /// - public ConfigurationField(params IValueValidator[] validators) - : this(validators.ToList()) - { } + /// + /// Initializes a new instance of the class. + /// + private ConfigurationField(List validators) + { + Validators = validators; + Config = new Dictionary(); - /// - /// Initializes a new instance of the class. - /// - private ConfigurationField(List validators) + // fill details from attribute, if any + ConfigurationFieldAttribute? attribute = GetType().GetCustomAttribute(false); + if (attribute is null) { - Validators = validators; - Config = new Dictionary(); - - // fill details from attribute, if any - var attribute = GetType().GetCustomAttribute(false); - if (attribute is null) return; - - Name = attribute.Name; - Description = attribute.Description; - HideLabel = attribute.HideLabel; - Key = attribute.Key; - View = attribute.View; + return; } - /// - /// Gets or sets the key of the field. - /// - [DataMember(Name = "key", IsRequired = true)] - public string Key { get; set; } = null!; - - /// - /// Gets or sets the name of the field. - /// - [DataMember(Name = "label", IsRequired = true)] - public string? Name { get; set; } - - /// - /// Gets or sets the property name of the field. - /// - public string? PropertyName { get; set; } - - /// - /// Gets or sets the property CLR type of the field. - /// - public Type? PropertyType { get; set; } - - /// - /// Gets or sets the description of the field. - /// - [DataMember(Name = "description")] - public string? Description { get; set; } - - /// - /// Gets or sets a value indicating whether to hide the label of the field. - /// - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } - - /// - /// Gets or sets the view to used in the editor. - /// - /// - /// Can be the full virtual path, or the relative path to the Umbraco folder, - /// or a simple view name which will map to ~/Views/PreValueEditors/{view}.html. - /// - [DataMember(Name = "view", IsRequired = true)] - public string? View { get; set; } - - /// - /// Gets the validators of the field. - /// - [DataMember(Name = "validation")] - public List Validators { get; } - - /// - /// Gets or sets extra configuration properties for the editor. - /// - [DataMember(Name = "config")] - public IDictionary Config { get; set; } + Name = attribute.Name; + Description = attribute.Description; + HideLabel = attribute.HideLabel; + Key = attribute.Key; + View = attribute.View; } + + /// + /// Gets or sets the key of the field. + /// + [DataMember(Name = "key", IsRequired = true)] + public string Key { get; set; } = null!; + + /// + /// Gets or sets the name of the field. + /// + [DataMember(Name = "label", IsRequired = true)] + public string? Name { get; set; } + + /// + /// Gets or sets the property name of the field. + /// + public string? PropertyName { get; set; } + + /// + /// Gets or sets the property CLR type of the field. + /// + public Type? PropertyType { get; set; } + + /// + /// Gets or sets the description of the field. + /// + [DataMember(Name = "description")] + public string? Description { get; set; } + + /// + /// Gets or sets a value indicating whether to hide the label of the field. + /// + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } + + /// + /// Gets or sets the view to used in the editor. + /// + /// + /// + /// Can be the full virtual path, or the relative path to the Umbraco folder, + /// or a simple view name which will map to ~/Views/PreValueEditors/{view}.html. + /// + /// + [DataMember(Name = "view", IsRequired = true)] + public string? View { get; set; } + + /// + /// Gets the validators of the field. + /// + [DataMember(Name = "validation")] + public List Validators { get; } + + /// + /// Gets or sets extra configuration properties for the editor. + /// + [DataMember(Name = "config")] + public IDictionary Config { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs index 79e9655e25..c504a790be 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs @@ -1,117 +1,169 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Marks a ConfigurationEditor property as a configuration field, and a class as a configuration field type. +/// +/// Properties marked with this attribute are discovered as fields. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)] +public class ConfigurationFieldAttribute : Attribute { + private Type? _type; + /// - /// Marks a ConfigurationEditor property as a configuration field, and a class as a configuration field type. + /// Initializes a new instance of the class. /// - /// Properties marked with this attribute are discovered as fields. - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)] - public class ConfigurationFieldAttribute : Attribute + public ConfigurationFieldAttribute(Type type) { - private Type? _type; + Type = type; + Key = string.Empty; + } - /// - /// Initializes a new instance of the class. - /// - public ConfigurationFieldAttribute(Type type) + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the field. + /// The friendly name of the field. + /// The view to use to render the field editor. + public ConfigurationFieldAttribute(string key, string name, string view) + { + if (key == null) { - Type = type; - Key = string.Empty; + throw new ArgumentNullException(nameof(key)); } - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the field. - /// The friendly name of the field. - /// The view to use to render the field editor. - public ConfigurationFieldAttribute(string key, string name, string view) + if (string.IsNullOrWhiteSpace(key)) { - if (key == null) throw new ArgumentNullException(nameof(key)); - if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(key)); - 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 (view == null) throw new ArgumentNullException(nameof(view)); - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); - - Key = key; - Name = name; - View = view; + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(key)); } - /// - /// Initializes a new instance of the class. - /// - /// The friendly name of the field. - /// The view to use to render the field editor. - /// When no key is specified, the will derive a key - /// from the name of the property marked with this attribute. - public ConfigurationFieldAttribute(string name, string view) + if (name == null) { - 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 (view == null) throw new ArgumentNullException(nameof(view)); - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); - - Name = name; - View = view; - Key = string.Empty; + throw new ArgumentNullException(nameof(name)); } - /// - /// Gets or sets the key of the field. - /// - /// When null or empty, the should derive a key - /// from the name of the property marked with this attribute. - public string Key { get; } - - /// - /// Gets the friendly name of the field. - /// - public string? Name { get; } - - /// - /// Gets or sets the view to use to render the field editor. - /// - public string? View { get; } - - /// - /// Gets or sets the description of the field. - /// - public string? Description { get; set; } - - /// - /// Gets or sets a value indicating whether the field editor should be displayed without its label. - /// - public bool HideLabel + if (string.IsNullOrWhiteSpace(name)) { - get => HideLabelSettable.ValueOrDefault(false); - set => HideLabelSettable.Set(value); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Gets the settable underlying . - /// - public Settable HideLabelSettable { get; } = new Settable(); - - /// - /// Gets or sets the type of the field. - /// - /// - /// By default, fields are created as instances, - /// unless specified otherwise through this property. - /// The specified type must inherit from . - /// - public Type? Type + if (view == null) { - get => _type; - set + throw new ArgumentNullException(nameof(view)); + } + + if (string.IsNullOrWhiteSpace(view)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(view)); + } + + Key = key; + Name = name; + View = view; + } + + /// + /// Initializes a new instance of the class. + /// + /// The friendly name of the field. + /// The view to use to render the field editor. + /// + /// When no key is specified, the will derive a key + /// from the name of the property marked with this attribute. + /// + public ConfigurationFieldAttribute(string name, string view) + { + 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 (view == null) + { + throw new ArgumentNullException(nameof(view)); + } + + if (string.IsNullOrWhiteSpace(view)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(view)); + } + + Name = name; + View = view; + Key = string.Empty; + } + + /// + /// Gets or sets the key of the field. + /// + /// + /// When null or empty, the should derive a key + /// from the name of the property marked with this attribute. + /// + public string Key { get; } + + /// + /// Gets the friendly name of the field. + /// + public string? Name { get; } + + /// + /// Gets or sets the view to use to render the field editor. + /// + public string? View { get; } + + /// + /// Gets or sets the description of the field. + /// + public string? Description { get; set; } + + /// + /// Gets or sets a value indicating whether the field editor should be displayed without its label. + /// + public bool HideLabel + { + get => HideLabelSettable.ValueOrDefault(false); + set => HideLabelSettable.Set(value); + } + + /// + /// Gets the settable underlying . + /// + public Settable HideLabelSettable { get; } = new(); + + /// + /// Gets or sets the type of the field. + /// + /// + /// + /// By default, fields are created as instances, + /// unless specified otherwise through this property. + /// + /// The specified type must inherit from . + /// + public Type? Type + { + get => _type; + set + { + if (!typeof(ConfigurationField).IsAssignableFrom(value)) { - if (!typeof(ConfigurationField).IsAssignableFrom(value)) - throw new ArgumentException("Type must inherit from ConfigurationField.", nameof(value)); - _type = value; + throw new ArgumentException("Type must inherit from ConfigurationField.", nameof(value)); } + + _type = value; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerConfiguration.cs index 555d6f8418..8cbaecdbdb 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerConfiguration.cs @@ -1,16 +1,17 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class ContentPickerConfiguration : IIgnoreUserStartNodesConfig { - public class ContentPickerConfiguration : IIgnoreUserStartNodesConfig - { - [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] - public bool ShowOpenButton { get; set; } + [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] + public bool ShowOpenButton { get; set; } - [ConfigurationField("startNodeId", "Start node", "treepicker")] // + config in configuration editor ctor - public Udi? StartNodeId { get; set; } + [ConfigurationField("startNodeId", "Start node", "treepicker")] // + config in configuration editor ctor + public Udi? StartNodeId { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs index 4932030db2..3bffa4ad61 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs @@ -1,38 +1,33 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class ContentPickerConfigurationEditor : ConfigurationEditor { - internal class ContentPickerConfigurationEditor : ConfigurationEditor + public ContentPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) => + + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + Field(nameof(ContentPickerConfiguration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; + + public override IDictionary ToValueEditor(object? configuration) { - public ContentPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - // configure fields - // this is not part of ContentPickerConfiguration, - // but is required to configure the UI editor (when editing the configuration) - Field(nameof(ContentPickerConfiguration.StartNodeId)) - .Config = new Dictionary { { "idType", "udi" } }; - } + // get the configuration fields + IDictionary d = base.ToValueEditor(configuration); - public override IDictionary ToValueEditor(object? configuration) - { - // get the configuration fields - var d = base.ToValueEditor(configuration); + // add extra fields + // not part of ContentPickerConfiguration but used to configure the UI editor + d["showEditButton"] = false; + d["showPathOnHover"] = false; + d["idType"] = "udi"; - // add extra fields - // not part of ContentPickerConfiguration but used to configure the UI editor - d["showEditButton"] = false; - d["showPathOnHover"] = false; - d["idType"] = "udi"; - - return d; - } + return d; } } diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs index 5ca0564e69..7ef5407c4f 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs @@ -1,11 +1,7 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -14,69 +10,72 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Content property editor that stores UDI +/// +[DataEditor( + Constants.PropertyEditors.Aliases.ContentPicker, + EditorType.PropertyValue | EditorType.MacroParameter, + "Content Picker", + "contentpicker", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.Pickers)] +public class ContentPickerPropertyEditor : DataEditor { - /// - /// Content property editor that stores UDI - /// - [DataEditor( - Constants.PropertyEditors.Aliases.ContentPicker, - EditorType.PropertyValue | EditorType.MacroParameter, - "Content Picker", - "contentpicker", - ValueType = ValueTypes.String, - Group = Constants.PropertyEditors.Groups.Pickers)] - public class ContentPickerPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public ContentPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper) + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public ContentPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper) - : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + public ContentPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + } - public ContentPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, + protected override IConfigurationEditor CreateConfigurationEditor() => + new ContentPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); + + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference + { + public ContentPickerPropertyValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) + DataEditorAttribute attribute) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; } - protected override IConfigurationEditor CreateConfigurationEditor() + public IEnumerable GetReferences(object? value) { - return new ContentPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); - } + var asString = value is string str ? str : value?.ToString(); - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - - internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference - { - public ContentPickerPropertyValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + if (string.IsNullOrEmpty(asString)) { + yield break; } - public IEnumerable GetReferences(object? value) + if (UdiParser.TryParse(asString, out Udi? udi)) { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (UdiParser.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); + yield return new UmbracoEntityReference(udi); } } } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 5619a1bb87..115b6a2371 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -1,201 +1,219 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.Serialization; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a data editor. +/// +/// +/// +/// Editors can be deserialized from e.g. manifests, which is. why the class is not abstract, +/// the json serialization attributes are required, and the properties have an internal setter. +/// +/// +[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "(),nq}")] +[HideFromTypeFinder] +[DataContract] +public class DataEditor : IDataEditor { + private IDictionary? _defaultConfiguration; + /// - /// Represents a data editor. + /// Initializes a new instance of the class. /// - /// - /// Editors can be deserialized from e.g. manifests, which is. why the class is not abstract, - /// the json serialization attributes are required, and the properties have an internal setter. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "(),nq}")] - [HideFromTypeFinder] - [DataContract] - public class DataEditor : IDataEditor + public DataEditor(IDataValueEditorFactory dataValueEditorFactory, EditorType type = EditorType.PropertyValue) { - private IDictionary? _defaultConfiguration; + // defaults + DataValueEditorFactory = dataValueEditorFactory; + Type = type; + Icon = Constants.Icons.PropertyEditor; + Group = Constants.PropertyEditors.Groups.Common; - /// - /// Initializes a new instance of the class. - /// - public DataEditor(IDataValueEditorFactory dataValueEditorFactory, EditorType type = EditorType.PropertyValue) + // assign properties based on the attribute, if it is found + Attribute = GetType().GetCustomAttribute(false); + if (Attribute == null) { - - // defaults - DataValueEditorFactory = dataValueEditorFactory; - Type = type; - Icon = Constants.Icons.PropertyEditor; - Group = Constants.PropertyEditors.Groups.Common; - - // assign properties based on the attribute, if it is found - Attribute = GetType().GetCustomAttribute(false); - if (Attribute == null) - { - Alias = string.Empty; - Name = string.Empty; - return; - } - - Alias = Attribute.Alias; - Type = Attribute.Type; - Name = Attribute.Name; - Icon = Attribute.Icon; - Group = Attribute.Group; - IsDeprecated = Attribute.IsDeprecated; + Alias = string.Empty; + Name = string.Empty; + return; } - /// - /// Gets the editor attribute. - /// - protected DataEditorAttribute? Attribute { get; } - - /// - [DataMember(Name = "alias", IsRequired = true)] - public string Alias { get; set; } - - protected IDataValueEditorFactory DataValueEditorFactory { get; } - - /// - [IgnoreDataMember] - public EditorType Type { get; } - - /// - [DataMember(Name = "name", IsRequired = true)] - public string Name { get; internal set; } - - /// - [DataMember(Name = "icon")] - public string Icon { get; internal set; } - - /// - [DataMember(Name = "group")] - public string Group { get; internal set; } - - /// - [IgnoreDataMember] - public bool IsDeprecated { get; } - - /// - /// - /// If an explicit value editor has been assigned, then this explicit - /// instance is returned. Otherwise, a new instance is created by CreateValueEditor. - /// The instance created by CreateValueEditor is not cached, i.e. - /// a new instance is created each time the property value is retrieved. The - /// property editor is a singleton, and the value editor cannot be a singleton - /// since it depends on the datatype configuration. - /// Technically, it could be cached by datatype but let's keep things - /// simple enough for now. - /// - // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); - - /// - /// - /// If an explicit value editor has been assigned, then this explicit - /// instance is returned. Otherwise, a new instance is created by CreateValueEditor, - /// and configured with the configuration. - /// The instance created by CreateValueEditor is not cached, i.e. - /// a new instance is created each time the property value is retrieved. The - /// property editor is a singleton, and the value editor cannot be a singleton - /// since it depends on the datatype configuration. - /// Technically, it could be cached by datatype but let's keep things - /// simple enough for now. - /// - public virtual IDataValueEditor GetValueEditor(object? configuration) - { - // if an explicit value editor has been set (by the manifest parser) - // then return it, and ignore the configuration, which is going to be - // empty anyways - if (ExplicitValueEditor != null) - return ExplicitValueEditor; - - var editor = CreateValueEditor(); - if (configuration is not null) - { - ((DataValueEditor)editor).Configuration = configuration; // TODO: casting is bad - } - - return editor; - } - - /// - /// Gets or sets an explicit value editor. - /// - /// Used for manifest data editors. - [DataMember(Name = "editor")] - public IDataValueEditor? ExplicitValueEditor { get; set; } - - /// - /// - /// If an explicit configuration editor has been assigned, then this explicit - /// instance is returned. Otherwise, a new instance is created by CreateConfigurationEditor. - /// The instance created by CreateConfigurationEditor is not cached, i.e. - /// a new instance is created each time. The property editor is a singleton, and although the - /// configuration editor could technically be a singleton too, we'd rather not keep configuration editor - /// cached. - /// - public IConfigurationEditor GetConfigurationEditor() => ExplicitConfigurationEditor ?? CreateConfigurationEditor(); - - /// - /// Gets or sets an explicit configuration editor. - /// - /// Used for manifest data editors. - [DataMember(Name = "config")] - public IConfigurationEditor? ExplicitConfigurationEditor { get; set; } - - /// - [DataMember(Name = "defaultConfig")] - public IDictionary DefaultConfiguration - { - // for property value editors, get the ConfigurationEditor.DefaultConfiguration - // else fallback to a default, empty dictionary - - get => _defaultConfiguration ?? ((Type & EditorType.PropertyValue) > 0 ? GetConfigurationEditor().DefaultConfiguration : (_defaultConfiguration = new Dictionary())); - set => _defaultConfiguration = value; - } - - /// - public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); - - /// - /// Creates a value editor instance. - /// - /// - protected virtual IDataValueEditor CreateValueEditor() - { - if (Attribute == null) - throw new InvalidOperationException($"The editor is not attributed with {nameof(DataEditorAttribute)}"); - - return DataValueEditorFactory.Create(Attribute); - } - - /// - /// Creates a configuration editor instance. - /// - protected virtual IConfigurationEditor CreateConfigurationEditor() - { - var editor = new ConfigurationEditor(); - // pass the default configuration if this is not a property value editor - if ((Type & EditorType.PropertyValue) == 0 && _defaultConfiguration is not null) - { - editor.DefaultConfiguration = _defaultConfiguration; - } - return editor; - } - - /// - /// Provides a summary of the PropertyEditor for use with the . - /// - protected virtual string DebuggerDisplay() - { - return $"Name: {Name}, Alias: {Alias}"; - } + Alias = Attribute.Alias; + Type = Attribute.Type; + Name = Attribute.Name; + Icon = Attribute.Icon; + Group = Attribute.Group; + IsDeprecated = Attribute.IsDeprecated; } + + /// + /// Gets or sets an explicit value editor. + /// + /// Used for manifest data editors. + [DataMember(Name = "editor")] + public IDataValueEditor? ExplicitValueEditor { get; set; } + + /// + /// Gets the editor attribute. + /// + protected DataEditorAttribute? Attribute { get; } + + protected IDataValueEditorFactory DataValueEditorFactory { get; } + + /// + /// Gets or sets an explicit configuration editor. + /// + /// Used for manifest data editors. + [DataMember(Name = "config")] + public IConfigurationEditor? ExplicitConfigurationEditor { get; set; } + + /// + [DataMember(Name = "alias", IsRequired = true)] + public string Alias { get; set; } + + /// + [IgnoreDataMember] + public EditorType Type { get; } + + /// + [DataMember(Name = "name", IsRequired = true)] + public string Name { get; internal set; } + + /// + [DataMember(Name = "icon")] + public string Icon { get; internal set; } + + /// + [DataMember(Name = "group")] + public string Group { get; internal set; } + + /// + [IgnoreDataMember] + public bool IsDeprecated { get; } + + /// + [DataMember(Name = "defaultConfig")] + public IDictionary DefaultConfiguration + { + // for property value editors, get the ConfigurationEditor.DefaultConfiguration + // else fallback to a default, empty dictionary + get => _defaultConfiguration ?? ((Type & EditorType.PropertyValue) > 0 + ? GetConfigurationEditor().DefaultConfiguration + : _defaultConfiguration = new Dictionary()); + set => _defaultConfiguration = value; + } + + /// + /// + /// + /// If an explicit value editor has been assigned, then this explicit + /// instance is returned. Otherwise, a new instance is created by CreateValueEditor. + /// + /// + /// The instance created by CreateValueEditor is not cached, i.e. + /// a new instance is created each time the property value is retrieved. The + /// property editor is a singleton, and the value editor cannot be a singleton + /// since it depends on the datatype configuration. + /// + /// + /// Technically, it could be cached by datatype but let's keep things + /// simple enough for now. + /// + /// + // TODO: point of that one? shouldn't we always configure? + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); + + /// + /// + /// + /// If an explicit value editor has been assigned, then this explicit + /// instance is returned. Otherwise, a new instance is created by CreateValueEditor, + /// and configured with the configuration. + /// + /// + /// The instance created by CreateValueEditor is not cached, i.e. + /// a new instance is created each time the property value is retrieved. The + /// property editor is a singleton, and the value editor cannot be a singleton + /// since it depends on the datatype configuration. + /// + /// + /// Technically, it could be cached by datatype but let's keep things + /// simple enough for now. + /// + /// + public virtual IDataValueEditor GetValueEditor(object? configuration) + { + // if an explicit value editor has been set (by the manifest parser) + // then return it, and ignore the configuration, which is going to be + // empty anyways + if (ExplicitValueEditor != null) + { + return ExplicitValueEditor; + } + + IDataValueEditor editor = CreateValueEditor(); + if (configuration is not null) + { + ((DataValueEditor)editor).Configuration = configuration; // TODO: casting is bad + } + + return editor; + } + + /// + /// + /// + /// If an explicit configuration editor has been assigned, then this explicit + /// instance is returned. Otherwise, a new instance is created by CreateConfigurationEditor. + /// + /// + /// The instance created by CreateConfigurationEditor is not cached, i.e. + /// a new instance is created each time. The property editor is a singleton, and although the + /// configuration editor could technically be a singleton too, we'd rather not keep configuration editor + /// cached. + /// + /// + public IConfigurationEditor GetConfigurationEditor() => ExplicitConfigurationEditor ?? CreateConfigurationEditor(); + + /// + public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); + + /// + /// Creates a value editor instance. + /// + /// + protected virtual IDataValueEditor CreateValueEditor() + { + if (Attribute == null) + { + throw new InvalidOperationException($"The editor is not attributed with {nameof(DataEditorAttribute)}"); + } + + return DataValueEditorFactory.Create(Attribute); + } + + /// + /// Creates a configuration editor instance. + /// + protected virtual IConfigurationEditor CreateConfigurationEditor() + { + var editor = new ConfigurationEditor(); + + // pass the default configuration if this is not a property value editor + if ((Type & EditorType.PropertyValue) == 0 && _defaultConfiguration is not null) + { + editor.DefaultConfiguration = _defaultConfiguration; + } + + return editor; + } + + /// + /// Provides a summary of the PropertyEditor for use with the . + /// + protected virtual string DebuggerDisplay() => $"Name: {Name}, Alias: {Alias}"; } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index d99acb4781..ce15c66a80 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -1,134 +1,181 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Marks a class that represents a data editor. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class DataEditorAttribute : Attribute { + /// + /// Gets a special value indicating that the view should be null. + /// + public const string + NullView = "EXPLICITELY-SET-VIEW-TO-NULL-2B5B0B73D3DD47B28DDB84E02C349DFB"; // just a random string + + private string _valueType = ValueTypes.String; /// - /// Marks a class that represents a data editor. + /// Initializes a new instance of the class for a property editor. /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class DataEditorAttribute : Attribute + /// The unique identifier of the editor. + /// The friendly name of the editor. + public DataEditorAttribute(string alias, string name) + : this(alias, EditorType.PropertyValue, name, NullView) { - private string _valueType = ValueTypes.String; - - /// - /// Initializes a new instance of the class for a property editor. - /// - /// The unique identifier of the editor. - /// The friendly name of the editor. - public DataEditorAttribute(string alias, string name) - : this(alias, EditorType.PropertyValue, name, NullView) - { } - - /// - /// Initializes a new instance of the class for a property editor. - /// - /// The unique identifier of the editor. - /// The friendly name of the editor. - /// The view to use to render the editor. - public DataEditorAttribute(string alias, string name, string view) - : this(alias, EditorType.PropertyValue, name, view) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the editor. - /// The type of the editor. - /// The friendly name of the editor. - public DataEditorAttribute(string alias, EditorType type, string name) - : this(alias, type, name, NullView) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the editor. - /// The type of the editor. - /// The friendly name of the editor. - /// The view to use to render the editor. - /// - /// Set to to explicitly set the view to null. - /// Otherwise, cannot be null nor empty. - /// - public DataEditorAttribute(string alias, EditorType type, string name, string view) - { - 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)); - if ((type & ~(EditorType.PropertyValue | EditorType.MacroParameter)) > 0) throw new ArgumentOutOfRangeException(nameof(type), type, $"Not a valid {typeof(EditorType)} value."); - 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 (view == null) throw new ArgumentNullException(nameof(view)); - if (string.IsNullOrWhiteSpace(view)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(view)); - - Type = type; - Alias = alias; - Name = name; - View = view == NullView ? null : view; - } - - /// - /// Gets a special value indicating that the view should be null. - /// - public const string NullView = "EXPLICITELY-SET-VIEW-TO-NULL-2B5B0B73D3DD47B28DDB84E02C349DFB"; // just a random string - - /// - /// Gets the unique alias of the editor. - /// - public string Alias { get; } - - /// - /// Gets the type of the editor. - /// - public EditorType Type { get; } - - /// - /// Gets the friendly name of the editor. - /// - public string Name { get; } - - /// - /// Gets the view to use to render the editor. - /// - public string? View { get; } - - /// - /// Gets or sets the type of the edited value. - /// - /// Must be a valid value. - public string ValueType { - get => _valueType; - set - { - if (value == null) throw new ArgumentNullException(nameof(value)); - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(value)); - if (!ValueTypes.IsValue(value)) throw new ArgumentOutOfRangeException(nameof(value), value, $"Not a valid {typeof(ValueTypes)} value."); - - _valueType = value; - } - } - - /// - /// Gets or sets a value indicating whether the editor should be displayed without its label. - /// - public bool HideLabel { get; set; } - - /// - /// Gets or sets an optional icon. - /// - /// The icon can be used for example when presenting datatypes based upon the editor. - public string Icon { get; set; } = Constants.Icons.PropertyEditor; - - /// - /// Gets or sets an optional group. - /// - /// The group can be used for example to group the editors by category. - public string Group { get; set; } = Constants.PropertyEditors.Groups.Common; - - /// - /// Gets or sets a value indicating whether the value editor is deprecated. - /// - /// A deprecated editor is still supported but not proposed in the UI. - public bool IsDeprecated { get; set; } } + + /// + /// Initializes a new instance of the class for a property editor. + /// + /// The unique identifier of the editor. + /// The friendly name of the editor. + /// The view to use to render the editor. + public DataEditorAttribute(string alias, string name, string view) + : this(alias, EditorType.PropertyValue, name, view) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the editor. + /// The type of the editor. + /// The friendly name of the editor. + public DataEditorAttribute(string alias, EditorType type, string name) + : this(alias, type, name, NullView) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the editor. + /// The type of the editor. + /// The friendly name of the editor. + /// The view to use to render the editor. + /// + /// Set to to explicitly set the view to null. + /// Otherwise, cannot be null nor empty. + /// + public DataEditorAttribute(string alias, EditorType type, string name, string view) + { + 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)); + } + + if ((type & ~(EditorType.PropertyValue | EditorType.MacroParameter)) > 0) + { + throw new ArgumentOutOfRangeException(nameof(type), type, $"Not a valid {typeof(EditorType)} value."); + } + + 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 (view == null) + { + throw new ArgumentNullException(nameof(view)); + } + + if (string.IsNullOrWhiteSpace(view)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(view)); + } + + Type = type; + Alias = alias; + Name = name; + View = view == NullView ? null : view; + } + + /// + /// Gets the unique alias of the editor. + /// + public string Alias { get; } + + /// + /// Gets the type of the editor. + /// + public EditorType Type { get; } + + /// + /// Gets the friendly name of the editor. + /// + public string Name { get; } + + /// + /// Gets the view to use to render the editor. + /// + public string? View { get; } + + /// + /// Gets or sets the type of the edited value. + /// + /// Must be a valid value. + public string ValueType + { + get => _valueType; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(value)); + } + + if (!ValueTypes.IsValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value), value, $"Not a valid {typeof(ValueTypes)} value."); + } + + _valueType = value; + } + } + + /// + /// Gets or sets a value indicating whether the editor should be displayed without its label. + /// + public bool HideLabel { get; set; } + + /// + /// Gets or sets an optional icon. + /// + /// The icon can be used for example when presenting datatypes based upon the editor. + public string Icon { get; set; } = Constants.Icons.PropertyEditor; + + /// + /// Gets or sets an optional group. + /// + /// The group can be used for example to group the editors by category. + public string Group { get; set; } = Constants.PropertyEditors.Groups.Common; + + /// + /// Gets or sets a value indicating whether the value editor is deprecated. + /// + /// A deprecated editor is still supported but not proposed in the UI. + public bool IsDeprecated { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs index 0c4ca93fc1..40daf7ec7c 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DataEditorCollection : BuilderCollectionBase { - public class DataEditorCollection : BuilderCollectionBase + public DataEditorCollection(Func> items) + : base(items) { - public DataEditorCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs index 4794d37c21..36e70f2738 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorCollectionBuilder.cs @@ -1,9 +1,9 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class + DataEditorCollectionBuilder : LazyCollectionBuilderBase { - public class DataEditorCollectionBuilder : LazyCollectionBuilderBase - { - protected override DataEditorCollectionBuilder This => this; - } + protected override DataEditorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index b8e3e597a4..c75844bfa2 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.Linq; using System.Runtime.Serialization; using System.Xml.Linq; using Microsoft.Extensions.Logging; @@ -15,385 +12,435 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a value editor. +/// +[DataContract] +public class DataValueEditor : IDataValueEditor { + private readonly IJsonSerializer? _jsonSerializer; + private readonly ILocalizedTextService _localizedTextService; + private readonly IShortStringHelper _shortStringHelper; + /// - /// Represents a value editor. + /// Initializes a new instance of the class. /// - [DataContract] - public class DataValueEditor : IDataValueEditor + public DataValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer? jsonSerializer) // for tests, and manifest { - private readonly ILocalizedTextService _localizedTextService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IJsonSerializer? _jsonSerializer; + _localizedTextService = localizedTextService; + _shortStringHelper = shortStringHelper; + _jsonSerializer = jsonSerializer; + ValueType = ValueTypes.String; + Validators = new List(); + } - /// - /// Initializes a new instance of the class. - /// - public DataValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer? jsonSerializer) // for tests, and manifest + /// + /// Initializes a new instance of the class. + /// + public DataValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + { + if (attribute == null) { - _localizedTextService = localizedTextService; - _shortStringHelper = shortStringHelper; - _jsonSerializer = jsonSerializer; - ValueType = ValueTypes.String; - Validators = new List(); + throw new ArgumentNullException(nameof(attribute)); } - /// - /// Initializes a new instance of the class. - /// - public DataValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute) + _localizedTextService = localizedTextService; + _shortStringHelper = shortStringHelper; + _jsonSerializer = jsonSerializer; + + var view = attribute.View; + if (string.IsNullOrWhiteSpace(view)) { - if (attribute == null) throw new ArgumentNullException(nameof(attribute)); - _localizedTextService = localizedTextService; - _shortStringHelper = shortStringHelper; - _jsonSerializer = jsonSerializer; - - var view = attribute.View; - if (string.IsNullOrWhiteSpace(view)) - throw new ArgumentException("The attribute does not specify a view.", nameof(attribute)); - - if (view.StartsWith("~/")) - { - view = ioHelper.ResolveRelativeOrVirtualUrl(view); - } - - View = view; - ValueType = attribute.ValueType; - HideLabel = attribute.HideLabel; + throw new ArgumentException("The attribute does not specify a view.", nameof(attribute)); } - /// - /// Gets or sets the value editor configuration. - /// - public virtual object? Configuration { get; set; } - - /// - /// Gets or sets the editor view. - /// - /// - /// The view can be three things: (1) the full virtual path, or (2) the relative path to the current Umbraco - /// folder, or (3) a view name which maps to views/propertyeditors/{view}/{view}.html. - /// - [Required] - [DataMember(Name = "view")] - public string? View { get; set; } - - /// - /// The value type which reflects how it is validated and stored in the database - /// - [DataMember(Name = "valueType")] - public string ValueType { get; set; } - - /// - public IEnumerable Validate(object? value, bool required, string? format) + if (view.StartsWith("~/")) { - List? results = null; - var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList(); - if (r.Any()) { results = r; } - - // mandatory and regex validators cannot be part of valueEditor.Validators because they - // depend on values that are not part of the configuration, .Mandatory and .ValidationRegEx, - // so they have to be explicitly invoked here. - - if (required) - { - r = RequiredValidator.ValidateRequired(value, ValueType).ToList(); - if (r.Any()) { if (results == null) results = r; else results.AddRange(r); } - } - - var stringValue = value?.ToString(); - if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(stringValue)) - { - r = FormatValidator.ValidateFormat(value, ValueType, format).ToList(); - if (r.Any()) { if (results == null) results = r; else results.AddRange(r); } - } - - return results ?? Enumerable.Empty(); + view = ioHelper.ResolveRelativeOrVirtualUrl(view); } - /// - /// A collection of validators for the pre value editor - /// - [DataMember(Name = "validation")] - public List Validators { get; private set; } = new List(); + View = view; + ValueType = attribute.ValueType; + HideLabel = attribute.HideLabel; + } - /// - /// Gets the validator used to validate the special property type -level "required". - /// - public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator(_localizedTextService); + /// + /// Gets or sets the value editor configuration. + /// + public virtual object? Configuration { get; set; } - /// - /// Gets the validator used to validate the special property type -level "format". - /// - public virtual IValueFormatValidator FormatValidator => new RegexValidator(_localizedTextService); + /// + /// Gets the validator used to validate the special property type -level "required". + /// + public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator(_localizedTextService); - /// - /// If this is true than the editor will be displayed full width without a label - /// - [DataMember(Name = "hideLabel")] - public bool HideLabel { get; set; } + /// + /// Gets the validator used to validate the special property type -level "format". + /// + public virtual IValueFormatValidator FormatValidator => new RegexValidator(_localizedTextService); - /// - /// Set this to true if the property editor is for display purposes only - /// - public virtual bool IsReadOnly => false; + /// + /// Gets or sets the editor view. + /// + /// + /// + /// The view can be three things: (1) the full virtual path, or (2) the relative path to the current Umbraco + /// folder, or (3) a view name which maps to views/propertyeditors/{view}/{view}.html. + /// + /// + [Required] + [DataMember(Name = "view")] + public string? View { get; set; } - /// - /// Used to try to convert the string value to the correct CLR type based on the specified for this value editor. - /// - /// The value. - /// - /// The result of the conversion attempt. - /// - /// ValueType was out of range. - internal Attempt TryConvertValueToCrlType(object? value) + /// + /// The value type which reflects how it is validated and stored in the database + /// + [DataMember(Name = "valueType")] + public string ValueType { get; set; } + + /// + /// A collection of validators for the pre value editor + /// + [DataMember(Name = "validation")] + public List Validators { get; private set; } = new(); + + /// + public IEnumerable Validate(object? value, bool required, string? format) + { + List? results = null; + var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList(); + if (r.Any()) { - // Ensure empty string and JSON values are converted to null - if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) + results = r; + } + + // mandatory and regex validators cannot be part of valueEditor.Validators because they + // depend on values that are not part of the configuration, .Mandatory and .ValidationRegEx, + // so they have to be explicitly invoked here. + if (required) + { + r = RequiredValidator.ValidateRequired(value, ValueType).ToList(); + if (r.Any()) { - value = null; - } - else if (value is not string && ValueType.InvariantEquals(ValueTypes.Json)) - { - // Only serialize value when it's not already a string - var jsonValue = _jsonSerializer?.Serialize(value); - if (jsonValue?.DetectIsEmptyJson() ?? false) + if (results == null) { - value = null; + results = r; } else { - value = jsonValue; + results.AddRange(r); } } - - // Convert the string to a known type - Type valueType; - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Ntext: - case ValueStorageType.Nvarchar: - valueType = typeof(string); - break; - - case ValueStorageType.Integer: - // Ensure these are nullable so we can return a null if required - // NOTE: This is allowing type of 'long' because I think JSON.NEt will deserialize a numerical value as long instead of int - // Even though our DB will not support this (will get truncated), we'll at least parse to this - valueType = typeof(long?); - - // If parsing is successful, we need to return as an int, we're only dealing with long's here because of JSON.NET, - // we actually don't support long values and if we return a long value, it will get set as a 'long' on the Property.Value (object) and then - // when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match. - var result = value.TryConvertTo(valueType); - - return result.Success && result.Result != null - ? Attempt.Succeed((int)(long)result.Result) - : result; - - case ValueStorageType.Decimal: - // Ensure these are nullable so we can return a null if required - valueType = typeof(decimal?); - break; - - case ValueStorageType.Date: - // Ensure these are nullable so we can return a null if required - valueType = typeof(DateTime?); - break; - - default: - throw new ArgumentOutOfRangeException("ValueType was out of range."); - } - - return value.TryConvertTo(valueType); } - /// - /// A method to deserialize the string value that has been saved in the content editor to an object to be stored in the database. - /// - /// The value returned by the editor. - /// The current value that has been persisted to the database for this editor. This value may be useful for how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used. - /// The value that gets persisted to the database. - /// - /// By default this will attempt to automatically convert the string value to the value type supplied by ValueType. - /// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the - /// value to the DB will fail when it tries to validate the value type. - /// - public virtual object? FromEditor(ContentPropertyData editorValue, object? currentValue) + var stringValue = value?.ToString(); + if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(stringValue)) { - var result = TryConvertValueToCrlType(editorValue.Value); - if (result.Success == false) + r = FormatValidator.ValidateFormat(value, ValueType, format).ToList(); + if (r.Any()) { - StaticApplicationLogging.Logger.LogWarning("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); - return null; + if (results == null) + { + results = r; + } + else + { + results.AddRange(r); + } } - - return result.Result; } - /// - /// A method used to format the database value to a value that can be used by the editor. - /// - /// The property. - /// The culture. - /// The segment. - /// - /// ValueType was out of range. - /// - /// The object returned will automatically be serialized into JSON notation. For most property editors - /// the value returned is probably just a string, but in some cases a JSON structure will be returned. - /// - public virtual object? ToEditor(IProperty property, string? culture = null, string? segment = null) - { - var value = property.GetValue(culture, segment); - if (value == null) - { - return string.Empty; - } + return results ?? Enumerable.Empty(); + } - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Ntext: - case ValueStorageType.Nvarchar: - // If it is a string type, we will attempt to see if it is JSON stored data, if it is we'll try to convert - // to a real JSON object so we can pass the true JSON object directly to Angular! - var stringValue = value as string ?? value.ToString(); - if (stringValue!.DetectIsJson()) + /// + /// If this is true than the editor will be displayed full width without a label + /// + [DataMember(Name = "hideLabel")] + public bool HideLabel { get; set; } + + /// + /// Set this to true if the property editor is for display purposes only + /// + public virtual bool IsReadOnly => false; + + /// + /// A method to deserialize the string value that has been saved in the content editor to an object to be stored in the + /// database. + /// + /// The value returned by the editor. + /// + /// The current value that has been persisted to the database for this editor. This value may be + /// useful for how the value then get's deserialized again to be re-persisted. In most cases it will probably not be + /// used. + /// + /// The value that gets persisted to the database. + /// + /// By default this will attempt to automatically convert the string value to the value type supplied by ValueType. + /// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the + /// value to the DB will fail when it tries to validate the value type. + /// + public virtual object? FromEditor(ContentPropertyData editorValue, object? currentValue) + { + Attempt result = TryConvertValueToCrlType(editorValue.Value); + if (result.Success == false) + { + StaticApplicationLogging.Logger.LogWarning( + "The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); + return null; + } + + return result.Result; + } + + /// + /// A method used to format the database value to a value that can be used by the editor. + /// + /// The property. + /// The culture. + /// The segment. + /// + /// ValueType was out of range. + /// + /// The object returned will automatically be serialized into JSON notation. For most property editors + /// the value returned is probably just a string, but in some cases a JSON structure will be returned. + /// + public virtual object? ToEditor(IProperty property, string? culture = null, string? segment = null) + { + var value = property.GetValue(culture, segment); + if (value == null) + { + return string.Empty; + } + + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Ntext: + case ValueStorageType.Nvarchar: + // If it is a string type, we will attempt to see if it is JSON stored data, if it is we'll try to convert + // to a real JSON object so we can pass the true JSON object directly to Angular! + var stringValue = value as string ?? value.ToString(); + if (stringValue!.DetectIsJson()) + { + try { - try - { - var json = _jsonSerializer?.Deserialize(stringValue!); - return json; - } - catch - { - // Swallow this exception, we thought it was JSON but it really isn't so continue returning a string - } + dynamic? json = _jsonSerializer?.Deserialize(stringValue!); + return json; } - - return stringValue; - - case ValueStorageType.Integer: - case ValueStorageType.Decimal: - // Decimals need to be formatted with invariant culture (dots, not commas) - // Anything else falls back to ToString() - var decimalValue = value.TryConvertTo(); - - return decimalValue.Success - ? decimalValue.Result.ToString(NumberFormatInfo.InvariantInfo) - : value.ToString(); - - case ValueStorageType.Date: - var dateValue = value.TryConvertTo(); - if (dateValue.Success == false || dateValue.Result == null) + catch { - return string.Empty; + // Swallow this exception, we thought it was JSON but it really isn't so continue returning a string } + } - // Dates will be formatted as yyyy-MM-dd HH:mm:ss - return dateValue.Result.Value.ToIsoString(); + return stringValue; - default: - throw new ArgumentOutOfRangeException("ValueType was out of range."); - } - } + case ValueStorageType.Integer: + case ValueStorageType.Decimal: + // Decimals need to be formatted with invariant culture (dots, not commas) + // Anything else falls back to ToString() + Attempt decimalValue = value.TryConvertTo(); - // TODO: the methods below should be replaced by proper property value convert ToXPath usage! + return decimalValue.Success + ? decimalValue.Result.ToString(NumberFormatInfo.InvariantInfo) + : value.ToString(); - /// - /// Converts a property to Xml fragments. - /// - public IEnumerable ConvertDbToXml(IProperty property, bool published) - { - published &= property.PropertyType.SupportsPublishing; + case ValueStorageType.Date: + Attempt dateValue = value.TryConvertTo(); + if (dateValue.Success == false || dateValue.Result == null) + { + return string.Empty; + } - var nodeName = property.PropertyType.Alias.ToSafeAlias(_shortStringHelper); + // Dates will be formatted as yyyy-MM-dd HH:mm:ss + return dateValue.Result.Value.ToIsoString(); - foreach (var pvalue in property.Values) - { - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - if (value == null || value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) - continue; - - var xElement = new XElement(nodeName); - if (pvalue.Culture != null) - xElement.Add(new XAttribute("lang", pvalue.Culture)); - if (pvalue.Segment != null) - xElement.Add(new XAttribute("segment", pvalue.Segment)); - - var xValue = ConvertDbToXml(property.PropertyType, value); - xElement.Add(xValue); - - yield return xElement; - } - } - - /// - /// Converts a property value to an Xml fragment. - /// - /// - /// By default, this returns the value of ConvertDbToString but ensures that if the db value type is - /// NVarchar or NText, the value is returned as a CDATA fragment - else it's a Text fragment. - /// Returns an XText or XCData instance which must be wrapped in a element. - /// If the value is empty we will not return as CDATA since that will just take up more space in the file. - /// - public XNode ConvertDbToXml(IPropertyType propertyType, object? value) - { - //check for null or empty value, we don't want to return CDATA if that is the case - if (value == null || value.ToString().IsNullOrWhiteSpace()) - { - return new XText(ConvertDbToString(propertyType, value)); - } - - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Date: - case ValueStorageType.Integer: - case ValueStorageType.Decimal: - return new XText(ConvertDbToString(propertyType, value)); - case ValueStorageType.Nvarchar: - case ValueStorageType.Ntext: - //put text in cdata - return new XCData(ConvertDbToString(propertyType, value)); - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// Converts a property value to a string. - /// - public virtual string ConvertDbToString(IPropertyType propertyType, object? value) - { - if (value == null) - return string.Empty; - - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Nvarchar: - case ValueStorageType.Ntext: - return value.ToXmlString(); - case ValueStorageType.Integer: - case ValueStorageType.Decimal: - return value.ToXmlString(value.GetType()); - case ValueStorageType.Date: - //treat dates differently, output the format as xml format - var date = value.TryConvertTo(); - if (date.Success == false || date.Result == null) - return string.Empty; - return date.Result.ToXmlString(); - default: - throw new ArgumentOutOfRangeException(); - } + default: + throw new ArgumentOutOfRangeException("ValueType was out of range."); } } + + // TODO: the methods below should be replaced by proper property value convert ToXPath usage! + + /// + /// Converts a property to Xml fragments. + /// + public IEnumerable ConvertDbToXml(IProperty property, bool published) + { + published &= property.PropertyType.SupportsPublishing; + + var nodeName = property.PropertyType.Alias.ToSafeAlias(_shortStringHelper); + + foreach (IPropertyValue pvalue in property.Values) + { + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + if (value == null || (value is string stringValue && string.IsNullOrWhiteSpace(stringValue))) + { + continue; + } + + var xElement = new XElement(nodeName); + if (pvalue.Culture != null) + { + xElement.Add(new XAttribute("lang", pvalue.Culture)); + } + + if (pvalue.Segment != null) + { + xElement.Add(new XAttribute("segment", pvalue.Segment)); + } + + XNode xValue = ConvertDbToXml(property.PropertyType, value); + xElement.Add(xValue); + + yield return xElement; + } + } + + /// + /// Converts a property value to an Xml fragment. + /// + /// + /// + /// By default, this returns the value of ConvertDbToString but ensures that if the db value type is + /// NVarchar or NText, the value is returned as a CDATA fragment - else it's a Text fragment. + /// + /// Returns an XText or XCData instance which must be wrapped in a element. + /// If the value is empty we will not return as CDATA since that will just take up more space in the file. + /// + public XNode ConvertDbToXml(IPropertyType propertyType, object? value) + { + // check for null or empty value, we don't want to return CDATA if that is the case + if (value == null || value.ToString().IsNullOrWhiteSpace()) + { + return new XText(ConvertDbToString(propertyType, value)); + } + + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Date: + case ValueStorageType.Integer: + case ValueStorageType.Decimal: + return new XText(ConvertDbToString(propertyType, value)); + case ValueStorageType.Nvarchar: + case ValueStorageType.Ntext: + // put text in cdata + return new XCData(ConvertDbToString(propertyType, value)); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Converts a property value to a string. + /// + public virtual string ConvertDbToString(IPropertyType propertyType, object? value) + { + if (value == null) + { + return string.Empty; + } + + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Nvarchar: + case ValueStorageType.Ntext: + return value.ToXmlString(); + case ValueStorageType.Integer: + case ValueStorageType.Decimal: + return value.ToXmlString(value.GetType()); + case ValueStorageType.Date: + // treat dates differently, output the format as xml format + Attempt date = value.TryConvertTo(); + if (date.Success == false || date.Result == null) + { + return string.Empty; + } + + return date.Result.ToXmlString(); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Used to try to convert the string value to the correct CLR type based on the specified for + /// this value editor. + /// + /// The value. + /// + /// The result of the conversion attempt. + /// + /// ValueType was out of range. + internal Attempt TryConvertValueToCrlType(object? value) + { + // Ensure empty string and JSON values are converted to null + if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) + { + value = null; + } + else if (value is not string && ValueType.InvariantEquals(ValueTypes.Json)) + { + // Only serialize value when it's not already a string + var jsonValue = _jsonSerializer?.Serialize(value); + if (jsonValue?.DetectIsEmptyJson() ?? false) + { + value = null; + } + else + { + value = jsonValue; + } + } + + // Convert the string to a known type + Type valueType; + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Ntext: + case ValueStorageType.Nvarchar: + valueType = typeof(string); + break; + + case ValueStorageType.Integer: + // Ensure these are nullable so we can return a null if required + // NOTE: This is allowing type of 'long' because I think JSON.NEt will deserialize a numerical value as long instead of int + // Even though our DB will not support this (will get truncated), we'll at least parse to this + valueType = typeof(long?); + + // If parsing is successful, we need to return as an int, we're only dealing with long's here because of JSON.NET, + // we actually don't support long values and if we return a long value, it will get set as a 'long' on the Property.Value (object) and then + // when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match. + Attempt result = value.TryConvertTo(valueType); + + return result.Success && result.Result != null + ? Attempt.Succeed((int)(long)result.Result) + : result; + + case ValueStorageType.Decimal: + // Ensure these are nullable so we can return a null if required + valueType = typeof(decimal?); + break; + + case ValueStorageType.Date: + // Ensure these are nullable so we can return a null if required + valueType = typeof(DateTime?); + break; + + default: + throw new ArgumentOutOfRangeException("ValueType was out of range."); + } + + return value.TryConvertTo(valueType); + } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditorFactory.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditorFactory.cs index 300bdde672..86b771bcaa 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditorFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditorFactory.cs @@ -1,18 +1,15 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DataValueEditorFactory : IDataValueEditorFactory { - public class DataValueEditorFactory : IDataValueEditorFactory - { - private readonly IServiceProvider _serviceProvider; + private readonly IServiceProvider _serviceProvider; - public DataValueEditorFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + public DataValueEditorFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; - public TDataValueEditor Create(params object[] args) - where TDataValueEditor: class, IDataValueEditor - => _serviceProvider.CreateInstance(args); - - } + public TDataValueEditor Create(params object[] args) + where TDataValueEditor : class, IDataValueEditor + => _serviceProvider.CreateInstance(args); } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 099fa9126f..24d6f17eb0 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -1,61 +1,67 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DataValueReferenceFactoryCollection : BuilderCollectionBase { - public class DataValueReferenceFactoryCollection : BuilderCollectionBase + public DataValueReferenceFactoryCollection(Func> items) + : base(items) { - public DataValueReferenceFactoryCollection(System.Func> items) : base(items) + } + + // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented + // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection + public IEnumerable GetAllReferences( + IPropertyCollection properties, + PropertyEditorCollection propertyEditors) + { + var trackedRelations = new HashSet(); + + foreach (IProperty p in properties) { - } - - // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented - // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection - - public IEnumerable GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors) - { - var trackedRelations = new HashSet(); - - foreach (var p in properties) + if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out IDataEditor? editor)) { - if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; + continue; + } - //TODO: We will need to change this once we support tracking via variants/segments - // for now, we are tracking values from ALL variants + // TODO: We will need to change this once we support tracking via variants/segments + // for now, we are tracking values from ALL variants + foreach (IPropertyValue propertyVal in p.Values) + { + var val = propertyVal.EditedValue; - foreach (var propertyVal in p.Values) + IDataValueEditor? valueEditor = editor?.GetValueEditor(); + if (valueEditor is IDataValueReference reference) { - var val = propertyVal.EditedValue; - - var valueEditor = editor?.GetValueEditor(); - if (valueEditor is IDataValueReference reference) + IEnumerable refs = reference.GetReferences(val); + foreach (UmbracoEntityReference r in refs) { - var refs = reference.GetReferences(val); - foreach (var r in refs) - trackedRelations.Add(r); + trackedRelations.Add(r); } + } - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (var item in this) + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (IDataValueReferenceFactory item in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media/content items + if (item.IsForEditor(editor)) { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has referecnes to media/content items - if (item.IsForEditor(editor)) + foreach (UmbracoEntityReference r in item.GetDataValueReference().GetReferences(val)) { - foreach (var r in item.GetDataValueReference().GetReferences(val)) - trackedRelations.Add(r); + trackedRelations.Add(r); } } } } - - return trackedRelations; } + + return trackedRelations; } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs index b42ea74e88..f286827653 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase { - public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase - { - protected override DataValueReferenceFactoryCollectionBuilder This => this; - } + protected override DataValueReferenceFactoryCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DateTimeConfiguration.cs index 985d58f06d..27c1445160 100644 --- a/src/Umbraco.Core/PropertyEditors/DateTimeConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DateTimeConfiguration.cs @@ -1,20 +1,22 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the datetime value editor. +/// +public class DateTimeConfiguration { - /// - /// Represents the configuration for the datetime value editor. - /// - public class DateTimeConfiguration - { - [ConfigurationField("format", "Date format", "textstring", Description = "If left empty then the format is YYYY-MM-DD. (see momentjs.com for supported formats)")] - public string Format { get; set; } + public DateTimeConfiguration() => - public DateTimeConfiguration() - { - // different default values - Format = "YYYY-MM-DD HH:mm:ss"; - } + // different default values + Format = "YYYY-MM-DD HH:mm:ss"; - [ConfigurationField("offsetTime", "Offset time", "boolean", Description = "When enabled the time displayed will be offset with the server's timezone, this is useful for scenarios like scheduled publishing when an editor is in a different timezone than the hosted server")] - public bool OffsetTime { get; set; } - } + [ConfigurationField("format", "Date format", "textstring", Description = "If left empty then the format is YYYY-MM-DD. (see momentjs.com for supported formats)")] + public string Format { get; set; } + + [ConfigurationField( + "offsetTime", + "Offset time", + "boolean", + Description = "When enabled the time displayed will be offset with the server's timezone, this is useful for scenarios like scheduled publishing when an editor is in a different timezone than the hosted server")] + public bool OffsetTime { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs index 36c82175c2..d97f7e2c6d 100644 --- a/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs @@ -1,40 +1,42 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the datetime value editor. +/// +public class DateTimeConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editor for the datetime value editor. - /// - public class DateTimeConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public DateTimeConfigurationEditor(IIOHelper ioHelper) + : this( + ioHelper, + StaticServiceProvider.Instance.GetRequiredService()) { - public override IDictionary ToValueEditor(object? configuration) - { - var d = base.ToValueEditor(configuration); + } - var format = d["format"].ToString()!; + public DateTimeConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base( + ioHelper, editorConfigurationParser) + { + } - d["pickTime"] = format.ContainsAny(new string[] { "H", "m", "s" }); + public override IDictionary ToValueEditor(object? configuration) + { + IDictionary d = base.ToValueEditor(configuration); - return d; - } + var format = d["format"].ToString()!; - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public DateTimeConfigurationEditor(IIOHelper ioHelper) : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + d["pickTime"] = format.ContainsAny(new[] { "H", "m", "s" }); - public DateTimeConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } + return d; } } diff --git a/src/Umbraco.Core/PropertyEditors/DateValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DateValueEditor.cs index 1e65429b6e..25cb2c42ed 100644 --- a/src/Umbraco.Core/PropertyEditors/DateValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DateValueEditor.cs @@ -1,5 +1,3 @@ -using System; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors.Validators; @@ -8,34 +6,32 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// CUstom value editor so we can serialize with the correct date format (excluding time) +/// and includes the date validator +/// +internal class DateValueEditor : DataValueEditor { - /// - /// CUstom value editor so we can serialize with the correct date format (excluding time) - /// and includes the date validator - /// - internal class DateValueEditor : DataValueEditor + public DateValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) => + Validators.Add(new DateTimeValidator()); + + public override object ToEditor(IProperty property, string? culture = null, string? segment = null) { - public DateValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + Attempt date = property.GetValue(culture, segment).TryConvertTo(); + if (date.Success == false || date.Result == null) { - Validators.Add(new DateTimeValidator()); + return string.Empty; } - public override object ToEditor(IProperty property, string? culture= null, string? segment = null) - { - var date = property.GetValue(culture, segment).TryConvertTo(); - if (date.Success == false || date.Result == null) - { - return String.Empty; - } - //Dates will be formatted as yyyy-MM-dd - return date.Result.Value.ToString("yyyy-MM-dd"); - } + // Dates will be formatted as yyyy-MM-dd + return date.Result.Value.ToString("yyyy-MM-dd"); } } diff --git a/src/Umbraco.Core/PropertyEditors/DecimalConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DecimalConfigurationEditor.cs index 52eefbd400..1b4a094ca2 100644 --- a/src/Umbraco.Core/PropertyEditors/DecimalConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DecimalConfigurationEditor.cs @@ -1,37 +1,36 @@ -using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.PropertyEditors.Validators; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. +/// +public class DecimalConfigurationEditor : ConfigurationEditor { - /// - /// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. - /// - public class DecimalConfigurationEditor : ConfigurationEditor + public DecimalConfigurationEditor() { - public DecimalConfigurationEditor() + Fields.Add(new ConfigurationField(new DecimalValidator()) { - Fields.Add(new ConfigurationField(new DecimalValidator()) - { - Description = "Enter the minimum amount of number to be entered", - Key = "min", - View = "decimal", - Name = "Minimum" - }); + Description = "Enter the minimum amount of number to be entered", + Key = "min", + View = "decimal", + Name = "Minimum", + }); - Fields.Add(new ConfigurationField(new DecimalValidator()) - { - Description = "Enter the intervals amount between each step of number to be entered", - Key = "step", - View = "decimal", - Name = "Step Size" - }); + Fields.Add(new ConfigurationField(new DecimalValidator()) + { + Description = "Enter the intervals amount between each step of number to be entered", + Key = "step", + View = "decimal", + Name = "Step Size", + }); - Fields.Add(new ConfigurationField(new DecimalValidator()) - { - Description = "Enter the maximum amount of number to be entered", - Key = "max", - View = "decimal", - Name = "Maximum" - }); - } + Fields.Add(new ConfigurationField(new DecimalValidator()) + { + Description = "Enter the maximum amount of number to be entered", + Key = "max", + View = "decimal", + Name = "Maximum", + }); } } diff --git a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs index c940560c90..5dc4a3ea5b 100644 --- a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs @@ -1,41 +1,36 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors.Validators; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a decimal property and parameter editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.Decimal, + EditorType.PropertyValue | EditorType.MacroParameter, + "Decimal", + "decimal", + ValueType = ValueTypes.Decimal)] +public class DecimalPropertyEditor : DataEditor { /// - /// Represents a decimal property and parameter editor. + /// Initializes a new instance of the class. /// - [DataEditor( - Constants.PropertyEditors.Aliases.Decimal, - EditorType.PropertyValue | EditorType.MacroParameter, - "Decimal", - "decimal", - ValueType = ValueTypes.Decimal)] - public class DecimalPropertyEditor : DataEditor + public DecimalPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - /// - /// Initializes a new instance of the class. - /// - public DecimalPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } - - /// - protected override IDataValueEditor CreateValueEditor() - { - var editor = base.CreateValueEditor(); - editor.Validators.Add(new DecimalValidator()); - return editor; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new DecimalConfigurationEditor(); } + + /// + protected override IDataValueEditor CreateValueEditor() + { + IDataValueEditor editor = base.CreateValueEditor(); + editor.Validators.Add(new DecimalValidator()); + return editor; + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new DecimalConfigurationEditor(); } diff --git a/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs index 5cb11b7071..705ab034fc 100644 --- a/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs @@ -1,20 +1,19 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Provides a default implementation for +/// , returning a single field to index containing the property value. +/// +public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory { - /// - /// Provides a default implementation for , returning a single field to index containing the property value. - /// - public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory + /// + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) { - /// - public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) - { - yield return new KeyValuePair>( - property.Alias, - property.GetValue(culture, segment, published).Yield()); - } + yield return new KeyValuePair>( + property.Alias, + property.GetValue(culture, segment, published).Yield()); } } diff --git a/src/Umbraco.Core/PropertyEditors/DefaultPropertyValueConverterAttribute.cs b/src/Umbraco.Core/PropertyEditors/DefaultPropertyValueConverterAttribute.cs index a38ea29e0b..b74d9903cf 100644 --- a/src/Umbraco.Core/PropertyEditors/DefaultPropertyValueConverterAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DefaultPropertyValueConverterAttribute.cs @@ -1,33 +1,26 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Indicates that this is a default property value converter (shipped with Umbraco) +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class DefaultPropertyValueConverterAttribute : Attribute { + public DefaultPropertyValueConverterAttribute() => DefaultConvertersToShadow = Array.Empty(); + + public DefaultPropertyValueConverterAttribute(params Type[] convertersToShadow) => + DefaultConvertersToShadow = convertersToShadow; + /// - /// Indicates that this is a default property value converter (shipped with Umbraco) + /// A DefaultPropertyValueConverter can 'shadow' other default property value converters so that + /// a DefaultPropertyValueConverter can be more specific than another one. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class DefaultPropertyValueConverterAttribute : Attribute - { - public DefaultPropertyValueConverterAttribute() - { - DefaultConvertersToShadow = Array.Empty(); - } - - public DefaultPropertyValueConverterAttribute(params Type[] convertersToShadow) - { - DefaultConvertersToShadow = convertersToShadow; - } - - /// - /// A DefaultPropertyValueConverter can 'shadow' other default property value converters so that - /// a DefaultPropertyValueConverter can be more specific than another one. - /// - /// - /// An example where this is useful is that both the RelatedLiksEditorValueConverter and the JsonValueConverter - /// will be returned as value converters for the Related Links Property editor, however the JsonValueConverter - /// is a very generic converter and the RelatedLiksEditorValueConverter is more specific than it, so the RelatedLiksEditorValueConverter - /// can specify that it 'shadows' the JsonValueConverter. - /// - public Type[] DefaultConvertersToShadow { get; } - } + /// + /// An example where this is useful is that both the RelatedLiksEditorValueConverter and the JsonValueConverter + /// will be returned as value converters for the Related Links Property editor, however the JsonValueConverter + /// is a very generic converter and the RelatedLiksEditorValueConverter is more specific than it, so the + /// RelatedLiksEditorValueConverter + /// can specify that it 'shadows' the JsonValueConverter. + /// + public Type[] DefaultConvertersToShadow { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs index 4d74f4aec2..c0132d574d 100644 --- a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs @@ -1,8 +1,11 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class DropDownFlexibleConfiguration : ValueListConfiguration { - public class DropDownFlexibleConfiguration : ValueListConfiguration - { - [ConfigurationField("multiple", "Enable multiple choice", "boolean", Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")] - public bool Multiple { get; set; } - } + [ConfigurationField( + "multiple", + "Enable multiple choice", + "boolean", + Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")] + public bool Multiple { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/EditorType.cs b/src/Umbraco.Core/PropertyEditors/EditorType.cs index 93d0b91b18..15469e1e51 100644 --- a/src/Umbraco.Core/PropertyEditors/EditorType.cs +++ b/src/Umbraco.Core/PropertyEditors/EditorType.cs @@ -1,26 +1,23 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Represents the type of an editor. +/// +[Flags] +public enum EditorType { /// - /// Represents the type of an editor. + /// Nothing. /// - [Flags] - public enum EditorType - { - /// - /// Nothing. - /// - Nothing = 0, + Nothing = 0, - /// - /// Property value editor. - /// - PropertyValue = 1, + /// + /// Property value editor. + /// + PropertyValue = 1, - /// - /// Macro parameter editor. - /// - MacroParameter = 2 - } + /// + /// Macro parameter editor. + /// + MacroParameter = 2, } diff --git a/src/Umbraco.Core/PropertyEditors/EmailAddressConfiguration.cs b/src/Umbraco.Core/PropertyEditors/EmailAddressConfiguration.cs index 380d54dcad..cf3452c114 100644 --- a/src/Umbraco.Core/PropertyEditors/EmailAddressConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/EmailAddressConfiguration.cs @@ -1,14 +1,11 @@ -using System; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Represents the configuration for the email address value editor. +/// +public class EmailAddressConfiguration { - /// - /// Represents the configuration for the email address value editor. - /// - public class EmailAddressConfiguration - { - [ConfigurationField("IsRequired", "Required?", "hidden", Description = "Deprecated; Make this required by selecting mandatory when adding to the document type")] - [Obsolete("No longer used, use `Mandatory` for the property instead. Will be removed in the next major version")] - public bool IsRequired { get; set; } - } + [ConfigurationField("IsRequired", "Required?", "hidden", Description = "Deprecated; Make this required by selecting mandatory when adding to the document type")] + [Obsolete("No longer used, use `Mandatory` for the property instead. Will be removed in the next major version")] + public bool IsRequired { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/EmailAddressConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/EmailAddressConfigurationEditor.cs index e1e528dda2..2eb5075195 100644 --- a/src/Umbraco.Core/PropertyEditors/EmailAddressConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/EmailAddressConfigurationEditor.cs @@ -1,28 +1,27 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the email address value editor. - /// - public class EmailAddressConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public EmailAddressConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public EmailAddressConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the email address value editor. +/// +public class EmailAddressConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public EmailAddressConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public EmailAddressConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs index 9c2dffb61d..e9c8255a19 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfiguration.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration for the Eye Dropper picker value editor. - /// - public class EyeDropperColorPickerConfiguration - { - [ConfigurationField("showAlpha", "Show alpha", "boolean", Description = "Allow alpha transparency selection.")] - public bool ShowAlpha { get; set; } +namespace Umbraco.Cms.Core.PropertyEditors; - [ConfigurationField("showPalette", "Show palette", "boolean", Description = "Show a palette next to the color picker.")] - public bool ShowPalette { get; set; } - } +/// +/// Represents the configuration for the Eye Dropper picker value editor. +/// +public class EyeDropperColorPickerConfiguration +{ + [ConfigurationField("showAlpha", "Show alpha", "boolean", Description = "Allow alpha transparency selection.")] + public bool ShowAlpha { get; set; } + + [ConfigurationField("showPalette", "Show palette", "boolean", Description = "Show a palette next to the color picker.")] + public bool ShowPalette { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs index 49611f09b9..487034a6b1 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs @@ -1,51 +1,50 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class EyeDropperColorPickerConfigurationEditor : ConfigurationEditor { - internal class EyeDropperColorPickerConfigurationEditor : ConfigurationEditor + public EyeDropperColorPickerConfigurationEditor( + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - public EyeDropperColorPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } + } - /// - public override Dictionary ToConfigurationEditor(EyeDropperColorPickerConfiguration? configuration) + /// + public override Dictionary ToConfigurationEditor(EyeDropperColorPickerConfiguration? configuration) => + new() { - return new Dictionary + { "showAlpha", configuration?.ShowAlpha ?? false }, { "showPalette", configuration?.ShowPalette ?? false }, + }; + + /// + public override EyeDropperColorPickerConfiguration FromConfigurationEditor( + IDictionary? editorValues, EyeDropperColorPickerConfiguration? configuration) + { + var showAlpha = true; + var showPalette = true; + + if (editorValues is not null && editorValues.TryGetValue("showAlpha", out var alpha)) + { + Attempt attempt = alpha.TryConvertTo(); + if (attempt.Success) { - { "showAlpha", configuration?.ShowAlpha ?? false }, - { "showPalette", configuration?.ShowPalette ?? false }, - }; - } - - /// - public override EyeDropperColorPickerConfiguration FromConfigurationEditor(IDictionary? editorValues, EyeDropperColorPickerConfiguration? configuration) - { - var showAlpha = true; - var showPalette = true; - - if (editorValues is not null && editorValues.TryGetValue("showAlpha", out var alpha)) - { - var attempt = alpha.TryConvertTo(); - if (attempt.Success) - showAlpha = attempt.Result; + showAlpha = attempt.Result; } - - if (editorValues is not null && editorValues.TryGetValue("showPalette", out var palette)) - { - var attempt = palette.TryConvertTo(); - if (attempt.Success) - showPalette = attempt.Result; - } - - return new EyeDropperColorPickerConfiguration - { - ShowAlpha = showAlpha, - ShowPalette = showPalette - }; } + + if (editorValues is not null && editorValues.TryGetValue("showPalette", out var palette)) + { + Attempt attempt = palette.TryConvertTo(); + if (attempt.Success) + { + showPalette = attempt.Result; + } + } + + return new EyeDropperColorPickerConfiguration { ShowAlpha = showAlpha, ShowPalette = showPalette }; } } diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs index e19a380334..076ede0ce5 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs @@ -1,48 +1,44 @@ -using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +[DataEditor( + Constants.PropertyEditors.Aliases.ColorPickerEyeDropper, + EditorType.PropertyValue | EditorType.MacroParameter, + "Eye Dropper Color Picker", + "eyedropper", + Icon = "icon-colorpicker", + Group = Constants.PropertyEditors.Groups.Pickers)] +public class EyeDropperColorPickerPropertyEditor : DataEditor { - [DataEditor( - Constants.PropertyEditors.Aliases.ColorPickerEyeDropper, - EditorType.PropertyValue | EditorType.MacroParameter, - "Eye Dropper Color Picker", - "eyedropper", - Icon = "icon-colorpicker", - Group = Constants.PropertyEditors.Groups.Pickers)] - public class EyeDropperColorPickerPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public EyeDropperColorPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + EditorType type = EditorType.PropertyValue) + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService(), type) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public EyeDropperColorPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - EditorType type = EditorType.PropertyValue) - : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService(), type) - { - } - - public EyeDropperColorPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser, - EditorType type = EditorType.PropertyValue) - : base(dataValueEditorFactory, type) - { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new EyeDropperColorPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); } + + public EyeDropperColorPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser, + EditorType type = EditorType.PropertyValue) + : base(dataValueEditorFactory, type) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new EyeDropperColorPickerConfigurationEditor(_ioHelper, _editorConfigurationParser); } diff --git a/src/Umbraco.Core/PropertyEditors/FileExtensionConfigItem.cs b/src/Umbraco.Core/PropertyEditors/FileExtensionConfigItem.cs index 2b1997459c..4444466c03 100644 --- a/src/Umbraco.Core/PropertyEditors/FileExtensionConfigItem.cs +++ b/src/Umbraco.Core/PropertyEditors/FileExtensionConfigItem.cs @@ -1,14 +1,13 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +[DataContract] +public class FileExtensionConfigItem : IFileExtensionConfigItem { - [DataContract] - public class FileExtensionConfigItem : IFileExtensionConfigItem - { - [DataMember(Name = "id")] - public int Id { get; set; } + [DataMember(Name = "id")] + public int Id { get; set; } - [DataMember(Name = "value")] - public string? Value { get; set; } - } + [DataMember(Name = "value")] + public string? Value { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/FileUploadConfiguration.cs b/src/Umbraco.Core/PropertyEditors/FileUploadConfiguration.cs index 2953e2a1ed..289f649b00 100644 --- a/src/Umbraco.Core/PropertyEditors/FileUploadConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/FileUploadConfiguration.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Represents the configuration for the file upload address value editor. +/// +public class FileUploadConfiguration : IFileExtensionsConfig { - /// - /// Represents the configuration for the file upload address value editor. - /// - public class FileUploadConfiguration : IFileExtensionsConfig - { - [ConfigurationField("fileExtensions", "Accepted file extensions", "multivalues")] - public List FileExtensions { get; set; } = new List(); - } + [ConfigurationField("fileExtensions", "Accepted file extensions", "multivalues")] + public List FileExtensions { get; set; } = new(); } diff --git a/src/Umbraco.Core/PropertyEditors/FileUploadConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/FileUploadConfigurationEditor.cs index e8aa86e5d8..732e2d795a 100644 --- a/src/Umbraco.Core/PropertyEditors/FileUploadConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/FileUploadConfigurationEditor.cs @@ -1,25 +1,24 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the file upload value editor. - /// - public class FileUploadConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public FileUploadConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public FileUploadConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the file upload value editor. +/// +public class FileUploadConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public FileUploadConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public FileUploadConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/GridEditor.cs b/src/Umbraco.Core/PropertyEditors/GridEditor.cs index 0e7b238900..d661fa9704 100644 --- a/src/Umbraco.Core/PropertyEditors/GridEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/GridEditor.cs @@ -1,69 +1,73 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Configuration.Grid; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +[DataContract] +public class GridEditor : IGridEditorConfig { - - [DataContract] - public class GridEditor : IGridEditorConfig + public GridEditor() { - public GridEditor() - { - Config = new Dictionary(); - Alias = string.Empty; - } - - [DataMember(Name = "name", IsRequired = true)] - public string? Name { get; set; } - - [DataMember(Name = "nameTemplate")] - public string? NameTemplate { get; set; } - - [DataMember(Name = "alias", IsRequired = true)] - public string Alias { get; set; } - - [DataMember(Name = "view", IsRequired = true)] - public string? View{ get; set; } - - [DataMember(Name = "render")] - public string? Render { get; set; } - - [DataMember(Name = "icon", IsRequired = true)] - public string? Icon { get; set; } - - [DataMember(Name = "config")] - public IDictionary Config { get; set; } - - protected bool Equals(GridEditor other) - { - return string.Equals(Alias, other.Alias); - } - - /// - /// Determines whether the specified is equal to the current . - /// - /// - /// true if the specified object is equal to the current object; otherwise, false. - /// - /// The object to compare with the current object. - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((GridEditor) obj); - } - - /// - /// Serves as a hash function for a particular type. - /// - /// - /// A hash code for the current . - /// - public override int GetHashCode() - { - return Alias.GetHashCode(); - } + Config = new Dictionary(); + Alias = string.Empty; } + + [DataMember(Name = "name", IsRequired = true)] + public string? Name { get; set; } + + [DataMember(Name = "nameTemplate")] + public string? NameTemplate { get; set; } + + [DataMember(Name = "alias", IsRequired = true)] + public string Alias { get; set; } + + [DataMember(Name = "view", IsRequired = true)] + public string? View { get; set; } + + [DataMember(Name = "render")] + public string? Render { get; set; } + + [DataMember(Name = "icon", IsRequired = true)] + public string? Icon { get; set; } + + [DataMember(Name = "config")] + public IDictionary Config { get; set; } + + /// + /// Determines whether the specified is equal to the current + /// . + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((GridEditor)obj); + } + + protected bool Equals(GridEditor other) => string.Equals(Alias, other.Alias); + + /// + /// Serves as a hash function for a particular type. + /// + /// + /// A hash code for the current . + /// + public override int GetHashCode() => Alias.GetHashCode(); } diff --git a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs index b4d41f8e33..d61dcd0e98 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs @@ -1,76 +1,83 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents an editor for editing the configuration of editors. +/// +public interface IConfigurationEditor { /// - /// Represents an editor for editing the configuration of editors. + /// Gets the fields. /// - public interface IConfigurationEditor - { - /// - /// Gets the fields. - /// - [DataMember(Name = "fields")] - List Fields { get; } + [DataMember(Name = "fields")] + List Fields { get; } - /// - /// Gets the default configuration. - /// - /// - /// For basic configuration editors, this will be a dictionary of key/values. For advanced editors - /// which inherit from , this will be the dictionary - /// equivalent of an actual configuration object (ie an instance of TConfiguration, obtained - /// via . - /// - [DataMember(Name = "defaultConfig")] - IDictionary DefaultConfiguration { get; } + /// + /// Gets the default configuration. + /// + /// + /// + /// For basic configuration editors, this will be a dictionary of key/values. For advanced editors + /// which inherit from , this will be the dictionary + /// equivalent of an actual configuration object (ie an instance of TConfiguration, obtained + /// via . + /// + /// + [DataMember(Name = "defaultConfig")] + IDictionary DefaultConfiguration { get; } - /// - /// Gets the default configuration object. - /// - /// - /// For basic configuration editors, this will be , ie a - /// dictionary of key/values. For advanced editors which inherit from , - /// this will be an actual configuration object (ie an instance of TConfiguration. - /// - object? DefaultConfigurationObject { get; } + /// + /// Gets the default configuration object. + /// + /// + /// + /// For basic configuration editors, this will be , ie a + /// dictionary of key/values. For advanced editors which inherit from + /// , + /// this will be an actual configuration object (ie an instance of TConfiguration. + /// + /// + object? DefaultConfigurationObject { get; } - /// - /// Determines whether a configuration object is of the type expected by the configuration editor. - /// - bool IsConfiguration(object obj); + /// + /// Determines whether a configuration object is of the type expected by the configuration editor. + /// + bool IsConfiguration(object obj); - // notes - // ToConfigurationEditor returns a dictionary, and FromConfigurationEditor accepts a dictionary. - // this is due to the way our front-end editors work, see DataTypeController.PostSave - // and DataTypeConfigurationFieldDisplayResolver - we are not going to change it now. + // notes + // ToConfigurationEditor returns a dictionary, and FromConfigurationEditor accepts a dictionary. + // this is due to the way our front-end editors work, see DataTypeController.PostSave + // and DataTypeConfigurationFieldDisplayResolver - we are not going to change it now. - /// - /// Converts the serialized database value into the actual configuration object. - /// - /// Converting the configuration object to the serialized database value is - /// achieved by simply serializing the configuration. See . - object FromDatabase(string? configurationJson, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer); + /// + /// Converts the serialized database value into the actual configuration object. + /// + /// + /// Converting the configuration object to the serialized database value is + /// achieved by simply serializing the configuration. See . + /// + object FromDatabase( + string? configurationJson, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer); - /// - /// Converts the values posted by the configuration editor into the actual configuration object. - /// - /// The values posted by the configuration editor. - /// The current configuration object. - object? FromConfigurationEditor(IDictionary? editorValues, object? configuration); + /// + /// Converts the values posted by the configuration editor into the actual configuration object. + /// + /// The values posted by the configuration editor. + /// The current configuration object. + object? FromConfigurationEditor(IDictionary? editorValues, object? configuration); - /// - /// Converts the configuration object to values for the configuration editor. - /// - /// The configuration. - IDictionary ToConfigurationEditor(object? configuration); + /// + /// Converts the configuration object to values for the configuration editor. + /// + /// The configuration. + IDictionary ToConfigurationEditor(object? configuration); - /// - /// Converts the configuration object to values for the value editor. - /// - /// The configuration. - IDictionary? ToValueEditor(object? configuration); - } + /// + /// Converts the configuration object to values for the value editor. + /// + /// The configuration. + IDictionary? ToValueEditor(object? configuration); } diff --git a/src/Umbraco.Core/PropertyEditors/IConfigureValueType.cs b/src/Umbraco.Core/PropertyEditors/IConfigureValueType.cs index 831d5d19fd..47768838d6 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigureValueType.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigureValueType.cs @@ -1,18 +1,17 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a configuration that configures the value type. +/// +/// +/// This is used in to get the value type from the configuration. +/// +public interface IConfigureValueType { /// - /// Represents a configuration that configures the value type. + /// Gets the value type. /// - /// - /// This is used in to get the value type from the configuration. - /// - public interface IConfigureValueType - { - /// - /// Gets the value type. - /// - string ValueType { get; } - } + string ValueType { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs index dba30aaf60..6f72f29cf3 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs @@ -1,75 +1,73 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a data editor. +/// +/// This is the base interface for parameter and property editors. +public interface IDataEditor : IDiscoverable { /// - /// Represents a data editor. + /// Gets the alias of the editor. /// - /// This is the base interface for parameter and property editors. - public interface IDataEditor : IDiscoverable - { - /// - /// Gets the alias of the editor. - /// - string Alias { get; } + string Alias { get; } - /// - /// Gets the type of the editor. - /// - /// An editor can be a property value editor, or a parameter editor. - EditorType Type { get; } + /// + /// Gets the type of the editor. + /// + /// An editor can be a property value editor, or a parameter editor. + EditorType Type { get; } - /// - /// Gets the name of the editor. - /// - string Name { get; } + /// + /// Gets the name of the editor. + /// + string Name { get; } - /// - /// Gets the icon of the editor. - /// - /// Can be used to display editors when presenting them. - string Icon { get; } + /// + /// Gets the icon of the editor. + /// + /// Can be used to display editors when presenting them. + string Icon { get; } - /// - /// Gets the group of the editor. - /// - /// Can be used to organize editors when presenting them. - string Group { get; } + /// + /// Gets the group of the editor. + /// + /// Can be used to organize editors when presenting them. + string Group { get; } - /// - /// Gets a value indicating whether the editor is deprecated. - /// - /// Deprecated editors are supported but not proposed in the UI. - bool IsDeprecated { get; } + /// + /// Gets a value indicating whether the editor is deprecated. + /// + /// Deprecated editors are supported but not proposed in the UI. + bool IsDeprecated { get; } - /// - /// Gets a value editor. - /// - IDataValueEditor GetValueEditor(); // TODO: should be configured?! + /// + /// Gets the configuration for the value editor. + /// + IDictionary? DefaultConfiguration { get; } - /// - /// Gets a configured value editor. - /// - IDataValueEditor GetValueEditor(object? configuration); + /// + /// Gets the index value factory for the editor. + /// + IPropertyIndexValueFactory PropertyIndexValueFactory { get; } - /// - /// Gets the configuration for the value editor. - /// - IDictionary? DefaultConfiguration { get; } + /// + /// Gets a value editor. + /// + IDataValueEditor GetValueEditor(); // TODO: should be configured?! - /// - /// Gets an editor to edit the value editor configuration. - /// - /// - /// Is expected to throw if the editor does not support being configured, e.g. for most parameter editors. - /// - IConfigurationEditor GetConfigurationEditor(); + /// + /// Gets a configured value editor. + /// + IDataValueEditor GetValueEditor(object? configuration); - /// - /// Gets the index value factory for the editor. - /// - IPropertyIndexValueFactory PropertyIndexValueFactory { get; } - } + /// + /// Gets an editor to edit the value editor configuration. + /// + /// + /// Is expected to throw if the editor does not support being configured, e.g. for most parameter editors. + /// + IConfigurationEditor GetConfigurationEditor(); } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueEditorFactory.cs b/src/Umbraco.Core/PropertyEditors/IDataValueEditorFactory.cs index 663c7db6d6..a2f84cd71c 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueEditorFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueEditorFactory.cs @@ -1,11 +1,9 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface IDataValueEditorFactory { - public interface IDataValueEditorFactory - { - TDataValueEditor Create(params object[] args) - where TDataValueEditor : class, IDataValueEditor; - } + TDataValueEditor Create(params object[] args) + where TDataValueEditor : class, IDataValueEditor; } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index d44d732464..39d7d7e130 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -1,19 +1,17 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Resolve references from values +/// +public interface IDataValueReference { /// - /// Resolve references from values + /// Returns any references contained in the value /// - public interface IDataValueReference - { - /// - /// Returns any references contained in the value - /// - /// - /// - IEnumerable GetReferences(object? value); - } + /// + /// + IEnumerable GetReferences(object? value); } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs index fd1f2f50d2..8c768c295f 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs @@ -1,18 +1,16 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - public interface IDataValueReferenceFactory - { - /// - /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). - /// - /// - /// A value indicating whether the converter supports a datatype. - bool IsForEditor(IDataEditor? dataEditor); +namespace Umbraco.Cms.Core.PropertyEditors; - /// - /// - /// - /// - IDataValueReference GetDataValueReference(); - } +public interface IDataValueReferenceFactory +{ + /// + /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). + /// + /// + /// A value indicating whether the converter supports a datatype. + bool IsForEditor(IDataEditor? dataEditor); + + /// + /// + /// + IDataValueReference GetDataValueReference(); } diff --git a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs index 6e9e9221f6..6119543956 100644 --- a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs +++ b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfig.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Marker interface for any editor configuration that supports defining file extensions +/// +public interface IFileExtensionsConfig { - /// - /// Marker interface for any editor configuration that supports defining file extensions - /// - public interface IFileExtensionsConfig - { - List FileExtensions { get; set; } - } + List FileExtensions { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs index d32005fb7f..fa2e8fa5f6 100644 --- a/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs +++ b/src/Umbraco.Core/PropertyEditors/IFileExtensionConfigItem.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - public interface IFileExtensionConfigItem - { - int Id { get; set; } +namespace Umbraco.Cms.Core.PropertyEditors; - string? Value { get; set; } - } +public interface IFileExtensionConfigItem +{ + int Id { get; set; } + + string? Value { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs index d6c20b9cdb..7e6b0c4410 100644 --- a/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs +++ b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Marker interface for any editor configuration that supports Ignoring user start nodes +/// +public interface IIgnoreUserStartNodesConfig { - /// - /// Marker interface for any editor configuration that supports Ignoring user start nodes - /// - public interface IIgnoreUserStartNodesConfig - { - bool IgnoreUserStartNodes { get; set; } - } + bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/IManifestValueValidator.cs b/src/Umbraco.Core/PropertyEditors/IManifestValueValidator.cs index 28cf26022f..31078649a5 100644 --- a/src/Umbraco.Core/PropertyEditors/IManifestValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IManifestValueValidator.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Defines a value validator that can be referenced in a manifest. +/// +/// If the manifest can be configured, then it should expose a Configuration property. +public interface IManifestValueValidator : IValueValidator { /// - /// Defines a value validator that can be referenced in a manifest. + /// Gets the name of the validator. /// - /// If the manifest can be configured, then it should expose a Configuration property. - public interface IManifestValueValidator : IValueValidator - { - /// - /// Gets the name of the validator. - /// - string ValidationName { get; } - } + string ValidationName { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs index 61f31a85c9..2af36b856f 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Determines if a property type's value should be compressed in memory +/// +/// +/// +public interface IPropertyCacheCompression { /// - /// Determines if a property type's value should be compressed in memory + /// Whether a property on the content is/should be compressed /// - /// - /// - /// - public interface IPropertyCacheCompression - {/// - /// Whether a property on the content is/should be compressed - /// - /// The content - /// The property to compress or not - /// Whether this content is the published version - bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias, bool published); - } + /// The content + /// The property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias, bool published); } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs index a63029fc3d..1cff2e7552 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs @@ -1,16 +1,15 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface IPropertyCacheCompressionOptions { - public interface IPropertyCacheCompressionOptions - { - /// - /// Whether a property on the content is/should be compressed - /// - /// The content - /// The property to compress or not - /// The datatype of the property to compress or not - /// Whether this content is the published version - bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published); - } + /// + /// Whether a property on the content is/should be compressed + /// + /// The content + /// The property to compress or not + /// The datatype of the property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published); } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs index 6ac6b46f50..fd607f4054 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs @@ -1,24 +1,26 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a property index value factory. +/// +public interface IPropertyIndexValueFactory { /// - /// Represents a property index value factory. + /// Gets the index values for a property. /// - public interface IPropertyIndexValueFactory - { - /// - /// Gets the index values for a property. - /// - /// - /// Returns key-value pairs, where keys are indexed field names. By default, that would be the property alias, - /// and there would be only one pair, but some implementations (see for instance the grid one) may return more than - /// one pair, with different indexed field names. - /// And then, values are an enumerable of objects, because each indexed field can in turn have multiple - /// values. By default, there would be only one object: the property value. But some implementations may return - /// more than one value for a given field. - /// - IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published); - } + /// + /// + /// Returns key-value pairs, where keys are indexed field names. By default, that would be the property alias, + /// and there would be only one pair, but some implementations (see for instance the grid one) may return more than + /// one pair, with different indexed field names. + /// + /// + /// And then, values are an enumerable of objects, because each indexed field can in turn have multiple + /// values. By default, there would be only one object: the property value. But some implementations may return + /// more than one value for a given field. + /// + /// + IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published); } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs index 499a691204..37d6b82475 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs @@ -1,112 +1,132 @@ -using System; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Provides published content properties conversion service. +/// +/// This is not a simple "value converter" because it really works only for properties. +public interface IPropertyValueConverter : IDiscoverable { /// - /// Provides published content properties conversion service. + /// Gets a value indicating whether the converter supports a property type. /// - /// This is not a simple "value converter" because it really works only for properties. - public interface IPropertyValueConverter : IDiscoverable - { - /// - /// Gets a value indicating whether the converter supports a property type. - /// - /// The property type. - /// A value indicating whether the converter supports a property type. - bool IsConverter(IPublishedPropertyType propertyType); + /// The property type. + /// A value indicating whether the converter supports a property type. + bool IsConverter(IPublishedPropertyType propertyType); - /// - /// Determines whether a value is an actual value, or not a value. - /// - /// - /// Called for Source, Inter and Object levels, until one does not return null. - /// Can return true (is a value), false (is not a value), or null to indicate that it - /// cannot be determined at the specified level. For instance, if source is a string that - /// could contain JSON, the decision could be made on the intermediate value. Or, if it is - /// a picker, it could be made on the object value (the actual picked object). - /// - bool? IsValue(object? value, PropertyValueLevel level); + /// + /// Determines whether a value is an actual value, or not a value. + /// + /// + /// Called for Source, Inter and Object levels, until one does not return null. + /// + /// Can return true (is a value), false (is not a value), or null to indicate that it + /// cannot be determined at the specified level. For instance, if source is a string that + /// could contain JSON, the decision could be made on the intermediate value. Or, if it is + /// a picker, it could be made on the object value (the actual picked object). + /// + /// + bool? IsValue(object? value, PropertyValueLevel level); - /// - /// Gets the type of values returned by the converter. - /// - /// The property type. - /// The CLR type of values returned by the converter. - /// Some of the CLR types may be generated, therefore this method cannot directly return - /// a Type object (which may not exist yet). In which case it needs to return a ModelType instance. - Type GetPropertyValueType(IPublishedPropertyType propertyType); + /// + /// Gets the type of values returned by the converter. + /// + /// The property type. + /// The CLR type of values returned by the converter. + /// + /// Some of the CLR types may be generated, therefore this method cannot directly return + /// a Type object (which may not exist yet). In which case it needs to return a ModelType instance. + /// + Type GetPropertyValueType(IPublishedPropertyType propertyType); - /// - /// Gets the property cache level. - /// - /// The property type. - /// The property cache level. - PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType); + /// + /// Gets the property cache level. + /// + /// The property type. + /// The property cache level. + PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType); - /// - /// Converts a property source value to an intermediate value. - /// - /// The property set owning the property. - /// The property type. - /// The source value. - /// A value indicating whether conversion should take place in preview mode. - /// The result of the conversion. - /// - /// The converter should know how to convert a null source value, meaning that no - /// value has been assigned to the property. The intermediate value can be null. - /// With the XML cache, source values come from the XML cache and therefore are strings. - /// With objects caches, source values would come from the database and therefore be either - /// ints, DateTimes, decimals, or strings. - /// The converter should be prepared to handle both situations. - /// When source values are strings, the converter must handle empty strings, whitespace - /// strings, and xml-whitespace strings appropriately, ie it should know whether to preserve - /// white spaces. - /// - object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview); + /// + /// Converts a property source value to an intermediate value. + /// + /// The property set owning the property. + /// The property type. + /// The source value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// + /// The converter should know how to convert a null source value, meaning that no + /// value has been assigned to the property. The intermediate value can be null. + /// + /// With the XML cache, source values come from the XML cache and therefore are strings. + /// + /// With objects caches, source values would come from the database and therefore be either + /// ints, DateTimes, decimals, or strings. + /// + /// The converter should be prepared to handle both situations. + /// + /// When source values are strings, the converter must handle empty strings, whitespace + /// strings, and xml-whitespace strings appropriately, ie it should know whether to preserve + /// white spaces. + /// + /// + object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview); - /// - /// Converts a property intermediate value to an Object value. - /// - /// The property set owning the property. - /// The property type. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether conversion should take place in preview mode. - /// The result of the conversion. - /// - /// The converter should know how to convert a null intermediate value, or any intermediate value - /// indicating that no value has been assigned to the property. It is up to the converter to determine - /// what to return in that case: either null, or the default value... - /// The is passed to the converter so that it can be, in turn, - /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage - /// the cache levels of property values. It is not meant to be used by the converter. - /// - object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); + /// + /// Converts a property intermediate value to an Object value. + /// + /// The property set owning the property. + /// The property type. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// + /// The converter should know how to convert a null intermediate value, or any intermediate value + /// indicating that no value has been assigned to the property. It is up to the converter to determine + /// what to return in that case: either null, or the default value... + /// + /// + /// The is passed to the converter so that it can be, in turn, + /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage + /// the cache levels of property values. It is not meant to be used by the converter. + /// + /// + object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); - /// - /// Converts a property intermediate value to an XPath value. - /// - /// The property set owning the property. - /// The property type. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether conversion should take place in preview mode. - /// The result of the conversion. - /// - /// The converter should know how to convert a null intermediate value, or any intermediate value - /// indicating that no value has been assigned to the property. It is up to the converter to determine - /// what to return in that case: either null, or the default value... - /// If successful, the result should be either null, a string, or an XPathNavigator - /// instance. Whether an xml-whitespace string should be returned as null or literally, is - /// up to the converter. - /// The converter may want to return an XML fragment that represent a part of the content tree, - /// but should pay attention not to create infinite loops that would kill XPath and XSLT. - /// The is passed to the converter so that it can be, in turn, - /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage - /// the cache levels of property values. It is not meant to be used by the converter. - /// - object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); - } + /// + /// Converts a property intermediate value to an XPath value. + /// + /// The property set owning the property. + /// The property type. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// + /// The converter should know how to convert a null intermediate value, or any intermediate value + /// indicating that no value has been assigned to the property. It is up to the converter to determine + /// what to return in that case: either null, or the default value... + /// + /// + /// If successful, the result should be either null, a string, or an XPathNavigator + /// instance. Whether an xml-whitespace string should be returned as null or literally, is + /// up to the converter. + /// + /// + /// The converter may want to return an XML fragment that represent a part of the content tree, + /// but should pay attention not to create infinite loops that would kill XPath and XSLT. + /// + /// + /// The is passed to the converter so that it can be, in turn, + /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage + /// the cache levels of property values. It is not meant to be used by the converter. + /// + /// + object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); } diff --git a/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs index 9674eaea98..6070512329 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Defines a value format validator. +/// +public interface IValueFormatValidator { /// - /// Defines a value format validator. + /// Validates a value. /// - public interface IValueFormatValidator - { - /// - /// Validates a value. - /// - /// The value to validate. - /// The value type. - /// A format definition. - /// Validation results. - /// - /// The is expected to be a valid regular expression. - /// This is used to validate values against the property type validation regular expression. - /// - IEnumerable ValidateFormat(object? value, string valueType, string format); - } + /// The value to validate. + /// The value type. + /// A format definition. + /// Validation results. + /// + /// The is expected to be a valid regular expression. + /// This is used to validate values against the property type validation regular expression. + /// + IEnumerable ValidateFormat(object? value, string valueType, string format); } diff --git a/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs index 439bfcdc81..3bbc348431 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Defines a required value validator. +/// +public interface IValueRequiredValidator { /// - /// Defines a required value validator. + /// Validates a value. /// - public interface IValueRequiredValidator - { - /// - /// Validates a value. - /// - /// The value to validate. - /// The value type. - /// Validation results. - /// - /// This is used to validate values when the property type specifies that a value is required. - /// - IEnumerable ValidateRequired(object? value, string valueType); - } + /// The value to validate. + /// The value type. + /// Validation results. + /// + /// This is used to validate values when the property type specifies that a value is required. + /// + IEnumerable ValidateRequired(object? value, string valueType); } diff --git a/src/Umbraco.Core/PropertyEditors/IValueValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueValidator.cs index b4304fad59..7d26f8a96c 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueValidator.cs @@ -1,23 +1,24 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Defines a value validator. +/// +public interface IValueValidator { /// - /// Defines a value validator. + /// Validates a value. /// - public interface IValueValidator - { - /// - /// Validates a value. - /// - /// The value to validate. - /// The value type. - /// A datatype configuration. - /// Validation results. - /// - /// The value can be a string, a Json structure (JObject, JArray...)... corresponding to what was posted by an editor. - /// - IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration); - } + /// The value to validate. + /// The value type. + /// A datatype configuration. + /// Validation results. + /// + /// + /// The value can be a string, a Json structure (JObject, JArray...)... corresponding to what was posted by an + /// editor. + /// + /// + IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration); } diff --git a/src/Umbraco.Core/PropertyEditors/IntegerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IntegerConfigurationEditor.cs index e7c2114dd2..e5d01900c6 100644 --- a/src/Umbraco.Core/PropertyEditors/IntegerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IntegerConfigurationEditor.cs @@ -1,37 +1,36 @@ -using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.PropertyEditors.Validators; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. +/// +public class IntegerConfigurationEditor : ConfigurationEditor { - /// - /// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. - /// - public class IntegerConfigurationEditor : ConfigurationEditor + public IntegerConfigurationEditor() { - public IntegerConfigurationEditor() + Fields.Add(new ConfigurationField(new IntegerValidator()) { - Fields.Add(new ConfigurationField(new IntegerValidator()) - { - Description = "Enter the minimum amount of number to be entered", - Key = "min", - View = "number", - Name = "Minimum" - }); + Description = "Enter the minimum amount of number to be entered", + Key = "min", + View = "number", + Name = "Minimum", + }); - Fields.Add(new ConfigurationField(new IntegerValidator()) - { - Description = "Enter the intervals amount between each step of number to be entered", - Key = "step", - View = "number", - Name = "Step Size" - }); + Fields.Add(new ConfigurationField(new IntegerValidator()) + { + Description = "Enter the intervals amount between each step of number to be entered", + Key = "step", + View = "number", + Name = "Step Size", + }); - Fields.Add(new ConfigurationField(new IntegerValidator()) - { - Description = "Enter the maximum amount of number to be entered", - Key = "max", - View = "number", - Name = "Maximum" - }); - } + Fields.Add(new ConfigurationField(new IntegerValidator()) + { + Description = "Enter the maximum amount of number to be entered", + Key = "max", + View = "number", + Name = "Maximum", + }); } } diff --git a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs index f243158db3..be95623b56 100644 --- a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs @@ -1,38 +1,33 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors.Validators; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents an integer property and parameter editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.Integer, + EditorType.PropertyValue | EditorType.MacroParameter, + "Numeric", + "integer", + ValueType = ValueTypes.Integer)] +public class IntegerPropertyEditor : DataEditor { - /// - /// Represents an integer property and parameter editor. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.Integer, - EditorType.PropertyValue | EditorType.MacroParameter, - "Numeric", - "integer", - ValueType = ValueTypes.Integer)] - public class IntegerPropertyEditor : DataEditor + public IntegerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public IntegerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } - - /// - protected override IDataValueEditor CreateValueEditor() - { - var editor = base.CreateValueEditor(); - editor.Validators.Add(new IntegerValidator()); // ensure the value is validated - return editor; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new IntegerConfigurationEditor(); } + + /// + protected override IDataValueEditor CreateValueEditor() + { + IDataValueEditor editor = base.CreateValueEditor(); + editor.Validators.Add(new IntegerValidator()); // ensure the value is validated + return editor; + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new IntegerConfigurationEditor(); } diff --git a/src/Umbraco.Core/PropertyEditors/LabelConfiguration.cs b/src/Umbraco.Core/PropertyEditors/LabelConfiguration.cs index 28fe05d151..f023b86a78 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelConfiguration.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the label value editor. +/// +public class LabelConfiguration : IConfigureValueType { - /// - /// Represents the configuration for the label value editor. - /// - public class LabelConfiguration : IConfigureValueType - { - [ConfigurationField(Constants.PropertyEditors.ConfigurationKeys.DataValueType, "Value type", "valuetype")] - public string ValueType { get; set; } = ValueTypes.String; - } + [ConfigurationField(Constants.PropertyEditors.ConfigurationKeys.DataValueType, "Value type", "valuetype")] + public string ValueType { get; set; } = ValueTypes.String; } diff --git a/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs index b2a214f729..cb5a531f65 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs @@ -1,49 +1,51 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the label value editor. +/// +public class LabelConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration for the label value editor. - /// - public class LabelConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes and IEditorConfigurationParser instead")] + public LabelConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes and IEditorConfigurationParser instead")] - public LabelConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + } + + public LabelConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { + } + + /// + public override LabelConfiguration FromConfigurationEditor( + IDictionary? editorValues, + LabelConfiguration? configuration) + { + var newConfiguration = new LabelConfiguration(); + + // get the value type + // not simply deserializing Json because we want to validate the valueType + if (editorValues is not null && editorValues.TryGetValue( + Constants.PropertyEditors.ConfigurationKeys.DataValueType, + out var valueTypeObj) + && valueTypeObj is string stringValue) { - } - - public LabelConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } - - /// - public override LabelConfiguration FromConfigurationEditor(IDictionary? editorValues, LabelConfiguration? configuration) - { - var newConfiguration = new LabelConfiguration(); - - // get the value type - // not simply deserializing Json because we want to validate the valueType - - if (editorValues is not null && editorValues.TryGetValue(Cms.Core.Constants.PropertyEditors.ConfigurationKeys.DataValueType, out var valueTypeObj) - && valueTypeObj is string stringValue) + // validate + if (!string.IsNullOrWhiteSpace(stringValue) && ValueTypes.IsValue(stringValue)) { - if (!string.IsNullOrWhiteSpace(stringValue) && ValueTypes.IsValue(stringValue)) // validate - newConfiguration.ValueType = stringValue; + newConfiguration.ValueType = stringValue; } - - return newConfiguration; } - + return newConfiguration; } } diff --git a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs index c142b581d0..d9fd8694e9 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs @@ -1,7 +1,6 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -10,61 +9,65 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a property editor for label properties. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.Label, + "Label", + "readonlyvalue", + Icon = "icon-readonly")] +public class LabelPropertyEditor : DataEditor { - /// - /// Represents a property editor for label properties. - /// - [DataEditor( - Cms.Core.Constants.PropertyEditors.Aliases.Label, - "Label", - "readonlyvalue", - Icon = "icon-readonly")] - public class LabelPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public LabelPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper) + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public LabelPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper) - : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + /// + /// Initializes a new instance of the class. + /// + public LabelPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + } - /// - /// Initializes a new instance of the class. - /// - public LabelPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) + /// + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new LabelConfigurationEditor(_ioHelper, _editorConfigurationParser); + + // provides the property value editor + internal class LabelPropertyValueEditor : DataValueEditor + { + public LabelPropertyValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; } /// - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new LabelConfigurationEditor(_ioHelper, _editorConfigurationParser); - - // provides the property value editor - internal class LabelPropertyValueEditor : DataValueEditor - { - public LabelPropertyValueEditor( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) - { } - - /// - public override bool IsReadOnly => true; - } + public override bool IsReadOnly => true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs index 055867e80b..13f423a328 100644 --- a/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs @@ -1,128 +1,153 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the listview value editor. +/// +public class ListViewConfiguration { - /// - /// Represents the configuration for the listview value editor. - /// - public class ListViewConfiguration + public ListViewConfiguration() { - public ListViewConfiguration() + // initialize defaults + PageSize = 10; + OrderBy = "SortOrder"; + OrderDirection = "asc"; + + BulkActionPermissions = new BulkActionPermissionSettings { - // initialize defaults + AllowBulkPublish = true, + AllowBulkUnpublish = true, + AllowBulkCopy = true, + AllowBulkMove = true, + AllowBulkDelete = true, + }; - PageSize = 10; - OrderBy = "SortOrder"; - OrderDirection = "asc"; - - BulkActionPermissions = new BulkActionPermissionSettings + Layouts = new[] + { + new Layout { - AllowBulkPublish = true, - AllowBulkUnpublish = true, - AllowBulkCopy = true, - AllowBulkMove = true, - AllowBulkDelete = true - }; - - Layouts = new[] + Name = "List", + Icon = "icon-list", + IsSystem = 1, + Selected = true, + Path = "views/propertyeditors/listview/layouts/list/list.html", + }, + new Layout { - new Layout { Name = "List", Icon = "icon-list", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/list/list.html" }, - new Layout { Name = "Grid", Icon = "icon-thumbnails-small", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/grid/grid.html" } - }; + Name = "Grid", + Icon = "icon-thumbnails-small", + IsSystem = 1, + Selected = true, + Path = "views/propertyeditors/listview/layouts/grid/grid.html", + }, + }; - IncludeProperties = new[] - { - new Property { Alias = "sortOrder", Header = "Sort order", IsSystem = 1 }, - new Property { Alias = "updateDate", Header = "Last edited", IsSystem = 1 }, - new Property { Alias = "owner", Header = "Created by", IsSystem = 1 } - }; - } + IncludeProperties = new[] + { + new Property { Alias = "sortOrder", Header = "Sort order", IsSystem = 1 }, + new Property { Alias = "updateDate", Header = "Last edited", IsSystem = 1 }, + new Property { Alias = "owner", Header = "Created by", IsSystem = 1 }, + }; + } - [ConfigurationField("pageSize", "Page Size", "number", Description = "Number of items per page")] - public int PageSize { get; set; } + [ConfigurationField("pageSize", "Page Size", "number", Description = "Number of items per page")] + public int PageSize { get; set; } - [ConfigurationField("orderBy", "Order By", "views/propertyeditors/listview/sortby.prevalues.html", - Description = "The default sort order for the list")] - public string OrderBy { get; set; } + [ConfigurationField("orderBy", "Order By", "views/propertyeditors/listview/sortby.prevalues.html", Description = "The default sort order for the list")] + public string OrderBy { get; set; } - [ConfigurationField("orderDirection", "Order Direction", "views/propertyeditors/listview/orderDirection.prevalues.html")] - public string OrderDirection { get; set; } + [ConfigurationField("orderDirection", "Order Direction", "views/propertyeditors/listview/orderDirection.prevalues.html")] + public string OrderDirection { get; set; } - [ConfigurationField("includeProperties", "Columns Displayed", "views/propertyeditors/listview/includeproperties.prevalues.html", - Description = "The properties that will be displayed for each column")] - public Property[] IncludeProperties { get; set; } + [ConfigurationField( + "includeProperties", + "Columns Displayed", + "views/propertyeditors/listview/includeproperties.prevalues.html", + Description = "The properties that will be displayed for each column")] + public Property[] IncludeProperties { get; set; } - [ConfigurationField("layouts", "Layouts", "views/propertyeditors/listview/layouts.prevalues.html")] - public Layout[] Layouts { get; set; } + [ConfigurationField("layouts", "Layouts", "views/propertyeditors/listview/layouts.prevalues.html")] + public Layout[] Layouts { get; set; } - [ConfigurationField("bulkActionPermissions", "Bulk Action Permissions", "views/propertyeditors/listview/bulkActionPermissions.prevalues.html", - Description = "The bulk actions that are allowed from the list view")] - public BulkActionPermissionSettings BulkActionPermissions { get; set; } = new BulkActionPermissionSettings(); // TODO: managing defaults? + [ConfigurationField( + "bulkActionPermissions", + "Bulk Action Permissions", + "views/propertyeditors/listview/bulkActionPermissions.prevalues.html", + Description = "The bulk actions that are allowed from the list view")] + public BulkActionPermissionSettings BulkActionPermissions { get; set; } = new(); // TODO: managing defaults? - [ConfigurationField("icon", "Content app icon", "views/propertyeditors/listview/icon.prevalues.html", Description = "The icon of the listview content app")] + [ConfigurationField("icon", "Content app icon", "views/propertyeditors/listview/icon.prevalues.html", Description = "The icon of the listview content app")] + public string? Icon { get; set; } + + [ConfigurationField("tabName", "Content app name", "textstring", Description = "The name of the listview content app (default if empty: 'Child Items')")] + public string? TabName { get; set; } + + [ConfigurationField( + "showContentFirst", + "Show Content App First", + "boolean", + Description = "Enable this to show the content app by default instead of the list view app")] + public bool ShowContentFirst { get; set; } + + [ConfigurationField( + "useInfiniteEditor", + "Edit in Infinite Editor", + "boolean", + Description = "Enable this to use infinite editing to edit the content of the list view")] + public bool UseInfiniteEditor { get; set; } + + [DataContract] + public class Property + { + [DataMember(Name = "alias")] + public string? Alias { get; set; } + + [DataMember(Name = "header")] + public string? Header { get; set; } + + [DataMember(Name = "nameTemplate")] + public string? Template { get; set; } + + [DataMember(Name = "isSystem")] + public int IsSystem { get; set; } // TODO: bool + } + + [DataContract] + public class Layout + { + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "path")] + public string? Path { get; set; } + + [DataMember(Name = "icon")] public string? Icon { get; set; } - [ConfigurationField("tabName", "Content app name", "textstring", Description = "The name of the listview content app (default if empty: 'Child Items')")] - public string? TabName { get; set; } + [DataMember(Name = "isSystem")] + public int IsSystem { get; set; } // TODO: bool - [ConfigurationField("showContentFirst", "Show Content App First", "boolean", Description = "Enable this to show the content app by default instead of the list view app")] - public bool ShowContentFirst { get; set; } + [DataMember(Name = "selected")] + public bool Selected { get; set; } + } - [ConfigurationField("useInfiniteEditor", "Edit in Infinite Editor", "boolean", Description = "Enable this to use infinite editing to edit the content of the list view")] - public bool UseInfiniteEditor { get; set; } + [DataContract] + public class BulkActionPermissionSettings + { + [DataMember(Name = "allowBulkPublish")] + public bool AllowBulkPublish { get; set; } = true; - [DataContract] - public class Property - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } + [DataMember(Name = "allowBulkUnpublish")] + public bool AllowBulkUnpublish { get; set; } = true; - [DataMember(Name = "header")] - public string? Header { get; set; } + [DataMember(Name = "allowBulkCopy")] + public bool AllowBulkCopy { get; set; } = true; - [DataMember(Name = "nameTemplate")] - public string? Template { get; set; } + [DataMember(Name = "allowBulkMove")] + public bool AllowBulkMove { get; set; } = true; - [DataMember(Name = "isSystem")] - public int IsSystem { get; set; } // TODO: bool - } - - [DataContract] - public class Layout - { - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "path")] - public string? Path { get; set; } - - [DataMember(Name = "icon")] - public string? Icon { get; set; } - - [DataMember(Name = "isSystem")] - public int IsSystem { get; set; } // TODO: bool - - [DataMember(Name = "selected")] - public bool Selected { get; set; } - } - - [DataContract] - public class BulkActionPermissionSettings - { - [DataMember(Name = "allowBulkPublish")] - public bool AllowBulkPublish { get; set; } = true; - - [DataMember(Name = "allowBulkUnpublish")] - public bool AllowBulkUnpublish { get; set; } = true; - - [DataMember(Name = "allowBulkCopy")] - public bool AllowBulkCopy { get; set; } = true; - - [DataMember(Name = "allowBulkMove")] - public bool AllowBulkMove { get; set; } = true; - - [DataMember(Name = "allowBulkDelete")] - public bool AllowBulkDelete { get; set; } = true; - } + [DataMember(Name = "allowBulkDelete")] + public bool AllowBulkDelete { get; set; } = true; } } diff --git a/src/Umbraco.Core/PropertyEditors/ListViewConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ListViewConfigurationEditor.cs index d673ce4ee6..8ecab6d751 100644 --- a/src/Umbraco.Core/PropertyEditors/ListViewConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ListViewConfigurationEditor.cs @@ -1,28 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the listview value editor. - /// - public class ListViewConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public ListViewConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public ListViewConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the listview value editor. +/// +public class ListViewConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public ListViewConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public ListViewConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs index 81b1c1fba1..f2a08076b9 100644 --- a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs @@ -1,32 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class ManifestValueValidatorCollection : BuilderCollectionBase { - public class ManifestValueValidatorCollection : BuilderCollectionBase + public ManifestValueValidatorCollection(Func> items) + : base(items) { - public ManifestValueValidatorCollection(Func> items) : base(items) + } + + public IManifestValueValidator? Create(string name) + { + IManifestValueValidator v = GetByName(name); + + // TODO: what is this exactly? + // we cannot return this instance, need to clone it? + return (IManifestValueValidator?)Activator.CreateInstance(v.GetType()); // ouch + } + + public IManifestValueValidator GetByName(string name) + { + IManifestValueValidator? v = this.FirstOrDefault(x => x.ValidationName.InvariantEquals(name)); + if (v == null) { + throw new InvalidOperationException($"Could not find a validator named \"{name}\"."); } - public IManifestValueValidator? Create(string name) - { - var v = GetByName(name); - - // TODO: what is this exactly? - // we cannot return this instance, need to clone it? - return (IManifestValueValidator?) Activator.CreateInstance(v.GetType()); // ouch - } - - public IManifestValueValidator GetByName(string name) - { - var v = this.FirstOrDefault(x => x.ValidationName.InvariantEquals(name)); - if (v == null) - throw new InvalidOperationException($"Could not find a validator named \"{name}\"."); - return v; - } + return v; } } diff --git a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs index 66a967c828..044c7f2c0c 100644 --- a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class ManifestValueValidatorCollectionBuilder : SetCollectionBuilderBase { - public class ManifestValueValidatorCollectionBuilder : SetCollectionBuilderBase - { - protected override ManifestValueValidatorCollectionBuilder This => this; - } + protected override ManifestValueValidatorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs index 62ddd4c053..b11ef08f30 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs @@ -1,18 +1,16 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the markdown value editor. +/// +public class MarkdownConfiguration { - /// - /// Represents the configuration for the markdown value editor. - /// - public class MarkdownConfiguration - { - [ConfigurationField("preview", "Preview", "boolean", Description = "Display a live preview")] - public bool DisplayLivePreview { get; set; } + [ConfigurationField("preview", "Preview", "boolean", Description = "Display a live preview")] + public bool DisplayLivePreview { get; set; } - [ConfigurationField("defaultValue", "Default value", "textarea", Description = "If value is blank, the editor will show this")] - public string? DefaultValue { get; set; } + [ConfigurationField("defaultValue", "Default value", "textarea", Description = "If value is blank, the editor will show this")] + public string? DefaultValue { get; set; } - - [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] - public string? OverlaySize { get; set; } - } + [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] + public string? OverlaySize { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MarkdownConfigurationEditor.cs index 3f9bc61275..032bafd12b 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownConfigurationEditor.cs @@ -4,15 +4,15 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editorfor the markdown value editor. +/// +internal class MarkdownConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editorfor the markdown value editor. - /// - internal class MarkdownConfigurationEditor : ConfigurationEditor + public MarkdownConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - public MarkdownConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } } } diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs index 6db2ac552e..3cabd3a306 100644 --- a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs @@ -1,52 +1,51 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a markdown editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.MarkdownEditor, + "Markdown editor", + "markdowneditor", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.RichContent, + Icon = "icon-code")] +public class MarkdownPropertyEditor : DataEditor { - /// - /// Represents a markdown editor. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.MarkdownEditor, - "Markdown editor", - "markdowneditor", - ValueType = ValueTypes.Text, - Group = Constants.PropertyEditors.Groups.RichContent, - Icon = "icon-code")] - public class MarkdownPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MarkdownPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper) + : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MarkdownPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper) - : this(dataValueEditorFactory, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } - - /// - /// Initializes a new instance of the class. - /// - public MarkdownPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) - { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new MarkdownConfigurationEditor(_ioHelper, _editorConfigurationParser); } + + /// + /// Initializes a new instance of the class. + /// + public MarkdownPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new MarkdownConfigurationEditor(_ioHelper, _editorConfigurationParser); } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs b/src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs index 8b843fdf85..11ed4d1afd 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPicker3Configuration.cs @@ -1,61 +1,64 @@ using System.Runtime.Serialization; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Represents the configuration for the media picker value editor. +/// +public class MediaPicker3Configuration : IIgnoreUserStartNodesConfig { - /// - /// Represents the configuration for the media picker value editor. - /// - public class MediaPicker3Configuration : IIgnoreUserStartNodesConfig + [ConfigurationField("filter", "Accepted types", "treesourcetypepicker", Description = "Limit to specific types")] + public string? Filter { get; set; } + + [ConfigurationField("multiple", "Pick multiple items", "boolean", Description = "Outputs a IEnumerable")] + public bool Multiple { get; set; } + + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")] + public NumberRange ValidationLimit { get; set; } = new(); + + [ConfigurationField("startNodeId", "Start node", "mediapicker")] + public Udi? StartNodeId { get; set; } + + [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")] + public bool EnableLocalFocalPoint { get; set; } + + [ConfigurationField( + "crops", + "Image Crops", + "views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html", + Description = "Local crops, stored on document")] + public CropConfiguration[]? Crops { get; set; } + + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } + + [DataContract] + public class NumberRange { - [ConfigurationField("filter", "Accepted types", "treesourcetypepicker", - Description = "Limit to specific types")] - public string? Filter { get; set; } + [DataMember(Name = "min")] + public int? Min { get; set; } - [ConfigurationField("multiple", "Pick multiple items", "boolean", Description = "Outputs a IEnumerable")] - public bool Multiple { get; set; } + [DataMember(Name = "max")] + public int? Max { get; set; } + } - [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")] - public NumberRange ValidationLimit { get; set; } = new NumberRange(); + [DataContract] + public class CropConfiguration + { + [DataMember(Name = "alias")] + public string? Alias { get; set; } - [DataContract] - public class NumberRange - { - [DataMember(Name = "min")] - public int? Min { get; set; } + [DataMember(Name = "label")] + public string? Label { get; set; } - [DataMember(Name = "max")] - public int? Max { get; set; } - } + [DataMember(Name = "width")] + public int Width { get; set; } - [ConfigurationField("startNodeId", "Start node", "mediapicker")] - public Udi? StartNodeId { get; set; } - - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - - [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")] - public bool EnableLocalFocalPoint { get; set; } - - [ConfigurationField("crops", "Image Crops", "views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html", Description = "Local crops, stored on document")] - public CropConfiguration[]? Crops { get; set; } - - [DataContract] - public class CropConfiguration - { - [DataMember(Name = "alias")] - public string? Alias { get; set; } - - [DataMember(Name = "label")] - public string? Label { get; set; } - - [DataMember(Name = "width")] - public int Width { get; set; } - - [DataMember(Name = "height")] - public int Height { get; set; } - } + [DataMember(Name = "height")] + public int Height { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPicker3ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MediaPicker3ConfigurationEditor.cs index c5ab1c403c..9ccf64a6f0 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPicker3ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPicker3ConfigurationEditor.cs @@ -1,38 +1,35 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the media picker value editor. +/// +public class MediaPicker3ConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editor for the media picker value editor. - /// - public class MediaPicker3ConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MediaPicker3ConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MediaPicker3ConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + } - /// - /// Initializes a new instance of the class. - /// - public MediaPicker3ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - // configure fields - // this is not part of ContentPickerConfiguration, - // but is required to configure the UI editor (when editing the configuration) + /// + /// Initializes a new instance of the class. + /// + public MediaPicker3ConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + Field(nameof(MediaPicker3Configuration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; - Field(nameof(MediaPicker3Configuration.StartNodeId)) - .Config = new Dictionary { { "idType", "udi" } }; - - Field(nameof(MediaPicker3Configuration.Filter)) - .Config = new Dictionary { { "itemType", "media" } }; - } + Field(nameof(MediaPicker3Configuration.Filter)) + .Config = new Dictionary { { "itemType", "media" } }; } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs index d18eeac644..055f4fea4d 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs @@ -1,25 +1,26 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the media picker value editor. +/// +public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig { - /// - /// Represents the configuration for the media picker value editor. - /// - public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig - { - [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] - public bool Multiple { get; set; } + [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] + public bool Multiple { get; set; } - [ConfigurationField("onlyImages", "Pick only images", "boolean", Description = "Only let the editor choose images from media.")] - public bool OnlyImages { get; set; } + [ConfigurationField("onlyImages", "Pick only images", "boolean", Description = "Only let the editor choose images from media.")] + public bool OnlyImages { get; set; } - [ConfigurationField("disableFolderSelect", "Disable folder select", "boolean", Description = "Do not allow folders to be picked.")] - public bool DisableFolderSelect { get; set; } + [ConfigurationField("disableFolderSelect", "Disable folder select", "boolean", Description = "Do not allow folders to be picked.")] + public bool DisableFolderSelect { get; set; } - [ConfigurationField("startNodeId", "Start node", "mediapicker")] - public Udi? StartNodeId { get; set; } + [ConfigurationField("startNodeId", "Start node", "mediapicker")] + public Udi? StartNodeId { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs index a3dbbc04d7..62e9eac439 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs @@ -1,49 +1,46 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the media picker value editor. +/// +public class MediaPickerConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editor for the media picker value editor. - /// - public class MediaPickerConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MediaPickerConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MediaPickerConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + } - /// - /// Initializes a new instance of the class. - /// - public MediaPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - // configure fields - // this is not part of ContentPickerConfiguration, - // but is required to configure the UI editor (when editing the configuration) - Field(nameof(MediaPickerConfiguration.StartNodeId)) - .Config = new Dictionary { { "idType", "udi" } }; - } + /// + /// Initializes a new instance of the class. + /// + public MediaPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) => - public override IDictionary ToValueEditor(object? configuration) - { - // get the configuration fields - var d = base.ToValueEditor(configuration); + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + Field(nameof(MediaPickerConfiguration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; - // add extra fields - // not part of ContentPickerConfiguration but used to configure the UI editor - d["idType"] = "udi"; + public override IDictionary ToValueEditor(object? configuration) + { + // get the configuration fields + IDictionary d = base.ToValueEditor(configuration); - return d; - } + // add extra fields + // not part of ContentPickerConfiguration but used to configure the UI editor + d["idType"] = "udi"; + + return d; } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs index a58203c7b5..360ba1b023 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs @@ -1,34 +1,32 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors -{ - public class MediaUrlGeneratorCollection : BuilderCollectionBase - { - public MediaUrlGeneratorCollection(Func> items) - : base(items) - { } +namespace Umbraco.Cms.Core.PropertyEditors; - public bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath) +public class MediaUrlGeneratorCollection : BuilderCollectionBase +{ + public MediaUrlGeneratorCollection(Func> items) + : base(items) + { + } + + public bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath) + { + // We can't get a media path from a null value + // The value will be null when uploading a brand new image, since we try to get the "old path" which doesn't exist yet + if (value is not null) { - // We can't get a media path from a null value - // The value will be null when uploading a brand new image, since we try to get the "old path" which doesn't exist yet - if (value is not null) + foreach (IMediaUrlGenerator generator in this) { - foreach (IMediaUrlGenerator generator in this) + if (generator.TryGetMediaPath(propertyEditorAlias, value, out var generatorMediaPath)) { - if (generator.TryGetMediaPath(propertyEditorAlias, value, out var generatorMediaPath)) - { - mediaPath = generatorMediaPath; - return true; - } + mediaPath = generatorMediaPath; + return true; } } - - mediaPath = null; - return false; } + + mediaPath = null; + return false; } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs index 57ab93832b..0c9bf6070f 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs @@ -1,10 +1,9 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class MediaUrlGeneratorCollectionBuilder : SetCollectionBuilderBase { - public class MediaUrlGeneratorCollectionBuilder : SetCollectionBuilderBase - { - protected override MediaUrlGeneratorCollectionBuilder This => this; - } + protected override MediaUrlGeneratorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs index cccf0fe2b7..221481328b 100644 --- a/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs @@ -1,23 +1,17 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +[DataEditor( + Constants.PropertyEditors.Aliases.MemberGroupPicker, + "Member Group Picker", + "membergrouppicker", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.MemberGroup)] +public class MemberGroupPickerPropertyEditor : DataEditor { - [DataEditor( - Constants.PropertyEditors.Aliases.MemberGroupPicker, - "Member Group Picker", - "membergrouppicker", - ValueType = ValueTypes.Text, - Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.MemberGroup)] - public class MemberGroupPickerPropertyEditor : DataEditor + public MemberGroupPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public MemberGroupPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } } } diff --git a/src/Umbraco.Core/PropertyEditors/MemberPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MemberPickerConfiguration.cs index 6d6fb3a8b7..dc0ab648df 100644 --- a/src/Umbraco.Core/PropertyEditors/MemberPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MemberPickerConfiguration.cs @@ -1,12 +1,7 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +public class MemberPickerConfiguration : ConfigurationEditor { - public class MemberPickerConfiguration : ConfigurationEditor - { - public override IDictionary DefaultConfiguration => new Dictionary - { - { "idType", "udi" } - }; - } + public override IDictionary DefaultConfiguration => + new Dictionary { { "idType", "udi" } }; } diff --git a/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs index d348d6f22e..055bd354fd 100644 --- a/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs @@ -1,25 +1,19 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +[DataEditor( + Constants.PropertyEditors.Aliases.MemberPicker, + "Member Picker", + "memberpicker", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.Member)] +public class MemberPickerPropertyEditor : DataEditor { - [DataEditor( - Constants.PropertyEditors.Aliases.MemberPicker, - "Member Picker", - "memberpicker", - ValueType = ValueTypes.String, - Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.Member)] - public class MemberPickerPropertyEditor : DataEditor + public MemberPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public MemberPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } - - protected override IConfigurationEditor CreateConfigurationEditor() => new MemberPickerConfiguration(); } + + protected override IConfigurationEditor CreateConfigurationEditor() => new MemberPickerConfiguration(); } diff --git a/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs index ba1d03e7bb..c256c7b483 100644 --- a/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs @@ -1,43 +1,32 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a temporary representation of an editor for cases where a data type is created but not editor is +/// available. +/// +public class MissingPropertyEditor : IDataEditor { - /// - /// Represents a temporary representation of an editor for cases where a data type is created but not editor is available. - /// - public class MissingPropertyEditor : IDataEditor - { - public string Alias => "Umbraco.Missing"; + public string Alias => "Umbraco.Missing"; - public EditorType Type => EditorType.Nothing; + public EditorType Type => EditorType.Nothing; - public string Name => "Missing property editor"; + public string Name => "Missing property editor"; - public string Icon => string.Empty; + public string Icon => string.Empty; - public string Group => string.Empty; + public string Group => string.Empty; - public bool IsDeprecated => false; + public bool IsDeprecated => false; - public IDictionary DefaultConfiguration => throw new NotImplementedException(); + public IDictionary DefaultConfiguration => throw new NotImplementedException(); - public IPropertyIndexValueFactory PropertyIndexValueFactory => throw new NotImplementedException(); + public IPropertyIndexValueFactory PropertyIndexValueFactory => throw new NotImplementedException(); - public IConfigurationEditor GetConfigurationEditor() - { - return new ConfigurationEditor(); - } + public IConfigurationEditor GetConfigurationEditor() => new ConfigurationEditor(); - public IDataValueEditor GetValueEditor() - { - throw new NotImplementedException(); - } + public IDataValueEditor GetValueEditor() => throw new NotImplementedException(); - public IDataValueEditor GetValueEditor(object? configuration) - { - throw new NotImplementedException(); - } - } + public IDataValueEditor GetValueEditor(object? configuration) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfiguration.cs index 2825b5b8af..c1ca368c47 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfiguration.cs @@ -1,28 +1,29 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the multinode picker value editor. +/// +public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig { - /// - /// Represents the configuration for the multinode picker value editor. - /// - public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig - { - [ConfigurationField("startNode", "Node type", "treesource")] - public MultiNodePickerConfigurationTreeSource? TreeSource { get; set; } + [ConfigurationField("startNode", "Node type", "treesource")] + public MultiNodePickerConfigurationTreeSource? TreeSource { get; set; } - [ConfigurationField("filter", "Allow items of type", "treesourcetypepicker", Description = "Select the applicable types")] - public string? Filter { get; set; } + [ConfigurationField("filter", "Allow items of type", "treesourcetypepicker", Description = "Select the applicable types")] + public string? Filter { get; set; } - [ConfigurationField("minNumber", "Minimum number of items", "number")] - public int MinNumber { get; set; } + [ConfigurationField("minNumber", "Minimum number of items", "number")] + public int MinNumber { get; set; } - [ConfigurationField("maxNumber", "Maximum number of items", "number")] - public int MaxNumber { get; set; } + [ConfigurationField("maxNumber", "Maximum number of items", "number")] + public int MaxNumber { get; set; } - [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] - public bool ShowOpen { get; set; } + [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] + public bool ShowOpen { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs index aa66be9d39..a377dae5db 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs @@ -1,53 +1,49 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the multinode picker value editor. +/// +public class MultiNodePickerConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration for the multinode picker value editor. - /// - public class MultiNodePickerConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MultiNodePickerConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MultiNodePickerConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } + } - public MultiNodePickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - Field(nameof(MultiNodePickerConfiguration.TreeSource)) - .Config = new Dictionary { { "idType", "udi" } }; - } + public MultiNodePickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) => + Field(nameof(MultiNodePickerConfiguration.TreeSource)) + .Config = new Dictionary { { "idType", "udi" } }; - /// - public override Dictionary ToConfigurationEditor(MultiNodePickerConfiguration? configuration) - { - // sanitize configuration - var output = base.ToConfigurationEditor(configuration); + /// + public override Dictionary ToConfigurationEditor(MultiNodePickerConfiguration? configuration) + { + // sanitize configuration + Dictionary output = base.ToConfigurationEditor(configuration); - output["multiPicker"] = configuration?.MaxNumber > 1; + output["multiPicker"] = configuration?.MaxNumber > 1; - return output; - } + return output; + } - /// - public override IDictionary ToValueEditor(object? configuration) - { - var d = base.ToValueEditor(configuration); - d["multiPicker"] = true; - d["showEditButton"] = false; - d["showPathOnHover"] = false; - d["idType"] = "udi"; - return d; - } + /// + public override IDictionary ToValueEditor(object? configuration) + { + IDictionary d = base.ToValueEditor(configuration); + d["multiPicker"] = true; + d["showEditButton"] = false; + d["showPathOnHover"] = false; + d["idType"] = "udi"; + return d; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs index bc48bbdd54..2dcd0f6e93 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs @@ -1,20 +1,19 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the 'startNode' value for the +/// +[DataContract] +public class MultiNodePickerConfigurationTreeSource { - /// - /// Represents the 'startNode' value for the - /// - [DataContract] - public class MultiNodePickerConfigurationTreeSource - { - [DataMember(Name = "type")] - public string? ObjectType { get; set; } + [DataMember(Name = "type")] + public string? ObjectType { get; set; } - [DataMember(Name = "query")] - public string? StartNodeQuery { get; set; } + [DataMember(Name = "query")] + public string? StartNodeQuery { get; set; } - [DataMember(Name = "id")] - public Udi? StartNodeId { get; set; } - } + [DataMember(Name = "id")] + public Udi? StartNodeId { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs index caf933e6ad..35d51cb944 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs @@ -1,25 +1,27 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class MultiUrlPickerConfiguration : IIgnoreUserStartNodesConfig { + [ConfigurationField("minNumber", "Minimum number of items", "number")] + public int MinNumber { get; set; } - public class MultiUrlPickerConfiguration : IIgnoreUserStartNodesConfig - { - [ConfigurationField("minNumber", "Minimum number of items", "number")] - public int MinNumber { get; set; } + [ConfigurationField("maxNumber", "Maximum number of items", "number")] + public int MaxNumber { get; set; } - [ConfigurationField("maxNumber", "Maximum number of items", "number")] - public int MaxNumber { get; set; } + [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay.")] + public string? OverlaySize { get; set; } - [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay.")] - public string? OverlaySize { get; set; } + [ConfigurationField( + "hideAnchor", + "Hide anchor/query string input", + "boolean", + Description = "Selecting this hides the anchor/query string input field in the linkpicker overlay.")] + public bool HideAnchor { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore user start nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } - - [ConfigurationField("hideAnchor", - "Hide anchor/query string input", "boolean", - Description = "Selecting this hides the anchor/query string input field in the linkpicker overlay.")] - public bool HideAnchor { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore user start nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfigurationEditor.cs index f5baa18c04..f85cafa817 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfigurationEditor.cs @@ -1,26 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class MultiUrlPickerConfigurationEditor : ConfigurationEditor { - public class MultiUrlPickerConfigurationEditor : ConfigurationEditor + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MultiUrlPickerConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MultiUrlPickerConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + } - { - } - - public MultiUrlPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } + public MultiUrlPickerConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs index 506b3bebc9..6c7f93374d 100644 --- a/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs @@ -1,14 +1,12 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for a multiple textstring value editor. +/// +public class MultipleTextStringConfiguration { - /// - /// Represents the configuration for a multiple textstring value editor. - /// - public class MultipleTextStringConfiguration - { - // fields are configured in the editor + // fields are configured in the editor + public int Minimum { get; set; } - public int Minimum { get; set; } - - public int Maximum {get; set; } - } + public int Maximum { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs b/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs index aed6b5cd00..a22eb352c0 100644 --- a/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs @@ -1,43 +1,40 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the nested content value editor. +/// +public class NestedContentConfiguration { + [ConfigurationField("contentTypes", "Element Types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the Element Types to use as models for the items.")] + public ContentType[]? ContentTypes { get; set; } - /// - /// Represents the configuration for the nested content value editor. - /// - public class NestedContentConfiguration + [ConfigurationField("minItems", "Min Items", "number", Description = "Minimum number of items allowed.")] + public int? MinItems { get; set; } + + [ConfigurationField("maxItems", "Max Items", "number", Description = "Maximum number of items allowed.")] + public int? MaxItems { get; set; } + + [ConfigurationField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Requires editor confirmation for delete actions.")] + public bool ConfirmDeletes { get; set; } = true; + + [ConfigurationField("showIcons", "Show Icons", "boolean", Description = "Show the Element Type icons.")] + public bool ShowIcons { get; set; } = true; + + [ConfigurationField("hideLabel", "Hide Label", "boolean", Description = "Hide the property label and let the item list span the full width of the editor window.")] + public bool HideLabel { get; set; } + + [DataContract] + public class ContentType { - [ConfigurationField("contentTypes", "Element Types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the Element Types to use as models for the items.")] - public ContentType[]? ContentTypes { get; set; } + [DataMember(Name = "ncAlias")] + public string? Alias { get; set; } - [ConfigurationField("minItems", "Min Items", "number", Description = "Minimum number of items allowed.")] - public int? MinItems { get; set; } + [DataMember(Name = "ncTabAlias")] + public string? TabAlias { get; set; } - [ConfigurationField("maxItems", "Max Items", "number", Description = "Maximum number of items allowed.")] - public int? MaxItems { get; set; } - - [ConfigurationField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Requires editor confirmation for delete actions.")] - public bool ConfirmDeletes { get; set; } = true; - - [ConfigurationField("showIcons", "Show Icons", "boolean", Description = "Show the Element Type icons.")] - public bool ShowIcons { get; set; } = true; - - [ConfigurationField("hideLabel", "Hide Label", "boolean", Description = "Hide the property label and let the item list span the full width of the editor window.")] - public bool HideLabel { get; set; } - - - [DataContract] - public class ContentType - { - [DataMember(Name = "ncAlias")] - public string? Alias { get; set; } - - [DataMember(Name = "ncTabAlias")] - public string? TabAlias { get; set; } - - [DataMember(Name = "nameTemplate")] - public string? Template { get; set; } - } + [DataMember(Name = "nameTemplate")] + public string? Template { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/NestedContentConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/NestedContentConfigurationEditor.cs index bab2038d2d..5adb06b42f 100644 --- a/src/Umbraco.Core/PropertyEditors/NestedContentConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/NestedContentConfigurationEditor.cs @@ -1,28 +1,27 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the nested content value editor. - /// - public class NestedContentConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public NestedContentConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public NestedContentConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the nested content value editor. +/// +public class NestedContentConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public NestedContentConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public NestedContentConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs index 7e91d8e3ee..f1d295bc3d 100644 --- a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Default implementation for which does not compress any property +/// data +/// +public sealed class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions { - /// - /// Default implementation for which does not compress any property data - /// - public sealed class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions - { - public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) => false; - } + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) => false; } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs index c58c962df4..eec435ddf6 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs @@ -1,25 +1,24 @@ -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class ParameterEditorCollection : BuilderCollectionBase { - public class ParameterEditorCollection : BuilderCollectionBase + public ParameterEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) + : base(() => dataEditors + .Where(x => (x.Type & EditorType.MacroParameter) > 0) + .Union(manifestParser.CombinedManifest.PropertyEditors)) { - public ParameterEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) - : base(() => dataEditors - .Where(x => (x.Type & EditorType.MacroParameter) > 0) - .Union(manifestParser.CombinedManifest.PropertyEditors)) - { } + } - // note: virtual so it can be mocked - public virtual IDataEditor? this[string alias] - => this.SingleOrDefault(x => x.Alias == alias); + // note: virtual so it can be mocked + public virtual IDataEditor? this[string alias] + => this.SingleOrDefault(x => x.Alias == alias); - public virtual bool TryGet(string alias, out IDataEditor? editor) - { - editor = this.FirstOrDefault(x => x.Alias == alias); - return editor != null; - } + public virtual bool TryGet(string alias, out IDataEditor? editor) + { + editor = this.FirstOrDefault(x => x.Alias == alias); + return editor != null; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs index c7d8067fff..25bcc38d7e 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs @@ -1,31 +1,24 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +/// +/// Represents a content type parameter editor. +/// +[DataEditor( + "contentType", + EditorType.MacroParameter, + "Content Type Picker", + "entitypicker")] +public class ContentTypeParameterEditor : DataEditor { /// - /// Represents a content type parameter editor. + /// Initializes a new instance of the class. /// - [DataEditor( - "contentType", - EditorType.MacroParameter, - "Content Type Picker", - "entitypicker")] - public class ContentTypeParameterEditor : DataEditor + public ContentTypeParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - /// - /// Initializes a new instance of the class. - /// - public ContentTypeParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", false); - DefaultConfiguration.Add("entityType", "DocumentType"); - } + // configure + DefaultConfiguration.Add("multiple", false); + DefaultConfiguration.Add("entityType", "DocumentType"); } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs index 65056c75ce..2897a8c4ed 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs @@ -1,44 +1,52 @@ -using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; + +/// +/// Represents a parameter editor of some sort. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.MultiNodeTreePicker, + EditorType.MacroParameter, + "Multiple Content Picker", + "contentpicker")] +public class MultipleContentPickerParameterEditor : DataEditor { /// - /// Represents a parameter editor of some sort. + /// Initializes a new instance of the class. /// - [DataEditor( - Constants.PropertyEditors.Aliases.MultiNodeTreePicker, - EditorType.MacroParameter, - "Multiple Content Picker", - "contentpicker")] - public class MultipleContentPickerParameterEditor : DataEditor + public MultipleContentPickerParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - /// - /// Initializes a new instance of the class. - /// - public MultipleContentPickerParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) + // configure + DefaultConfiguration.Add("multiPicker", "1"); + DefaultConfiguration.Add("minNumber", 0); + DefaultConfiguration.Add("maxNumber", 0); + } + + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + internal class MultipleContentPickerParamateterValueEditor : MultiplePickerParamateterValueEditorBase + { + public MultipleContentPickerParamateterValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute, + IEntityService entityService) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) { - // configure - DefaultConfiguration.Add("multiPicker", "1"); - DefaultConfiguration.Add("minNumber",0 ); - DefaultConfiguration.Add("maxNumber", 0); } - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + public override string UdiEntityType { get; } = Constants.UdiEntityType.Document; - internal class MultipleContentPickerParamateterValueEditor : MultiplePickerParamateterValueEditorBase - { - public MultipleContentPickerParamateterValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) - { - } - - public override string UdiEntityType { get; } = Constants.UdiEntityType.Document; - public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Document; - } + public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Document; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs index 01bae2ada2..44ff5d94c6 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs @@ -1,25 +1,18 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "contentTypeMultiple", + EditorType.MacroParameter, + "Multiple Content Type Picker", + "entitypicker")] +public class MultipleContentTypeParameterEditor : DataEditor { - [DataEditor( - "contentTypeMultiple", - EditorType.MacroParameter, - "Multiple Content Type Picker", - "entitypicker")] - public class MultipleContentTypeParameterEditor : DataEditor + public MultipleContentTypeParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public MultipleContentTypeParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", true); - DefaultConfiguration.Add("entityType", "DocumentType"); - } + // configure + DefaultConfiguration.Add("multiple", true); + DefaultConfiguration.Add("entityType", "DocumentType"); } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs index 4a6bab528c..71f626107b 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs @@ -1,46 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Reflection.Metadata; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; + +/// +/// Represents a multiple media picker macro parameter editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.MultipleMediaPicker, + EditorType.MacroParameter, + "Multiple Media Picker", + "mediapicker", + ValueType = ValueTypes.Text)] +public class MultipleMediaPickerParameterEditor : DataEditor { /// - /// Represents a multiple media picker macro parameter editor. + /// Initializes a new instance of the class. /// - [DataEditor( - Constants.PropertyEditors.Aliases.MultipleMediaPicker, - EditorType.MacroParameter, - "Multiple Media Picker", - "mediapicker", - ValueType = ValueTypes.Text)] - public class MultipleMediaPickerParameterEditor : DataEditor + public MultipleMediaPickerParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) => + DefaultConfiguration.Add("multiPicker", "1"); + + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + internal class MultipleMediaPickerPropertyValueEditor : MultiplePickerParamateterValueEditorBase { - /// - /// Initializes a new instance of the class. - /// - public MultipleMediaPickerParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) + public MultipleMediaPickerPropertyValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute, + IEntityService entityService) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) { - DefaultConfiguration.Add("multiPicker", "1"); } - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + public override string UdiEntityType { get; } = Constants.UdiEntityType.Media; - internal class MultipleMediaPickerPropertyValueEditor : MultiplePickerParamateterValueEditorBase - { - public MultipleMediaPickerPropertyValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) - { - } - - public override string UdiEntityType { get; } = Constants.UdiEntityType.Media; - public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Media; - } + public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Media; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs index 5182c1fbd2..8aaea32ab4 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -7,53 +5,51 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors -{ - internal abstract class MultiplePickerParamateterValueEditorBase : DataValueEditor, IDataValueReference - { - private readonly IEntityService _entityService; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; - public MultiplePickerParamateterValueEditorBase( - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - DataEditorAttribute attribute, - IEntityService entityService) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) +internal abstract class MultiplePickerParamateterValueEditorBase : DataValueEditor, IDataValueReference +{ + private readonly IEntityService _entityService; + + public MultiplePickerParamateterValueEditorBase( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute, + IEntityService entityService) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) => + _entityService = entityService; + + public abstract string UdiEntityType { get; } + + public abstract UmbracoObjectTypes UmbracoObjectType { get; } + + public IEnumerable GetReferences(object? value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) { - _entityService = entityService; + yield break; } - public abstract string UdiEntityType { get; } - public abstract UmbracoObjectTypes UmbracoObjectType { get; } - public IEnumerable GetReferences(object? value) + foreach (var udiStr in asString.Split(',')) { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) + if (UdiParser.TryParse(udiStr, out Udi? udi)) { - yield break; + yield return new UmbracoEntityReference(udi); } - foreach (var udiStr in asString.Split(',')) + // this is needed to support the legacy case when the multiple media picker parameter editor stores ints not udis + if (int.TryParse(udiStr, out var id)) { - if (UdiParser.TryParse(udiStr, out Udi? udi)) + Attempt guidAttempt = _entityService.GetKey(id, UmbracoObjectType); + Guid guid = guidAttempt.Success ? guidAttempt.Result : Guid.Empty; + + if (guid != Guid.Empty) { - yield return new UmbracoEntityReference(udi); - } - - // this is needed to support the legacy case when the multiple media picker parameter editor stores ints not udis - if (int.TryParse(udiStr, out var id)) - { - Attempt guidAttempt = _entityService.GetKey(id, UmbracoObjectType); - Guid guid = guidAttempt.Success ? guidAttempt.Result : Guid.Empty; - - if (guid != Guid.Empty) - { - yield return new UmbracoEntityReference(new GuidUdi(Constants.UdiEntityType.Media, guid)); - } - + yield return new UmbracoEntityReference(new GuidUdi(Constants.UdiEntityType.Media, guid)); } } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs index d39f792971..f9485441b9 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs @@ -1,27 +1,21 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "tabPickerMultiple", + EditorType.MacroParameter, + "Multiple Tab Picker", + "entitypicker")] +public class MultiplePropertyGroupParameterEditor : DataEditor { - [DataEditor( - "tabPickerMultiple", - EditorType.MacroParameter, - "Multiple Tab Picker", - "entitypicker")] - public class MultiplePropertyGroupParameterEditor : DataEditor + public MultiplePropertyGroupParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public MultiplePropertyGroupParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", true); - DefaultConfiguration.Add("entityType", "PropertyGroup"); - //don't publish the id for a property group, publish its alias, which is actually just its lower cased name - DefaultConfiguration.Add("publishBy", "alias"); - } + // configure + DefaultConfiguration.Add("multiple", true); + DefaultConfiguration.Add("entityType", "PropertyGroup"); + + // don't publish the id for a property group, publish its alias, which is actually just its lower cased name + DefaultConfiguration.Add("publishBy", "alias"); } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs index 64e310551b..913c452fb9 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs @@ -1,27 +1,21 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "propertyTypePickerMultiple", + EditorType.MacroParameter, + "Multiple Property Type Picker", + "entitypicker")] +public class MultiplePropertyTypeParameterEditor : DataEditor { - [DataEditor( - "propertyTypePickerMultiple", - EditorType.MacroParameter, - "Multiple Property Type Picker", - "entitypicker")] - public class MultiplePropertyTypeParameterEditor : DataEditor + public MultiplePropertyTypeParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public MultiplePropertyTypeParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", "1"); - DefaultConfiguration.Add("entityType", "PropertyType"); - //don't publish the id for a property type, publish its alias - DefaultConfiguration.Add("publishBy", "alias"); - } + // configure + DefaultConfiguration.Add("multiple", "1"); + DefaultConfiguration.Add("entityType", "PropertyType"); + + // don't publish the id for a property type, publish its alias + DefaultConfiguration.Add("publishBy", "alias"); } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs index 6441e8cb24..345a3e4971 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs @@ -1,27 +1,21 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "tabPicker", + EditorType.MacroParameter, + "Tab Picker", + "entitypicker")] +public class PropertyGroupParameterEditor : DataEditor { - [DataEditor( - "tabPicker", - EditorType.MacroParameter, - "Tab Picker", - "entitypicker")] - public class PropertyGroupParameterEditor : DataEditor + public PropertyGroupParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public PropertyGroupParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", "0"); - DefaultConfiguration.Add("entityType", "PropertyGroup"); - //don't publish the id for a property group, publish it's alias (which is actually just it's lower cased name) - DefaultConfiguration.Add("publishBy", "alias"); - } + // configure + DefaultConfiguration.Add("multiple", "0"); + DefaultConfiguration.Add("entityType", "PropertyGroup"); + + // don't publish the id for a property group, publish it's alias (which is actually just it's lower cased name) + DefaultConfiguration.Add("publishBy", "alias"); } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs index 9e253d4e41..781d072e10 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs @@ -1,27 +1,21 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors; -namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +[DataEditor( + "propertyTypePicker", + EditorType.MacroParameter, + "Property Type Picker", + "entitypicker")] +public class PropertyTypeParameterEditor : DataEditor { - [DataEditor( - "propertyTypePicker", - EditorType.MacroParameter, - "Property Type Picker", - "entitypicker")] - public class PropertyTypeParameterEditor : DataEditor + public PropertyTypeParameterEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public PropertyTypeParameterEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { - // configure - DefaultConfiguration.Add("multiple", "0"); - DefaultConfiguration.Add("entityType", "PropertyType"); - //don't publish the id for a property type, publish its alias - DefaultConfiguration.Add("publishBy", "alias"); - } + // configure + DefaultConfiguration.Add("multiple", "0"); + DefaultConfiguration.Add("entityType", "PropertyType"); + + // don't publish the id for a property type, publish its alias + DefaultConfiguration.Add("publishBy", "alias"); } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs index ac275c46e3..75342371a4 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs @@ -1,53 +1,56 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.PropertyEditors; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +/// +/// Compresses property data based on config +/// +public class PropertyCacheCompression : IPropertyCacheCompression { + private readonly IPropertyCacheCompressionOptions _compressionOptions; + private readonly IReadOnlyDictionary _contentTypes; - /// - /// Compresses property data based on config - /// - public class PropertyCacheCompression : IPropertyCacheCompression + private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias, bool published), bool> + _isCompressedCache; + + private readonly PropertyEditorCollection _propertyEditors; + + public PropertyCacheCompression( + IPropertyCacheCompressionOptions compressionOptions, + IReadOnlyDictionary contentTypes, + PropertyEditorCollection propertyEditors, + ConcurrentDictionary<(int, string, bool), bool> compressedStoragePropertyEditorCache) { - private readonly IPropertyCacheCompressionOptions _compressionOptions; - private readonly IReadOnlyDictionary _contentTypes; - private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias, bool published), bool> _isCompressedCache; + _compressionOptions = compressionOptions; + _contentTypes = contentTypes ?? throw new ArgumentNullException(nameof(contentTypes)); + _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + _isCompressedCache = compressedStoragePropertyEditorCache; + } - public PropertyCacheCompression( - IPropertyCacheCompressionOptions compressionOptions, - IReadOnlyDictionary contentTypes, - PropertyEditorCollection propertyEditors, - ConcurrentDictionary<(int, string, bool), bool> compressedStoragePropertyEditorCache) + public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) + { + var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x => { - _compressionOptions = compressionOptions; - _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); - _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); - _isCompressedCache = compressedStoragePropertyEditorCache; - } - - public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) - { - var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x => + if (!_contentTypes.TryGetValue(x.contentTypeId, out IContentTypeComposition? ct)) { - if (!_contentTypes.TryGetValue(x.contentTypeId, out var ct)) - return false; + return false; + } - var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); - if (propertyType == null) - return false; + IPropertyType? propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); + if (propertyType == null) + { + return false; + } - if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) - return false; + if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out IDataEditor? propertyEditor)) + { + return false; + } - return _compressionOptions.IsCompressed(content, propertyType, propertyEditor!, published); - }); + return _compressionOptions.IsCompressed(content, propertyType, propertyEditor, published); + }); - return compressedStorage; - } + return compressedStorage; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs index 9c94008616..c835c0ae95 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs @@ -1,39 +1,40 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Specifies the level of cache for a property value. +/// +public enum PropertyCacheLevel { /// - /// Specifies the level of cache for a property value. + /// Default value. /// - public enum PropertyCacheLevel - { - /// - /// Default value. - /// - Unknown = 0, + Unknown = 0, - /// - /// Indicates that the property value can be cached at the element level, i.e. it can be - /// cached until the element itself is modified. - /// - Element = 1, + /// + /// Indicates that the property value can be cached at the element level, i.e. it can be + /// cached until the element itself is modified. + /// + Element = 1, - /// - /// Indicates that the property value can be cached at the elements level, i.e. it can - /// be cached until any element is modified. - /// - Elements = 2, + /// + /// Indicates that the property value can be cached at the elements level, i.e. it can + /// be cached until any element is modified. + /// + Elements = 2, - /// - /// Indicates that the property value can be cached at the snapshot level, i.e. it can be - /// cached for the duration of the current snapshot. - /// - /// In most cases, a snapshot is created per request, and therefore this is - /// equivalent to cache the value for the duration of the request. - Snapshot = 3, + /// + /// Indicates that the property value can be cached at the snapshot level, i.e. it can be + /// cached for the duration of the current snapshot. + /// + /// + /// In most cases, a snapshot is created per request, and therefore this is + /// equivalent to cache the value for the duration of the request. + /// + Snapshot = 3, - /// - /// Indicates that the property value cannot be cached and has to be converted each time - /// it is requested. - /// - None = 4 - } + /// + /// Indicates that the property value cannot be cached and has to be converted each time + /// it is requested. + /// + None = 4, } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs index 34f72cf5c0..ff700431d5 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs @@ -1,31 +1,31 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class PropertyEditorCollection : BuilderCollectionBase { - public class PropertyEditorCollection : BuilderCollectionBase + public PropertyEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) + : base(() => dataEditors + .Where(x => (x.Type & EditorType.PropertyValue) > 0) + .Union(manifestParser.CombinedManifest.PropertyEditors)) { - public PropertyEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) - : base(() => dataEditors - .Where(x => (x.Type & EditorType.PropertyValue) > 0) - .Union(manifestParser.CombinedManifest.PropertyEditors)) - { } + } - public PropertyEditorCollection(DataEditorCollection dataEditors) - : base(() => dataEditors - .Where(x => (x.Type & EditorType.PropertyValue) > 0)) - { } + public PropertyEditorCollection(DataEditorCollection dataEditors) + : base(() => dataEditors + .Where(x => (x.Type & EditorType.PropertyValue) > 0)) + { + } - // note: virtual so it can be mocked - public virtual IDataEditor? this[string? alias] - => this.SingleOrDefault(x => x.Alias == alias); + // note: virtual so it can be mocked + public virtual IDataEditor? this[string? alias] + => this.SingleOrDefault(x => x.Alias == alias); - public virtual bool TryGet(string? alias, [MaybeNullWhen(false)] out IDataEditor editor) - { - editor = this.FirstOrDefault(x => x.Alias == alias); - return editor != null; - } + public virtual bool TryGet(string? alias, [MaybeNullWhen(false)] out IDataEditor editor) + { + editor = this.FirstOrDefault(x => x.Alias == alias); + return editor != null; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs index fa57956cdd..ff92c2012f 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs @@ -1,22 +1,21 @@ -using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the interface to manage tags. +/// +public static class PropertyEditorTagsExtensions { /// - /// Provides extension methods for the interface to manage tags. + /// Determines whether an editor supports tags. /// - public static class PropertyEditorTagsExtensions - { - /// - /// Determines whether an editor supports tags. - /// - public static bool IsTagsEditor(this IDataEditor editor) - => editor.GetTagAttribute() != null; + public static bool IsTagsEditor(this IDataEditor editor) + => editor.GetTagAttribute() != null; - /// - /// Gets the tags configuration attribute of an editor. - /// - public static TagsPropertyEditorAttribute? GetTagAttribute(this IDataEditor? editor) - => editor?.GetType().GetCustomAttribute(false); - } + /// + /// Gets the tags configuration attribute of an editor. + /// + public static TagsPropertyEditorAttribute? GetTagAttribute(this IDataEditor? editor) + => editor?.GetType().GetCustomAttribute(false); } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs index 0442ae1b18..d73eb5a2eb 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs @@ -1,61 +1,60 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Provides a default implementation for . +/// +/// +public abstract class PropertyValueConverterBase : IPropertyValueConverter { - /// - /// Provides a default implementation for . - /// - /// - public abstract class PropertyValueConverterBase : IPropertyValueConverter + /// + public virtual bool IsConverter(IPublishedPropertyType propertyType) + => false; + + /// + public virtual bool? IsValue(object? value, PropertyValueLevel level) { - /// - public virtual bool IsConverter(IPublishedPropertyType propertyType) - => false; - - /// - public virtual bool? IsValue(object? value, PropertyValueLevel level) + switch (level) { - switch (level) - { - case PropertyValueLevel.Source: - // the default implementation uses the old magic null & string comparisons, - // other implementations may be more clever, and/or test the final converted object values - return value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)); - case PropertyValueLevel.Inter: - return null; - case PropertyValueLevel.Object: - return null; - default: - throw new NotSupportedException($"Invalid level: {level}."); - } + case PropertyValueLevel.Source: + // the default implementation uses the old magic null & string comparisons, + // other implementations may be more clever, and/or test the final converted object values + return value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)); + case PropertyValueLevel.Inter: + return null; + case PropertyValueLevel.Object: + return null; + default: + throw new NotSupportedException($"Invalid level: {level}."); } + } - [Obsolete("This method is not part of the IPropertyValueConverter contract, therefore not used and will be removed in future versions; use IsValue instead.")] - public virtual bool HasValue(IPublishedProperty property, string culture, string segment) - { - var value = property.GetSourceValue(culture, segment); - return value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)); - } + /// + public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(object); - /// - public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(object); + /// + public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; - /// - public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; + /// + public virtual object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source; - /// - public virtual object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - => source; + /// + public virtual object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + => inter; - /// - public virtual object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - => inter; + /// + public virtual object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + => inter?.ToString() ?? string.Empty; - /// - public virtual object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - => inter?.ToString() ?? string.Empty; + [Obsolete( + "This method is not part of the IPropertyValueConverter contract, therefore not used and will be removed in future versions; use IsValue instead.")] + public virtual bool HasValue(IPublishedProperty property, string culture, string segment) + { + var value = property.GetSourceValue(culture, segment); + return value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)); } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs index 9214f10482..20eb9ae4c4 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs @@ -1,47 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class PropertyValueConverterCollection : BuilderCollectionBase { - public class PropertyValueConverterCollection : BuilderCollectionBase + private readonly object _locker = new(); + private Dictionary? _defaultConverters; + + public PropertyValueConverterCollection(Func> items) + : base(items) { - public PropertyValueConverterCollection(Func> items) : base(items) - { - } + } - private readonly object _locker = new object(); - private Dictionary? _defaultConverters; - - private Dictionary DefaultConverters + private Dictionary DefaultConverters + { + get { - get + lock (_locker) { - lock (_locker) + if (_defaultConverters != null) { - if (_defaultConverters != null) - return _defaultConverters; - - _defaultConverters = new Dictionary(); - - foreach (var converter in this) - { - var attr = converter.GetType().GetCustomAttribute(false); - if (attr != null) - _defaultConverters[converter] = attr.DefaultConvertersToShadow; - } - return _defaultConverters; } + + _defaultConverters = new Dictionary(); + + foreach (IPropertyValueConverter converter in this) + { + DefaultPropertyValueConverterAttribute? attr = converter.GetType().GetCustomAttribute(false); + if (attr != null) + { + _defaultConverters[converter] = attr.DefaultConvertersToShadow; + } + } + + return _defaultConverters; } } - - internal bool IsDefault(IPropertyValueConverter converter) - => DefaultConverters.ContainsKey(converter); - - internal bool Shadows(IPropertyValueConverter shadowing, IPropertyValueConverter shadowed) - => DefaultConverters.TryGetValue(shadowing, out Type[]? types) && types.Contains(shadowed.GetType()); } + + internal bool IsDefault(IPropertyValueConverter converter) + => DefaultConverters.ContainsKey(converter); + + internal bool Shadows(IPropertyValueConverter shadowing, IPropertyValueConverter shadowed) + => DefaultConverters.TryGetValue(shadowing, out Type[]? types) && types.Contains(shadowed.GetType()); } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs index f7bbca2b02..6d1e329c7e 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +public class PropertyValueConverterCollectionBuilder : OrderedCollectionBuilderBase { - public class PropertyValueConverterCollectionBuilder : OrderedCollectionBuilderBase - { - protected override PropertyValueConverterCollectionBuilder This => this; - } + protected override PropertyValueConverterCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs index 583bf87f3e..52389fb92a 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs @@ -1,23 +1,22 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Indicates the level of a value. +/// +public enum PropertyValueLevel { /// - /// Indicates the level of a value. + /// The source value, i.e. what is in the database. /// - public enum PropertyValueLevel - { - /// - /// The source value, i.e. what is in the database. - /// - Source, + Source, - /// - /// The conversion intermediate value. - /// - Inter, + /// + /// The conversion intermediate value. + /// + Inter, - /// - /// The converted value. - /// - Object - } + /// + /// The converted value. + /// + Object, } diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs index e0bbae88b5..6a80144d0d 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs @@ -1,27 +1,27 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the rich text value editor. +/// +public class RichTextConfiguration : IIgnoreUserStartNodesConfig { - /// - /// Represents the configuration for the rich text value editor. - /// - public class RichTextConfiguration : IIgnoreUserStartNodesConfig - { - // TODO: Make these strongly typed, for now this works though - [ConfigurationField("editor", "Editor", "views/propertyeditors/rte/rte.prevalues.html", HideLabel = true)] - public object? Editor { get; set; } + // TODO: Make these strongly typed, for now this works though + [ConfigurationField("editor", "Editor", "views/propertyeditors/rte/rte.prevalues.html", HideLabel = true)] + public object? Editor { get; set; } - [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] - public string? OverlaySize { get; set; } + [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] + public string? OverlaySize { get; set; } - [ConfigurationField("hideLabel", "Hide Label", "boolean")] - public bool HideLabel { get; set; } + [ConfigurationField("hideLabel", "Hide Label", "boolean")] + public bool HideLabel { get; set; } - [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, - "Ignore User Start Nodes", "boolean", - Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] - public bool IgnoreUserStartNodes { get; set; } + [ConfigurationField("mediaParentId", "Image Upload Folder", "mediafolderpicker", Description = "Choose the upload location of pasted images")] + public GuidUdi? MediaParentId { get; set; } - [ConfigurationField("mediaParentId", "Image Upload Folder", "mediafolderpicker", - Description = "Choose the upload location of pasted images")] - public GuidUdi? MediaParentId { get; set; } - } + [ConfigurationField( + Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", + "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfigurationEditor.cs index a967ec2367..4e0b5b557d 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfigurationEditor.cs @@ -1,28 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the rich text value editor. - /// - public class RichTextConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public RichTextConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public RichTextConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the rich text value editor. +/// +public class RichTextConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public RichTextConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public RichTextConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs index 8d41873a11..709fb3ce9f 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs @@ -1,26 +1,25 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the slider value editor. +/// +public class SliderConfiguration { - /// - /// Represents the configuration for the slider value editor. - /// - public class SliderConfiguration - { - [ConfigurationField("enableRange", "Enable range", "boolean")] - public bool EnableRange { get; set; } + [ConfigurationField("enableRange", "Enable range", "boolean")] + public bool EnableRange { get; set; } - [ConfigurationField("initVal1", "Initial value", "number")] - public decimal InitialValue { get; set; } + [ConfigurationField("initVal1", "Initial value", "number")] + public decimal InitialValue { get; set; } - [ConfigurationField("initVal2", "Initial value 2", "number", Description = "Used when range is enabled")] - public decimal InitialValue2 { get; set; } + [ConfigurationField("initVal2", "Initial value 2", "number", Description = "Used when range is enabled")] + public decimal InitialValue2 { get; set; } - [ConfigurationField("minVal", "Minimum value", "number")] - public decimal MinimumValue { get; set; } + [ConfigurationField("minVal", "Minimum value", "number")] + public decimal MinimumValue { get; set; } - [ConfigurationField("maxVal", "Maximum value", "number")] - public decimal MaximumValue { get; set; } + [ConfigurationField("maxVal", "Maximum value", "number")] + public decimal MaximumValue { get; set; } - [ConfigurationField("step", "Step increments", "number")] - public decimal StepIncrements { get; set; } - } + [ConfigurationField("step", "Step increments", "number")] + public decimal StepIncrements { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/SliderConfigurationEditor.cs index 6cd9db8399..586e4cd3af 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfigurationEditor.cs @@ -1,28 +1,28 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the slider value editor. - /// - public class SliderConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public SliderConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public SliderConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the slider value editor. +/// +public class SliderConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public SliderConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public SliderConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base( + ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs index 61fa80472d..5a9808f227 100644 --- a/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs @@ -1,21 +1,22 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the tag value editor. +/// +public class TagConfiguration { - /// - /// Represents the configuration for the tag value editor. - /// - public class TagConfiguration - { - [ConfigurationField("group", "Tag group", "requiredfield", - Description = "Define a tag group")] - public string Group { get; set; } = "default"; + [ConfigurationField("group", "Tag group", "requiredfield", Description = "Define a tag group")] + public string Group { get; set; } = "default"; - [ConfigurationField("storageType", "Storage Type", "views/propertyeditors/tags/tags.prevalues.html", - Description = "Select whether to store the tags in cache as JSON (default) or as CSV. The only benefits of storage as JSON is that you are able to have commas in a tag value")] - public TagsStorageType StorageType { get; set; } = TagsStorageType.Json; + [ConfigurationField( + "storageType", + "Storage Type", + "views/propertyeditors/tags/tags.prevalues.html", + Description = "Select whether to store the tags in cache as JSON (default) or as CSV. The only benefits of storage as JSON is that you are able to have commas in a tag value")] + public TagsStorageType StorageType { get; set; } = TagsStorageType.Json; - // not a field - public char Delimiter { get; set; } - } + // not a field + public char Delimiter { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs index 2f77642e5f..f22f9b74c4 100644 --- a/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs @@ -1,8 +1,6 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -10,51 +8,55 @@ using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration editor for the tag value editor. +/// +public class TagConfigurationEditor : ConfigurationEditor { - /// - /// Represents the configuration editor for the tag value editor. - /// - public class TagConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public TagConfigurationEditor(ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService) + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public TagConfigurationEditor(ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService) : this(validators, ioHelper, localizedTextService, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public TagConfigurationEditor(ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { + Field(nameof(TagConfiguration.Group)).Validators.Add(new RequiredValidator(localizedTextService)); + Field(nameof(TagConfiguration.StorageType)).Validators.Add(new RequiredValidator(localizedTextService)); + } + + public override Dictionary ToConfigurationEditor(TagConfiguration? configuration) + { + Dictionary dictionary = base.ToConfigurationEditor(configuration); + + // the front-end editor expects the string value of the storage type + if (!dictionary.TryGetValue("storageType", out var storageType)) { + storageType = TagsStorageType.Json; // default to Json } - public TagConfigurationEditor(ManifestValueValidatorCollection validators, IIOHelper ioHelper, ILocalizedTextService localizedTextService, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) + dictionary["storageType"] = storageType.ToString()!; + + return dictionary; + } + + public override TagConfiguration? FromConfigurationEditor( + IDictionary? editorValues, + TagConfiguration? configuration) + { + // the front-end editor returns the string value of the storage type + // pure Json could do with + // [JsonConverter(typeof(StringEnumConverter))] + // but here we're only deserializing to object and it's too late + if (editorValues is not null) { - Field(nameof(TagConfiguration.Group)).Validators.Add(new RequiredValidator(localizedTextService)); - Field(nameof(TagConfiguration.StorageType)).Validators.Add(new RequiredValidator(localizedTextService)); + editorValues["storageType"] = Enum.Parse(typeof(TagsStorageType), (string)editorValues["storageType"]!); } - public override Dictionary ToConfigurationEditor(TagConfiguration? configuration) - { - var dictionary = base.ToConfigurationEditor(configuration); - - // the front-end editor expects the string value of the storage type - if (!dictionary.TryGetValue("storageType", out var storageType)) - storageType = TagsStorageType.Json; //default to Json - dictionary["storageType"] = storageType.ToString()!; - - return dictionary; - } - - public override TagConfiguration? FromConfigurationEditor(IDictionary? editorValues, TagConfiguration? configuration) - { - // the front-end editor returns the string value of the storage type - // pure Json could do with - // [JsonConverter(typeof(StringEnumConverter))] - // but here we're only deserializing to object and it's too late - - if (editorValues is not null) - { - editorValues["storageType"] = Enum.Parse(typeof(TagsStorageType), (string) editorValues["storageType"]!); - } - - return base.FromConfigurationEditor(editorValues, configuration); - } + return base.FromConfigurationEditor(editorValues, configuration); } } diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index c21ea09ac9..849d6446a9 100644 --- a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs @@ -1,61 +1,58 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Marks property editors that support tags. +/// +[AttributeUsage(AttributeTargets.Class)] +public class TagsPropertyEditorAttribute : Attribute { /// - /// Marks property editors that support tags. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Class)] - public class TagsPropertyEditorAttribute : Attribute + public TagsPropertyEditorAttribute(Type tagsConfigurationProvider) + : this() => + TagsConfigurationProviderType = tagsConfigurationProvider ?? + throw new ArgumentNullException(nameof(tagsConfigurationProvider)); + + /// + /// Initializes a new instance of the class. + /// + public TagsPropertyEditorAttribute() { - /// - /// Initializes a new instance of the class. - /// - public TagsPropertyEditorAttribute(Type tagsConfigurationProvider) - : this() - { - TagsConfigurationProviderType = tagsConfigurationProvider ?? throw new ArgumentNullException(nameof(tagsConfigurationProvider)); - } - - /// - /// Initializes a new instance of the class. - /// - public TagsPropertyEditorAttribute() - { - Delimiter = ','; - ReplaceTags = true; - TagGroup = "default"; - StorageType = TagsStorageType.Json; - } - - /// - /// Gets or sets a value indicating how tags are stored. - /// - public TagsStorageType StorageType { get; set; } - - /// - /// Gets or sets the delimited for delimited strings. - /// - /// Default is a comma. Has no meaning when tags are stored as Json. - public char Delimiter { get; set; } - - /// - /// Gets or sets a value indicating whether to replace the tags entirely. - /// - // TODO: what's the usage? - public bool ReplaceTags { get; set; } - - /// - /// Gets or sets the tags group. - /// - /// Default is "default". - public string TagGroup { get; set; } - - /// - /// Gets the type of the dynamic configuration provider. - /// - //TODO: This is not used and should be implemented in a nicer way, see https://github.com/umbraco/Umbraco-CMS/issues/6017#issuecomment-516253562 - public Type? TagsConfigurationProviderType { get; } + Delimiter = ','; + ReplaceTags = true; + TagGroup = "default"; + StorageType = TagsStorageType.Json; } + + /// + /// Gets or sets a value indicating how tags are stored. + /// + public TagsStorageType StorageType { get; set; } + + /// + /// Gets or sets the delimited for delimited strings. + /// + /// Default is a comma. Has no meaning when tags are stored as Json. + public char Delimiter { get; set; } + + /// + /// Gets or sets a value indicating whether to replace the tags entirely. + /// + // TODO: what's the usage? + public bool ReplaceTags { get; set; } + + /// + /// Gets or sets the tags group. + /// + /// Default is "default". + public string TagGroup { get; set; } + + /// + /// Gets the type of the dynamic configuration provider. + /// + // TODO: This is not used and should be implemented in a nicer way, see https://github.com/umbraco/Umbraco-CMS/issues/6017#issuecomment-516253562 + public Type? TagsConfigurationProviderType { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs index 86ca35ef64..8e6355258b 100644 --- a/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TextAreaConfiguration.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration for the textarea value editor. - /// - public class TextAreaConfiguration - { - [ConfigurationField("maxChars", "Maximum allowed characters", "number", Description = "If empty - no character limit")] - public int? MaxChars { get; set; } +namespace Umbraco.Cms.Core.PropertyEditors; - [ConfigurationField("rows", "Number of rows", "number", Description = "If empty - 10 rows would be set as the default value")] - public int? Rows { get; set; } - } +/// +/// Represents the configuration for the textarea value editor. +/// +public class TextAreaConfiguration +{ + [ConfigurationField("maxChars", "Maximum allowed characters", "number", Description = "If empty - no character limit")] + public int? MaxChars { get; set; } + + [ConfigurationField("rows", "Number of rows", "number", Description = "If empty - 10 rows would be set as the default value")] + public int? Rows { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TextAreaConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TextAreaConfigurationEditor.cs index 4fa4e7908c..7ae52825fb 100644 --- a/src/Umbraco.Core/PropertyEditors/TextAreaConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TextAreaConfigurationEditor.cs @@ -1,28 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the textarea value editor. - /// - public class TextAreaConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public TextAreaConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public TextAreaConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the textarea value editor. +/// +public class TextAreaConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public TextAreaConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public TextAreaConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/TextOnlyValueEditor.cs b/src/Umbraco.Core/PropertyEditors/TextOnlyValueEditor.cs index cb401cf92a..6a0995dccd 100644 --- a/src/Umbraco.Core/PropertyEditors/TextOnlyValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TextOnlyValueEditor.cs @@ -1,56 +1,58 @@ -using System; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Custom value editor which ensures that the value stored is just plain text and that +/// no magic json formatting occurs when translating it to and from the database values +/// +public class TextOnlyValueEditor : DataValueEditor { - /// - /// Custom value editor which ensures that the value stored is just plain text and that - /// no magic json formatting occurs when translating it to and from the database values - /// - public class TextOnlyValueEditor : DataValueEditor + public TextOnlyValueEditor( + DataEditorAttribute attribute, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - public TextOnlyValueEditor( - DataEditorAttribute attribute, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) - { } + } - /// - /// A method used to format the database value to a value that can be used by the editor - /// - /// - /// - /// - /// - /// - /// The object returned will always be a string and if the database type is not a valid string type an exception is thrown - /// - public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + /// + /// A method used to format the database value to a value that can be used by the editor + /// + /// + /// + /// + /// + /// + /// The object returned will always be a string and if the database type is not a valid string type an exception is + /// thrown + /// + public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + { + var val = property.GetValue(culture, segment); + + if (val == null) { - var val = property.GetValue(culture, segment); - - if (val == null) return string.Empty; - - switch (ValueTypes.ToStorageType(ValueType)) - { - case ValueStorageType.Ntext: - case ValueStorageType.Nvarchar: - return val.ToString() ?? string.Empty; - case ValueStorageType.Integer: - case ValueStorageType.Decimal: - case ValueStorageType.Date: - default: - throw new InvalidOperationException("The " + typeof(TextOnlyValueEditor) + " can only be used with string based property editors"); - } + return string.Empty; } + switch (ValueTypes.ToStorageType(ValueType)) + { + case ValueStorageType.Ntext: + case ValueStorageType.Nvarchar: + return val.ToString() ?? string.Empty; + case ValueStorageType.Integer: + case ValueStorageType.Decimal: + case ValueStorageType.Date: + default: + throw new InvalidOperationException("The " + typeof(TextOnlyValueEditor) + + " can only be used with string based property editors"); + } } } diff --git a/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs index 60f4169ce6..74de3fea8e 100644 --- a/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs @@ -1,58 +1,57 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Templates; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +[DefaultPropertyValueConverter] +public class TextStringValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class TextStringValueConverter : PropertyValueConverterBase + private static readonly string[] PropertyTypeAliases = { - public TextStringValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser) - { - _linkParser = linkParser; - _urlParser = urlParser; - } + Constants.PropertyEditors.Aliases.TextBox, Constants.PropertyEditors.Aliases.TextArea, + }; - private static readonly string[] PropertyTypeAliases = - { - Constants.PropertyEditors.Aliases.TextBox, - Constants.PropertyEditors.Aliases.TextArea - }; - private readonly HtmlLocalLinkParser _linkParser; - private readonly HtmlUrlParser _urlParser; + private readonly HtmlLocalLinkParser _linkParser; + private readonly HtmlUrlParser _urlParser; - public override bool IsConverter(IPublishedPropertyType propertyType) - => PropertyTypeAliases.Contains(propertyType.EditorAlias); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return null; - var sourceString = source.ToString(); - - // ensures string is parsed for {localLink} and URLs are resolved correctly - sourceString = _linkParser.EnsureInternalLinks(sourceString!, preview); - sourceString = _urlParser.EnsureUrls(sourceString); - - return sourceString; - } - - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - { - // source should come from ConvertSource and be a string (or null) already - return inter ?? string.Empty; - } - - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - { - // source should come from ConvertSource and be a string (or null) already - return inter; - } + public TextStringValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser) + { + _linkParser = linkParser; + _urlParser = urlParser; } + + public override bool IsConverter(IPublishedPropertyType propertyType) + => PropertyTypeAliases.Contains(propertyType.EditorAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) + { + return null; + } + + var sourceString = source.ToString(); + + // ensures string is parsed for {localLink} and URLs are resolved correctly + sourceString = _linkParser.EnsureInternalLinks(sourceString!, preview); + sourceString = _urlParser.EnsureUrls(sourceString); + + return sourceString; + } + + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a string (or null) already + inter ?? string.Empty; + + public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a string (or null) already + inter; } diff --git a/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs index fb56567bc5..26262f3589 100644 --- a/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the textbox value editor. +/// +public class TextboxConfiguration { - /// - /// Represents the configuration for the textbox value editor. - /// - public class TextboxConfiguration - { - [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 512 character limit")] - public int? MaxChars { get; set; } - } + [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 512 character limit")] + public int? MaxChars { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TextboxConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TextboxConfigurationEditor.cs index 81ea1f07b8..69d39a44ab 100644 --- a/src/Umbraco.Core/PropertyEditors/TextboxConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TextboxConfigurationEditor.cs @@ -1,28 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the textbox value editor. - /// - public class TextboxConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public TextboxConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public TextboxConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the textbox value editor. +/// +public class TextboxConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public TextboxConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public TextboxConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs index 945e10fd17..604f4d3c30 100644 --- a/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the configuration for the boolean value editor. +/// +public class TrueFalseConfiguration { - /// - /// Represents the configuration for the boolean value editor. - /// - public class TrueFalseConfiguration - { - [ConfigurationField("default", "Initial State", "boolean", Description = "The initial state for the toggle, when it is displayed for the first time in the backoffice, eg. for a new content item.")] - public bool Default { get; set; } + [ConfigurationField("default", "Initial State", "boolean", Description = "The initial state for the toggle, when it is displayed for the first time in the backoffice, eg. for a new content item.")] + public bool Default { get; set; } - [ConfigurationField("showLabels", "Show toggle labels", "boolean", Description = "Show labels next to toggle button.")] - public bool ShowLabels { get; set; } + [ConfigurationField("showLabels", "Show toggle labels", "boolean", Description = "Show labels next to toggle button.")] + public bool ShowLabels { get; set; } - [ConfigurationField("labelOn", "Label On", "textstring", Description = "Label text when enabled.")] - public string? LabelOn { get; set; } + [ConfigurationField("labelOn", "Label On", "textstring", Description = "Label text when enabled.")] + public string? LabelOn { get; set; } - [ConfigurationField("labelOff", "Label Off", "textstring", Description = "Label text when disabled.")] - public string? LabelOff { get; set; } - } + [ConfigurationField("labelOff", "Label Off", "textstring", Description = "Label text when disabled.")] + public string? LabelOff { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs index d5210edc87..72578f7c5e 100644 --- a/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs @@ -1,28 +1,27 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors -{ - /// - /// Represents the configuration editor for the boolean value editor. - /// - public class TrueFalseConfigurationEditor : ConfigurationEditor - { - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public TrueFalseConfigurationEditor(IIOHelper ioHelper) - : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } +namespace Umbraco.Cms.Core.PropertyEditors; - public TrueFalseConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } +/// +/// Represents the configuration editor for the boolean value editor. +/// +public class TrueFalseConfigurationEditor : ConfigurationEditor +{ + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public TrueFalseConfigurationEditor(IIOHelper ioHelper) + : this(ioHelper, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public TrueFalseConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } diff --git a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs index d8bade11e1..4e5fc41d49 100644 --- a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs @@ -1,25 +1,27 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Compress large, non published text properties +/// +public class UnPublishedContentPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions { - /// - /// Compress large, non published text properties - /// - public class UnPublishedContentPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) { - public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) + if (!published && propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) { - if (!published && propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) - { - //Only compress non published content that supports publishing and the property is text - return true; - } - if (propertyType.ValueStorageType == ValueStorageType.Integer && Constants.PropertyEditors.Aliases.Boolean.Equals(dataEditor.Alias)) - { - //Compress boolean values from int to bool - return true; - } - return false; + // Only compress non published content that supports publishing and the property is text + return true; } + + if (propertyType.ValueStorageType == ValueStorageType.Integer && + Constants.PropertyEditors.Aliases.Boolean.Equals(dataEditor.Alias)) + { + // Compress boolean values from int to bool + return true; + } + + return false; } } diff --git a/src/Umbraco.Core/PropertyEditors/UserPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/UserPickerConfiguration.cs index 3e2a48ffd6..9dce63bf12 100644 --- a/src/Umbraco.Core/PropertyEditors/UserPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/UserPickerConfiguration.cs @@ -1,13 +1,9 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +public class UserPickerConfiguration : ConfigurationEditor { - public class UserPickerConfiguration : ConfigurationEditor + public override IDictionary DefaultConfiguration => new Dictionary { - public override IDictionary DefaultConfiguration => new Dictionary - { - { "entityType", "User" }, - { "multiPicker", "0" } - }; - } + { "entityType", "User" }, { "multiPicker", "0" }, + }; } diff --git a/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs index 17dd8060f5..269178c0b0 100644 --- a/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs @@ -1,25 +1,19 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; +namespace Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors +[DataEditor( + Constants.PropertyEditors.Aliases.UserPicker, + "User Picker", + "userpicker", + ValueType = ValueTypes.Integer, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.User)] +public class UserPickerPropertyEditor : DataEditor { - [DataEditor( - Constants.PropertyEditors.Aliases.UserPicker, - "User Picker", - "userpicker", - ValueType = ValueTypes.Integer, - Group = Constants.PropertyEditors.Groups.People, - Icon = Constants.Icons.User)] - public class UserPickerPropertyEditor : DataEditor + public UserPickerPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - public UserPickerPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) - { } - - protected override IConfigurationEditor CreateConfigurationEditor() => new UserPickerConfiguration(); } + + protected override IConfigurationEditor CreateConfigurationEditor() => new UserPickerConfiguration(); } diff --git a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs index ac7eb9ff61..1332b0b03c 100644 --- a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs +++ b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs @@ -1,41 +1,40 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors.Validation +namespace Umbraco.Cms.Core.PropertyEditors.Validation; + +/// +/// A collection of for an element type within complex editor +/// represented by an Element Type +/// +/// +/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: +/// https://github.com/umbraco/Umbraco-CMS/pull/8339 +/// +public class ComplexEditorElementTypeValidationResult : ValidationResult { + public ComplexEditorElementTypeValidationResult(string elementTypeAlias, Guid blockId) + : base(string.Empty) + { + ElementTypeAlias = elementTypeAlias; + BlockId = blockId; + } + + public IList ValidationResults { get; } = + new List(); + /// - /// A collection of for an element type within complex editor represented by an Element Type + /// The element type alias of the validation result /// /// - /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: - /// https://github.com/umbraco/Umbraco-CMS/pull/8339 + /// This is useful for debugging purposes but it's not actively used in the angular app /// - public class ComplexEditorElementTypeValidationResult : ValidationResult - { - public ComplexEditorElementTypeValidationResult(string elementTypeAlias, Guid blockId) - : base(string.Empty) - { - ElementTypeAlias = elementTypeAlias; - BlockId = blockId; - } + public string ElementTypeAlias { get; } - public IList ValidationResults { get; } = new List(); - - /// - /// The element type alias of the validation result - /// - /// - /// This is useful for debugging purposes but it's not actively used in the angular app - /// - public string ElementTypeAlias { get; } - - /// - /// The Block ID of the validation result - /// - /// - /// This is the GUID id of the content item based on the element type - /// - public Guid BlockId { get; } - } + /// + /// The Block ID of the validation result + /// + /// + /// This is the GUID id of the content item based on the element type + /// + public Guid BlockId { get; } } diff --git a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs index 449ef432d8..06749c765a 100644 --- a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs +++ b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs @@ -1,36 +1,35 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -namespace Umbraco.Cms.Core.PropertyEditors.Validation +namespace Umbraco.Cms.Core.PropertyEditors.Validation; + +/// +/// A collection of for a property type within a complex editor represented by an +/// Element Type +/// +/// +/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: +/// https://github.com/umbraco/Umbraco-CMS/pull/8339 +/// +public class ComplexEditorPropertyTypeValidationResult : ValidationResult { - /// - /// A collection of for a property type within a complex editor represented by an Element Type - /// - /// - /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: - /// https://github.com/umbraco/Umbraco-CMS/pull/8339 - /// - public class ComplexEditorPropertyTypeValidationResult : ValidationResult + private readonly List _validationResults = new(); + + public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias) + : base(string.Empty) => + PropertyTypeAlias = propertyTypeAlias; + + public IReadOnlyList ValidationResults => _validationResults; + + public string PropertyTypeAlias { get; } + + public void AddValidationResult(ValidationResult validationResult) { - public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias) - : base(string.Empty) + if (validationResult is ComplexEditorValidationResult && + _validationResults.Any(x => x is ComplexEditorValidationResult)) { - PropertyTypeAlias = propertyTypeAlias; + throw new InvalidOperationException($"Cannot add more than one {typeof(ComplexEditorValidationResult)}"); } - private readonly List _validationResults = new List(); - - public void AddValidationResult(ValidationResult validationResult) - { - if (validationResult is ComplexEditorValidationResult && _validationResults.Any(x => x is ComplexEditorValidationResult)) - throw new InvalidOperationException($"Cannot add more than one {typeof(ComplexEditorValidationResult)}"); - - _validationResults.Add(validationResult); - } - - public IReadOnlyList ValidationResults => _validationResults; - public string PropertyTypeAlias { get; } + _validationResults.Add(validationResult); } } diff --git a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorValidationResult.cs b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorValidationResult.cs index 225963f461..6ea03ae60f 100644 --- a/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorValidationResult.cs +++ b/src/Umbraco.Core/PropertyEditors/Validation/ComplexEditorValidationResult.cs @@ -1,25 +1,24 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors.Validation +namespace Umbraco.Cms.Core.PropertyEditors.Validation; + +/// +/// A collection of for a complex editor represented by an +/// Element Type +/// +/// +/// For example, each represents validation results for a row in Nested +/// Content. +/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: +/// https://github.com/umbraco/Umbraco-CMS/pull/8339 +/// +public class ComplexEditorValidationResult : ValidationResult { - - /// - /// A collection of for a complex editor represented by an Element Type - /// - /// - /// For example, each represents validation results for a row in Nested Content. - /// - /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: - /// https://github.com/umbraco/Umbraco-CMS/pull/8339 - /// - public class ComplexEditorValidationResult : ValidationResult + public ComplexEditorValidationResult() + : base(string.Empty) { - public ComplexEditorValidationResult() - : base(string.Empty) - { - } - - public IList ValidationResults { get; } = new List(); } + + public IList ValidationResults { get; } = + new List(); } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs index 7c15a418d8..530935d276 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs @@ -1,34 +1,31 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators -{ - /// - /// Used to validate if the value is a valid date/time - /// - public class DateTimeValidator : IValueValidator - { - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) - { - //don't validate if empty - if (value == null || value.ToString().IsNullOrWhiteSpace()) - { - yield break; - } +namespace Umbraco.Cms.Core.PropertyEditors.Validators; - DateTime dt; - if (DateTime.TryParse(value.ToString(), out dt) == false) - { - yield return new ValidationResult(string.Format("The string value {0} cannot be parsed into a DateTime", value), - new[] - { - //we only store a single value for this editor so the 'member' or 'field' - // we'll associate this error with will simply be called 'value' - "value" - }); - } +/// +/// Used to validate if the value is a valid date/time +/// +public class DateTimeValidator : IValueValidator +{ + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + // don't validate if empty + if (value == null || value.ToString().IsNullOrWhiteSpace()) + { + yield break; + } + + if (DateTime.TryParse(value.ToString(), out DateTime dt) == false) + { + yield return new ValidationResult( + string.Format("The string value {0} cannot be parsed into a DateTime", value), + new[] + { + // we only store a single value for this editor so the 'member' or 'field' + // we'll associate this error with will simply be called 'value' + "value", + }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs index 1fb2486e45..cc00b4614e 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs @@ -1,26 +1,28 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates that the value is a valid decimal +/// +public sealed class DecimalValidator : IManifestValueValidator { - /// - /// A validator that validates that the value is a valid decimal - /// - public sealed class DecimalValidator : IManifestValueValidator + /// + public string ValidationName => "Decimal"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { - /// - public string ValidationName => "Decimal"; - - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + if (value == null || value.ToString() == string.Empty) { - if (value == null || value.ToString() == string.Empty) - yield break; + yield break; + } - var result = value.TryConvertTo(); - if (result.Success == false) - yield return new ValidationResult("The value " + value + " is not a valid decimal", new[] { "value" }); + Attempt result = value.TryConvertTo(); + if (result.Success == false) + { + yield return new ValidationResult("The value " + value + " is not a valid decimal", new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DelimitedValueValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DelimitedValueValidator.cs index 8e93e5189e..73907a4266 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DelimitedValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DelimitedValueValidator.cs @@ -1,59 +1,58 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates a delimited set of values against a common regex +/// +public sealed class DelimitedValueValidator : IManifestValueValidator { /// - /// A validator that validates a delimited set of values against a common regex + /// Gets or sets the configuration, when parsed as . /// - public sealed class DelimitedValueValidator : IManifestValueValidator + public DelimitedValueValidatorConfig? Configuration { get; set; } + + /// + public string ValidationName => "Delimited"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { - /// - public string ValidationName => "Delimited"; - - /// - /// Gets or sets the configuration, when parsed as . - /// - public DelimitedValueValidatorConfig? Configuration { get; set; } - - - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + // TODO: localize these! + if (value != null) { - // TODO: localize these! - if (value != null) - { - var delimiter = Configuration?.Delimiter ?? ","; - var regex = (Configuration?.Pattern != null) ? new Regex(Configuration.Pattern) : null; + var delimiter = Configuration?.Delimiter ?? ","; + Regex? regex = Configuration?.Pattern != null ? new Regex(Configuration.Pattern) : null; - var stringVal = value.ToString(); - var split = stringVal!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - for (var i = 0; i < split.Length; i++) + var stringVal = value.ToString(); + var split = stringVal!.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); + for (var i = 0; i < split.Length; i++) + { + var s = split[i]; + + // next if we have a regex statement validate with that + if (regex != null) { - var s = split[i]; - //next if we have a regex statement validate with that - if (regex != null) + if (regex.IsMatch(s) == false) { - if (regex.IsMatch(s) == false) - { - yield return new ValidationResult("The item at index " + i + " did not match the expression " + regex, - new[] - { - //make the field name called 'value0' where 0 is the index - "value" + i - }); - } + yield return new ValidationResult( + "The item at index " + i + " did not match the expression " + regex, + new[] + { + // make the field name called 'value0' where 0 is the index + "value" + i, + }); } } } } } - - public class DelimitedValueValidatorConfig - { - public string? Delimiter { get; set; } - public string? Pattern { get; set; } - } +} + +public class DelimitedValueValidatorConfig +{ + public string? Delimiter { get; set; } + + public string? Pattern { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs index 0db537ede5..8b984dc533 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs @@ -1,28 +1,26 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates an email address +/// +public sealed class EmailValidator : IManifestValueValidator { - /// - /// A validator that validates an email address - /// - public sealed class EmailValidator : IManifestValueValidator + /// + public string ValidationName => "Email"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) { - /// - public string ValidationName => "Email"; + var asString = value == null ? string.Empty : value.ToString(); - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + var emailVal = new EmailAddressAttribute(); + + if (asString != string.Empty && emailVal.IsValid(asString) == false) { - var asString = value == null ? "" : value.ToString(); - - var emailVal = new EmailAddressAttribute(); - - if (asString != string.Empty && emailVal.IsValid(asString) == false) - { - // TODO: localize these! - yield return new ValidationResult("Email is invalid", new[] { "value" }); - } + // TODO: localize these! + yield return new ValidationResult("Email is invalid", new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs index 351d0de82d..2123d213f6 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs @@ -1,27 +1,25 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators -{ - /// - /// A validator that validates that the value is a valid integer - /// - public sealed class IntegerValidator : IManifestValueValidator - { - /// - public string ValidationName => "Integer"; +namespace Umbraco.Cms.Core.PropertyEditors.Validators; - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) +/// +/// A validator that validates that the value is a valid integer +/// +public sealed class IntegerValidator : IManifestValueValidator +{ + /// + public string ValidationName => "Integer"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + if (value != null && value.ToString() != string.Empty) { - if (value != null && value.ToString() != string.Empty) + Attempt result = value.TryConvertTo(); + if (result.Success == false) { - var result = value.TryConvertTo(); - if (result.Success == false) - { - yield return new ValidationResult("The value " + value + " is not a valid integer", new[] { "value" }); - } + yield return new ValidationResult("The value " + value + " is not a valid integer", new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs index ead85c30e4..5a9032303c 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs @@ -1,84 +1,107 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates that the value against a regular expression. +/// +public sealed class RegexValidator : IValueFormatValidator, IManifestValueValidator { + private const string ValueIsInvalid = "Value is invalid, it does not match the correct pattern"; + private readonly ILocalizedTextService _textService; + private string _regex; + /// - /// A validator that validates that the value against a regular expression. + /// Initializes a new instance of the class. /// - public sealed class RegexValidator : IValueFormatValidator, IManifestValueValidator + /// + /// Use this constructor when the validator is used as an , + /// and the regular expression is supplied at validation time. This constructor is also used when + /// the validator is used as an and the regular expression + /// is supplied via the method. + /// + public RegexValidator(ILocalizedTextService textService) + : this(textService, string.Empty) { - private readonly ILocalizedTextService _textService; - private string _regex; + } - const string ValueIsInvalid = "Value is invalid, it does not match the correct pattern"; + /// + /// Initializes a new instance of the class. + /// + /// + /// Use this constructor when the validator is used as an , + /// and the regular expression must be supplied when the validator is created. + /// + public RegexValidator(ILocalizedTextService textService, string regex) + { + _textService = textService; + _regex = regex; + } - /// - public string ValidationName => "Regex"; - - /// - /// Initializes a new instance of the class. - /// - /// Use this constructor when the validator is used as an , - /// and the regular expression is supplied at validation time. This constructor is also used when - /// the validator is used as an and the regular expression - /// is supplied via the method. - public RegexValidator(ILocalizedTextService textService) : this(textService, string.Empty) - { } - - /// - /// Initializes a new instance of the class. - /// - /// Use this constructor when the validator is used as an , - /// and the regular expression must be supplied when the validator is created. - public RegexValidator(ILocalizedTextService textService, string regex) + /// + /// Gets or sets the configuration, when parsed as . + /// + public string Configuration + { + get => _regex; + set { - _textService = textService; - _regex = regex; - } - - /// - /// Gets or sets the configuration, when parsed as . - /// - public string Configuration - { - get => _regex; - set + if (value == null) { - if (value == null) throw new ArgumentNullException(nameof(value)); - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(value)); - - _regex = value; - } - } - - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) - { - if (_regex == null) - { - throw new InvalidOperationException("The validator has not been configured."); + throw new ArgumentNullException(nameof(value)); } - return ValidateFormat(value, valueType, _regex); + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(value)); + } + + _regex = value; + } + } + + /// + public string ValidationName => "Regex"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + if (_regex == null) + { + throw new InvalidOperationException("The validator has not been configured."); } - /// - public IEnumerable ValidateFormat(object? value, string? valueType, string format) + return ValidateFormat(value, valueType, _regex); + } + + /// + public IEnumerable ValidateFormat(object? value, string? valueType, string format) + { + if (format == null) { - if (format == null) throw new ArgumentNullException(nameof(format)); - if (string.IsNullOrWhiteSpace(format)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(format)); - if (value == null || !new Regex(format).IsMatch(value.ToString()!)) - { - yield return new ValidationResult(_textService?.Localize("validation", "invalidPattern") ?? ValueIsInvalid, new[] { "value" }); - } + throw new ArgumentNullException(nameof(format)); + } + + if (string.IsNullOrWhiteSpace(format)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(format)); + } + + if (value == null || !new Regex(format).IsMatch(value.ToString()!)) + { + yield return new ValidationResult( + _textService?.Localize("validation", "invalidPattern") ?? ValueIsInvalid, + new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs index 050ba5a388..296e8eed36 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs @@ -1,56 +1,53 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.Validators +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +/// +/// A validator that validates that the value is not null or empty (if it is a string) +/// +public sealed class RequiredValidator : IValueRequiredValidator, IManifestValueValidator { - /// - /// A validator that validates that the value is not null or empty (if it is a string) - /// - public sealed class RequiredValidator : IValueRequiredValidator, IManifestValueValidator + private const string ValueCannotBeNull = "Value cannot be null"; + private const string ValueCannotBeEmpty = "Value cannot be empty"; + private readonly ILocalizedTextService _textService; + + public RequiredValidator(ILocalizedTextService textService) => _textService = textService; + + /// + public string ValidationName => "Required"; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) => + ValidateRequired(value, valueType); + + /// + public IEnumerable ValidateRequired(object? value, string? valueType) { - private readonly ILocalizedTextService _textService; - const string ValueCannotBeNull = "Value cannot be null"; - const string ValueCannotBeEmpty = "Value cannot be empty"; - public RequiredValidator(ILocalizedTextService textService) + if (value == null) { - _textService = textService; + yield return new ValidationResult( + _textService?.Localize("validation", "invalidNull") ?? ValueCannotBeNull, + new[] { "value" }); + yield break; } - /// - public string ValidationName => "Required"; - - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + if (valueType.InvariantEquals(ValueTypes.Json)) { - return ValidateRequired(value, valueType); + if (value.ToString()?.DetectIsEmptyJson() ?? false) + { + yield return new ValidationResult( + _textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); + } + + yield break; } - /// - public IEnumerable ValidateRequired(object? value, string? valueType) + if (value.ToString().IsNullOrWhiteSpace()) { - if (value == null) - { - yield return new ValidationResult(_textService?.Localize("validation", "invalidNull") ?? ValueCannotBeNull, new[] {"value"}); - yield break; - } - - if (valueType.InvariantEquals(ValueTypes.Json)) - { - if (value.ToString()?.DetectIsEmptyJson() ?? false) - { - - yield return new ValidationResult(_textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); - } - - yield break; - } - - if (value.ToString().IsNullOrWhiteSpace()) - { - yield return new ValidationResult(_textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); - } + yield return new ValidationResult( + _textService?.Localize("validation", "invalidEmpty") ?? ValueCannotBeEmpty, new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs index 2aeee98bf4..2e5c17fe7e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs @@ -1,39 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class CheckboxListValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class CheckboxListValueConverter : PropertyValueConverterBase + private readonly IJsonSerializer _jsonSerializer; + + public CheckboxListValueConverter(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) { - private readonly IJsonSerializer _jsonSerializer; + var sourceString = source?.ToString() ?? string.Empty; - public CheckboxListValueConverter(IJsonSerializer jsonSerializer) + if (string.IsNullOrEmpty(sourceString)) { - _jsonSerializer = jsonSerializer; + return Enumerable.Empty(); } - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - var sourceString = source?.ToString() ?? string.Empty; - - if (string.IsNullOrEmpty(sourceString)) - return Enumerable.Empty(); - - return _jsonSerializer.Deserialize(sourceString); - } + return _jsonSerializer.Deserialize(sourceString); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs index 126b4516d1..eded7b7329 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs @@ -1,92 +1,111 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +internal class ContentPickerValueConverter : PropertyValueConverterBase { - internal class ContentPickerValueConverter : PropertyValueConverterBase + private static readonly List PropertiesToExclude = new() { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), + Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture), + }; - private static readonly List PropertiesToExclude = new List + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public ContentPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) => + _publishedSnapshotAccessor = publishedSnapshotAccessor; + + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.ContentPicker); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IPublishedContent); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Elements; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), - Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture) - }; - - public ContentPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) => _publishedSnapshotAccessor = publishedSnapshotAccessor; - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.ContentPicker); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(IPublishedContent); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Elements; - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return null; - - - if(source is not string) - { - var attemptConvertInt = source.TryConvertTo(); - if (attemptConvertInt.Success) - return attemptConvertInt.Result; - } - //Don't attempt to convert to int for UDI - if( source is string strSource - && !string.IsNullOrWhiteSpace(strSource) - && !strSource.StartsWith("umb") - && int.TryParse(strSource, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) - { - return intValue; - } - - var attemptConvertUdi = source.TryConvertTo(); - if (attemptConvertUdi.Success) - return attemptConvertUdi.Result; return null; } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + if (source is not string) { - if (inter == null) - return null; - - if ((propertyType.Alias != null && PropertiesToExclude.Contains(propertyType.Alias.ToLower(CultureInfo.InvariantCulture))) == false) + Attempt attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) { - IPublishedContent? content; - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - if (inter is int id) + return attemptConvertInt.Result; + } + } + + // Don't attempt to convert to int for UDI + if (source is string strSource + && !string.IsNullOrWhiteSpace(strSource) + && !strSource.StartsWith("umb") + && int.TryParse(strSource, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + return intValue; + } + + Attempt attemptConvertUdi = source.TryConvertTo(); + if (attemptConvertUdi.Success) + { + return attemptConvertUdi.Result; + } + + return null; + } + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + { + if (inter == null) + { + return null; + } + + if ((propertyType.Alias != null && + PropertiesToExclude.Contains(propertyType.Alias.ToLower(CultureInfo.InvariantCulture))) == false) + { + IPublishedContent? content; + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + if (inter is int id) + { + content = publishedSnapshot.Content?.GetById(id); + if (content != null) { - content = publishedSnapshot.Content?.GetById(id); - if (content != null) - return content; - } - else - { - var udi = inter as GuidUdi; - if (udi is null) - return null; - content = publishedSnapshot.Content?.GetById(udi.Guid); - if (content != null && content.ContentType.ItemType == PublishedItemType.Content) - return content; + return content; } } + else + { + if (inter is not GuidUdi udi) + { + return null; + } - return inter; + content = publishedSnapshot.Content?.GetById(udi.Guid); + if (content != null && content.ContentType.ItemType == PublishedItemType.Content) + { + return content; + } + } } - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + return inter; + } + + public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + { + if (inter == null) { - if (inter == null) return null; - return inter.ToString(); + return null; } + + return inter.ToString(); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index 7182719ee1..7941946964 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -1,52 +1,57 @@ -using System; using System.Xml; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class DatePickerValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class DatePickerValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTime); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(DateTime); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTime); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (DateTime); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + if (source == null) { - if (source == null) return DateTime.MinValue; - - // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" - // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: - // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 - // We should just be using TryConvertTo instead. - - if (source is string sourceString) - { - var attempt = sourceString.TryConvertTo(); - return attempt.Success == false ? DateTime.MinValue : attempt.Result; - } - - // in the database a DateTime is: DateTime - // default value is: DateTime.MinValue - return source is DateTime ? source : DateTime.MinValue; + return DateTime.MinValue; } - // default ConvertSourceToObject just returns source ie a DateTime value - - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" + // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: + // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 + // We should just be using TryConvertTo instead. + if (source is string sourceString) { - // source should come from ConvertSource and be a DateTime already - if (inter is null) - { - return null; - } - return XmlConvert.ToString((DateTime) inter, XmlDateTimeSerializationMode.Unspecified); + Attempt attempt = sourceString.TryConvertTo(); + return attempt.Success == false ? DateTime.MinValue : attempt.Result; } + + // in the database a DateTime is: DateTime + // default value is: DateTime.MinValue + return source is DateTime ? source : DateTime.MinValue; + } + + // default ConvertSourceToObject just returns source ie a DateTime value + public override object? ConvertIntermediateToXPath( + IPublishedElement owner, + IPublishedPropertyType propertyType, + PropertyCacheLevel referenceCacheLevel, + object? inter, + bool preview) + { + // source should come from ConvertSource and be a DateTime already + if (inter is null) + { + return null; + } + + return XmlConvert.ToString((DateTime)inter, XmlDateTimeSerializationMode.Unspecified); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index 06eb23bc70..5a7f0a4adc 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -1,48 +1,48 @@ -using System; using System.Globalization; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class DecimalValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class DecimalValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.Decimal.Equals(propertyType.EditorAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(decimal); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.Decimal.Equals(propertyType.EditorAlias); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (decimal); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + if (source == null) { - if (source == null) - { - return 0M; - } - - // is it already a decimal? - if(source is decimal) - { - return source; - } - - // is it a double? - if(source is double sourceDouble) - { - return Convert.ToDecimal(sourceDouble); - } - - // is it a string? - if (source is string sourceString) - { - return decimal.TryParse(sourceString, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out decimal d) ? d : 0M; - } - - // couldn't convert the source value - default to zero return 0M; } + + // is it already a decimal? + if (source is decimal) + { + return source; + } + + // is it a double? + if (source is double sourceDouble) + { + return Convert.ToDecimal(sourceDouble); + } + + // is it a string? + if (source is string sourceString) + { + return decimal.TryParse(sourceString, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var d) + ? d + : 0M; + } + + // couldn't convert the source value - default to zero + return 0M; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs index ea7a8b2301..97074b66a3 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs @@ -1,24 +1,25 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class EmailAddressValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class EmailAddressValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.EmailAddress); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.EmailAddress); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - return source?.ToString() ?? string.Empty; - } - } + public override object ConvertIntermediateToObject( + IPublishedElement owner, + IPublishedPropertyType propertyType, + PropertyCacheLevel cacheLevel, + object? source, + bool preview) => + source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/EyeDropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/EyeDropperValueConverter.cs index 6ea5aae9bb..b6bbff3b41 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/EyeDropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EyeDropperValueConverter.cs @@ -1,22 +1,20 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class EyeDropperValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class EyeDropperValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPickerEyeDropper); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPickerEyeDropper); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - => source?.ToString() ?? string.Empty; - } + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + => source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs index 4bffc5a928..f0be275436 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs @@ -1,24 +1,24 @@ -using System; -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class IntegerValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class IntegerValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.Integer.Equals(propertyType.EditorAlias); + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.Integer.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (int); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(int); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - return source.TryConvertTo().Result; - } - } + public override object ConvertSourceToIntermediate( + IPublishedElement owner, + IPublishedPropertyType propertyType, + object? source, + bool preview) => + source.TryConvertTo().Result; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs index 9f2c06cdf9..81f163745a 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs @@ -1,85 +1,125 @@ -using System; -using System.Globalization; +using System.Globalization; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// We need this property converter so that we always force the value of a label to be a string +/// +/// +/// Without a property converter defined for the label type, the value will be converted with +/// the `ConvertUsingDarkMagic` method which will try to parse the value into it's correct type, but this +/// can cause issues if the string is detected as a number and then strips leading zeros. +/// Example: http://issues.umbraco.org/issue/U4-7929 +/// +[DefaultPropertyValueConverter] +public class LabelValueConverter : PropertyValueConverterBase { - /// - /// We need this property converter so that we always force the value of a label to be a string - /// - /// - /// Without a property converter defined for the label type, the value will be converted with - /// the `ConvertUsingDarkMagic` method which will try to parse the value into it's correct type, but this - /// can cause issues if the string is detected as a number and then strips leading zeros. - /// Example: http://issues.umbraco.org/issue/U4-7929 - /// - [DefaultPropertyValueConverter] - public class LabelValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + LabelConfiguration? valueType = + ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); + switch (valueType?.ValueType) { - var valueType = ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); - switch (valueType?.ValueType) - { - case ValueTypes.DateTime: - case ValueTypes.Date: - return typeof(DateTime); - case ValueTypes.Time: - return typeof(TimeSpan); - case ValueTypes.Decimal: - return typeof(decimal); - case ValueTypes.Integer: - return typeof(int); - case ValueTypes.Bigint: - return typeof(long); - default: // everything else is a string - return typeof(string); - } + case ValueTypes.DateTime: + case ValueTypes.Date: + return typeof(DateTime); + case ValueTypes.Time: + return typeof(TimeSpan); + case ValueTypes.Decimal: + return typeof(decimal); + case ValueTypes.Integer: + return typeof(int); + case ValueTypes.Bigint: + return typeof(long); + default: // everything else is a string + return typeof(string); } + } - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + LabelConfiguration? valueType = + ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); + switch (valueType?.ValueType) { - var valueType = ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); - switch (valueType?.ValueType) - { - case ValueTypes.DateTime: - case ValueTypes.Date: - if (source is DateTime sourceDateTime) - return sourceDateTime; - if (source is string sourceDateTimeString) - return DateTime.TryParse(sourceDateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue; - return DateTime.MinValue; - case ValueTypes.Time: - if (source is DateTime sourceTime) - return sourceTime.TimeOfDay; - if (source is string sourceTimeString) - return TimeSpan.TryParse(sourceTimeString, CultureInfo.InvariantCulture, out var ts) ? ts : TimeSpan.Zero; - return TimeSpan.Zero; - case ValueTypes.Decimal: - if (source is decimal sourceDecimal) return sourceDecimal; - if (source is string sourceDecimalString) - return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0; - if (source is double sourceDouble) - return Convert.ToDecimal(sourceDouble); - return (decimal)0; - case ValueTypes.Integer: - if (source is int sourceInt) return sourceInt; - if (source is string sourceIntString) - return int.TryParse(sourceIntString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : 0; - return 0; - case ValueTypes.Bigint: - if (source is string sourceLongString) - return long.TryParse(sourceLongString, out var i) ? i : 0; - return (long)0; - default: // everything else is a string - return source?.ToString() ?? string.Empty; - } + case ValueTypes.DateTime: + case ValueTypes.Date: + if (source is DateTime sourceDateTime) + { + return sourceDateTime; + } + + if (source is string sourceDateTimeString) + { + return DateTime.TryParse(sourceDateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dt) + ? dt + : DateTime.MinValue; + } + + return DateTime.MinValue; + case ValueTypes.Time: + if (source is DateTime sourceTime) + { + return sourceTime.TimeOfDay; + } + + if (source is string sourceTimeString) + { + return TimeSpan.TryParse(sourceTimeString, CultureInfo.InvariantCulture, out TimeSpan ts) + ? ts + : TimeSpan.Zero; + } + + return TimeSpan.Zero; + case ValueTypes.Decimal: + if (source is decimal sourceDecimal) + { + return sourceDecimal; + } + + if (source is string sourceDecimalString) + { + return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) + ? d + : 0; + } + + if (source is double sourceDouble) + { + return Convert.ToDecimal(sourceDouble); + } + + return 0M; + case ValueTypes.Integer: + if (source is int sourceInt) + { + return sourceInt; + } + + if (source is string sourceIntString) + { + return int.TryParse(sourceIntString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? i + : 0; + } + + return 0; + case ValueTypes.Bigint: + if (source is string sourceLongString) + { + return long.TryParse(sourceLongString, out var i) ? i : 0; + } + + return 0L; + default: // everything else is a string + return source?.ToString() ?? string.Empty; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index 2df96fc310..06269ef8e8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -1,92 +1,105 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// The media picker property value converter. +/// +[DefaultPropertyValueConverter] +public class MediaPickerValueConverter : PropertyValueConverterBase { - /// - /// The media picker property value converter. - /// - [DefaultPropertyValueConverter] - public class MediaPickerValueConverter : PropertyValueConverterBase + // hard-coding "image" here but that's how it works at UI level too + private const string ImageTypeAlias = "image"; + + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public MediaPickerValueConverter( + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedModelFactory publishedModelFactory) { - // hard-coding "image" here but that's how it works at UI level too - private const string ImageTypeAlias = "image"; + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? + throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _publishedModelFactory = publishedModelFactory; + } - private readonly IPublishedModelFactory _publishedModelFactory; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + public override bool IsConverter(IPublishedPropertyType propertyType) => + propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MediaPicker); - public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, - IPublishedModelFactory publishedModelFactory) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + { + var isMultiple = IsMultipleDataType(propertyType.DataType); + return isMultiple + ? typeof(IEnumerable) + : typeof(IPublishedContent); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? - throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); - _publishedModelFactory = publishedModelFactory; + return null; } - public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MediaPicker); + Udi[]? nodeIds = source.ToString()? + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(UdiParser.Parse) + .ToArray(); + return nodeIds; + } - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + private bool IsMultipleDataType(PublishedDataType dataType) + { + MediaPickerConfiguration? config = + ConfigurationEditor.ConfigurationAs(dataType.Configuration); + return config?.Multiple ?? false; + } + + public override object? ConvertIntermediateToObject( + IPublishedElement owner, + IPublishedPropertyType propertyType, + PropertyCacheLevel cacheLevel, + object? source, + bool preview) + { + var isMultiple = IsMultipleDataType(propertyType.DataType); + + var udis = (Udi[]?)source; + var mediaItems = new List(); + + if (source == null) { - var isMultiple = IsMultipleDataType(propertyType.DataType); - return isMultiple - ? typeof(IEnumerable) - : typeof(IPublishedContent); + return isMultiple ? mediaItems : null; } - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - private bool IsMultipleDataType(PublishedDataType dataType) + if (udis?.Any() ?? false) { - var config = ConfigurationEditor.ConfigurationAs(dataType.Configuration); - return config?.Multiple ?? false; - } - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, - object? source, bool preview) - { - if (source == null) return null; - - var nodeIds = source.ToString()? - .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(UdiParser.Parse) - .ToArray(); - return nodeIds; - } - - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, - PropertyCacheLevel cacheLevel, object? source, bool preview) - { - var isMultiple = IsMultipleDataType(propertyType.DataType); - - var udis = (Udi[]?)source; - var mediaItems = new List(); - - if (source == null) return isMultiple ? mediaItems : null; - - if (udis?.Any() ?? false) + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + foreach (Udi udi in udis) { - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - foreach (var udi in udis) + if (udi is not GuidUdi guidUdi) { - var guidUdi = udi as GuidUdi; - if (guidUdi is null) continue; - var item = publishedSnapshot?.Media?.GetById(guidUdi.Guid); - if (item != null) - mediaItems.Add(item); + continue; } - return isMultiple ? mediaItems : FirstOrDefault(mediaItems); + IPublishedContent? item = publishedSnapshot?.Media?.GetById(guidUdi.Guid); + if (item != null) + { + mediaItems.Add(item); + } } - return source; + return isMultiple ? mediaItems : FirstOrDefault(mediaItems); } - private object? FirstOrDefault(IList mediaItems) => mediaItems.Count == 0 ? null : mediaItems[0]; + return source; } + + private object? FirstOrDefault(IList mediaItems) => mediaItems.Count == 0 ? null : mediaItems[0]; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs index 2fcaa011fd..a94da59c36 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs @@ -1,24 +1,19 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class MemberGroupPickerValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class MemberGroupPickerValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberGroupPicker); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberGroupPicker); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - return source?.ToString() ?? string.Empty; - } - } + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs index 241b968df9..8c12264198 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -6,91 +5,100 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class MemberPickerValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class MemberPickerValueConverter : PropertyValueConverterBase + private readonly IMemberService _memberService; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public MemberPickerValueConverter( + IMemberService memberService, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IUmbracoContextAccessor umbracoContextAccessor) { - private readonly IMemberService _memberService; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + _memberService = memberService; + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _umbracoContextAccessor = umbracoContextAccessor; + } - public MemberPickerValueConverter( - IMemberService memberService, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberPicker); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IPublishedContent); + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - _memberService = memberService; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _umbracoContextAccessor = umbracoContextAccessor; - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberPicker); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(IPublishedContent); - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) - return null; - - var attemptConvertInt = source.TryConvertTo(); - if (attemptConvertInt.Success) - return attemptConvertInt.Result; - var attemptConvertUdi = source.TryConvertTo(); - if (attemptConvertUdi.Success) - return attemptConvertUdi.Result; return null; } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + Attempt attemptConvertInt = source.TryConvertTo(); + if (attemptConvertInt.Success) { - if (source == null) + return attemptConvertInt.Result; + } + + Attempt attemptConvertUdi = source.TryConvertTo(); + if (attemptConvertUdi.Success) + { + return attemptConvertUdi.Result; + } + + return null; + } + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + { + if (source == null) + { + return null; + } + + IPublishedContent? member; + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + if (source is int id) + { + IMember? m = _memberService.GetById(id); + if (m == null) { return null; } - IPublishedContent? member; - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - if (source is int id) + member = publishedSnapshot?.Members?.Get(m); + if (member != null) { - IMember? m = _memberService.GetById(id); - if (m == null) - { - return null; - } - member = publishedSnapshot?.Members?.Get(m); - if (member != null) - { - return member; - } + return member; } - else - { - var sourceUdi = source as GuidUdi; - if (sourceUdi is null) - return null; - - IMember? m = _memberService.GetByKey(sourceUdi.Guid); - if (m == null) - { - return null; - } - - member = publishedSnapshot?.Members?.Get(m); - - if (member != null) - { - return member; - } - } - - return source; } + else + { + if (source is not GuidUdi sourceUdi) + { + return null; + } + + IMember? m = _memberService.GetByKey(sourceUdi.Guid); + if (m == null) + { + return null; + } + + member = publishedSnapshot?.Members?.Get(m); + + if (member != null) + { + return member; + } + } + + return source; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index faab6e712e..de8965ef3b 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -9,163 +6,187 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// The multi node tree picker property editor value converter. +/// +[DefaultPropertyValueConverter(typeof(MustBeStringValueConverter))] +public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase { - - /// - /// The multi node tree picker property editor value converter. - /// - [DefaultPropertyValueConverter(typeof(MustBeStringValueConverter))] - public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase + private static readonly List PropertiesToExclude = new() { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IMemberService _memberService; + Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), + Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture), + }; - private static readonly List PropertiesToExclude = new List + private readonly IMemberService _memberService; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public MultiNodeTreePickerValueConverter( + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IUmbracoContextAccessor umbracoContextAccessor, + IMemberService memberService) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? + throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _umbracoContextAccessor = umbracoContextAccessor; + _memberService = memberService; + } + + public override bool IsConverter(IPublishedPropertyType propertyType) => + propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Snapshot; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => IsSingleNodePicker(propertyType) + ? typeof(IPublishedContent) + : typeof(IEnumerable); + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) { - Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture), - Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture) - }; - - public MultiNodeTreePickerValueConverter( - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor, - IMemberService memberService) - { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); - _umbracoContextAccessor = umbracoContextAccessor; - _memberService = memberService; - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - { - return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - } - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => IsSingleNodePicker(propertyType) - ? typeof(IPublishedContent) - : typeof(IEnumerable); - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return null; - - if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) - { - var nodeIds = source.ToString()? - .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(UdiParser.Parse) - .ToArray(); - return nodeIds; - } return null; } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) { - if (source == null) - { - return null; - } + Udi[]? nodeIds = source.ToString()? + .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(UdiParser.Parse) + .ToArray(); + return nodeIds; + } - // TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton - if (_umbracoContextAccessor.TryGetUmbracoContext(out _)) + return null; + } + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) + { + if (source == null) + { + return null; + } + + // TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton + if (_umbracoContextAccessor.TryGetUmbracoContext(out _)) + { + if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) { - if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker)) + var udis = (Udi[])source; + var isSingleNodePicker = IsSingleNodePicker(propertyType); + + if ((propertyType.Alias != null && PropertiesToExclude.InvariantContains(propertyType.Alias)) == false) { - var udis = (Udi[])source; - var isSingleNodePicker = IsSingleNodePicker(propertyType); + var multiNodeTreePicker = new List(); - if ((propertyType.Alias != null && PropertiesToExclude.InvariantContains(propertyType.Alias)) == false) + UmbracoObjectTypes objectType = UmbracoObjectTypes.Unknown; + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + foreach (Udi udi in udis) { - var multiNodeTreePicker = new List(); - - var objectType = UmbracoObjectTypes.Unknown; - var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - foreach (var udi in udis) + if (udi is not GuidUdi guidUdi) { - var guidUdi = udi as GuidUdi; - if (guidUdi is null) continue; + continue; + } - IPublishedContent? multiNodeTreePickerItem = null; - switch (udi.EntityType) - { - case Constants.UdiEntityType.Document: - multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Document, id => publishedSnapshot.Content?.GetById(guidUdi.Guid)); - break; - case Constants.UdiEntityType.Media: - multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Media, id => publishedSnapshot.Media?.GetById(guidUdi.Guid)); - break; - case Constants.UdiEntityType.Member: - multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Member, id => + IPublishedContent? multiNodeTreePickerItem = null; + switch (udi.EntityType) + { + case Constants.UdiEntityType.Document: + multiNodeTreePickerItem = GetPublishedContent( + udi, + ref objectType, + UmbracoObjectTypes.Document, + id => publishedSnapshot.Content?.GetById(guidUdi.Guid)); + break; + case Constants.UdiEntityType.Media: + multiNodeTreePickerItem = GetPublishedContent( + udi, + ref objectType, + UmbracoObjectTypes.Media, + id => publishedSnapshot.Media?.GetById(guidUdi.Guid)); + break; + case Constants.UdiEntityType.Member: + multiNodeTreePickerItem = GetPublishedContent( + udi, + ref objectType, + UmbracoObjectTypes.Member, + id => { IMember? m = _memberService.GetByKey(guidUdi.Guid); if (m == null) { return null; } + IPublishedContent? member = publishedSnapshot?.Members?.Get(m); return member; }); - break; - } - - if (multiNodeTreePickerItem != null && multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element) - { - multiNodeTreePicker.Add(multiNodeTreePickerItem); - if (isSingleNodePicker) - { - break; - } - } + break; } - if (isSingleNodePicker) + if (multiNodeTreePickerItem != null && + multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element) { - return multiNodeTreePicker.FirstOrDefault(); + multiNodeTreePicker.Add(multiNodeTreePickerItem); + if (isSingleNodePicker) + { + break; + } } - return multiNodeTreePicker; } - // return the first nodeId as this is one of the excluded properties that expects a single id - return udis.FirstOrDefault(); + if (isSingleNodePicker) + { + return multiNodeTreePicker.FirstOrDefault(); + } + + return multiNodeTreePicker; } + + // return the first nodeId as this is one of the excluded properties that expects a single id + return udis.FirstOrDefault(); } - return source; } - /// - /// Attempt to get an IPublishedContent instance based on ID and content type - /// - /// The content node ID - /// The type of content being requested - /// The type of content expected/supported by - /// A function to fetch content of type - /// The requested content, or null if either it does not exist or does not match - private IPublishedContent? GetPublishedContent(T nodeId, ref UmbracoObjectTypes actualType, UmbracoObjectTypes expectedType, Func contentFetcher) + return source; + } + + private static bool IsSingleNodePicker(IPublishedPropertyType propertyType) => + propertyType.DataType.ConfigurationAs()?.MaxNumber == 1; + + /// + /// Attempt to get an IPublishedContent instance based on ID and content type + /// + /// The content node ID + /// The type of content being requested + /// The type of content expected/supported by + /// A function to fetch content of type + /// + /// The requested content, or null if either it does not exist or does not match + /// + /// + private IPublishedContent? GetPublishedContent(T nodeId, ref UmbracoObjectTypes actualType, UmbracoObjectTypes expectedType, Func contentFetcher) + { + // is the actual type supported by the content fetcher? + if (actualType != UmbracoObjectTypes.Unknown && actualType != expectedType) { - // is the actual type supported by the content fetcher? - if (actualType != UmbracoObjectTypes.Unknown && actualType != expectedType) - { - // no, return null - return null; - } - - // attempt to get the content - var content = contentFetcher(nodeId); - if (content != null) - { - // if we found the content, assign the expected type to the actual type so we don't have to keep looking for other types of content - actualType = expectedType; - } - return content; + // no, return null + return null; } - private static bool IsSingleNodePicker(IPublishedPropertyType propertyType) => propertyType.DataType.ConfigurationAs()?.MaxNumber == 1; + // attempt to get the content + IPublishedContent? content = contentFetcher(nodeId); + if (content != null) + { + // if we found the content, assign the expected type to the actual type so we don't have to keep looking for other types of content + actualType = expectedType; + } + + return content; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index b4ce51c077..3d631afead 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -1,81 +1,79 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml; +using System.Xml; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class MultipleTextStringValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class MultipleTextStringValueConverter : PropertyValueConverterBase + private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; + + public override bool IsConverter(IPublishedPropertyType propertyType) + => Constants.PropertyEditors.Aliases.MultipleTextstring.Equals(propertyType.EditorAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => Constants.PropertyEditors.Aliases.MultipleTextstring.Equals(propertyType.EditorAlias); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + // data is (both in database and xml): + // + // + // Strong + // Flexible + // Efficient + // + // + var sourceString = source?.ToString(); + if (string.IsNullOrWhiteSpace(sourceString)) { - // data is (both in database and xml): - // - // - // Strong - // Flexible - // Efficient - // - // - - var sourceString = source?.ToString(); - if (string.IsNullOrWhiteSpace(sourceString)) return Enumerable.Empty(); - - //SD: I have no idea why this logic is here, I'm pretty sure we've never saved the multiple txt string - // as xml in the database, it's always been new line delimited. Will ask Stephen about this. - // In the meantime, we'll do this xml check, see if it parses and if not just continue with - // splitting by newline - // - // RS: SD/Stephan Please consider post before deciding to remove - //// https://our.umbraco.com/forum/contributing-to-umbraco-cms/76989-keep-the-xml-values-in-the-multipletextstringvalueconverter - var values = new List(); - var pos = sourceString.IndexOf("", StringComparison.Ordinal); - while (pos >= 0) - { - pos += "".Length; - var npos = sourceString.IndexOf("<", pos, StringComparison.Ordinal); - var value = sourceString.Substring(pos, npos - pos); - values.Add(value); - pos = sourceString.IndexOf("", pos, StringComparison.Ordinal); - } - - // fall back on normal behaviour - return values.Any() == false - ? sourceString.Split(NewLineDelimiters, StringSplitOptions.None) - : values.ToArray(); + return Enumerable.Empty(); } - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + // SD: I have no idea why this logic is here, I'm pretty sure we've never saved the multiple txt string + // as xml in the database, it's always been new line delimited. Will ask Stephen about this. + // In the meantime, we'll do this xml check, see if it parses and if not just continue with + // splitting by newline + // + // RS: SD/Stephan Please consider post before deciding to remove + //// https://our.umbraco.com/forum/contributing-to-umbraco-cms/76989-keep-the-xml-values-in-the-multipletextstringvalueconverter + var values = new List(); + var pos = sourceString.IndexOf("", StringComparison.Ordinal); + while (pos >= 0) { - var d = new XmlDocument(); - var e = d.CreateElement("values"); - d.AppendChild(e); - - var values = (IEnumerable?) inter; - if (values is not null) - { - foreach (var value in values) - { - var ee = d.CreateElement("value"); - ee.InnerText = value; - e.AppendChild(ee); - } - } - - return d.CreateNavigator(); + pos += "".Length; + var npos = sourceString.IndexOf("<", pos, StringComparison.Ordinal); + var value = sourceString.Substring(pos, npos - pos); + values.Add(value); + pos = sourceString.IndexOf("", pos, StringComparison.Ordinal); } + + // fall back on normal behaviour + return values.Any() == false + ? sourceString.Split(NewLineDelimiters, StringSplitOptions.None) + : values.ToArray(); + } + + public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + { + var d = new XmlDocument(); + XmlElement e = d.CreateElement("values"); + d.AppendChild(e); + + var values = (IEnumerable?)inter; + if (values is not null) + { + foreach (var value in values) + { + XmlElement ee = d.CreateElement("value"); + ee.InnerText = value; + e.AppendChild(ee); + } + } + + return d.CreateNavigator(); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs index d172e534c4..141cfe53ec 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs @@ -1,39 +1,34 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// Ensures that no matter what is selected in (editor), the value results in a string. +/// +/// +/// +/// For more details see issues http://issues.umbraco.org/issue/U4-3776 (MNTP) +/// and http://issues.umbraco.org/issue/U4-4160 (media picker). +/// +/// +/// The cache level is set to .Content because the string is supposed to depend +/// on the source value only, and not on any other content. It is NOT appropriate +/// to use that converter for values whose .ToString() would depend on other content. +/// +/// +[DefaultPropertyValueConverter] +public class MustBeStringValueConverter : PropertyValueConverterBase { - /// - /// Ensures that no matter what is selected in (editor), the value results in a string. - /// - /// - /// For more details see issues http://issues.umbraco.org/issue/U4-3776 (MNTP) - /// and http://issues.umbraco.org/issue/U4-4160 (media picker). - /// The cache level is set to .Content because the string is supposed to depend - /// on the source value only, and not on any other content. It is NOT appropriate - /// to use that converter for values whose .ToString() would depend on other content. - /// - [DefaultPropertyValueConverter] - public class MustBeStringValueConverter : PropertyValueConverterBase - { - private static readonly string[] Aliases = - { - Constants.PropertyEditors.Aliases.MultiNodeTreePicker - }; + private static readonly string[] Aliases = { Constants.PropertyEditors.Aliases.MultiNodeTreePicker }; - public override bool IsConverter(IPublishedPropertyType propertyType) - => Aliases.Contains(propertyType.EditorAlias); + public override bool IsConverter(IPublishedPropertyType propertyType) + => Aliases.Contains(propertyType.EditorAlias); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - return source?.ToString(); - } - } + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => source?.ToString(); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs index 162764fbf5..c18363a2db 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs @@ -1,29 +1,29 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class RadioButtonListValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class RadioButtonListValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); + Attempt attempt = source.TryConvertTo(); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + if (attempt.Success) { - var attempt = source.TryConvertTo(); - - if (attempt.Success) - return attempt.Result; - - return null; + return attempt.Result; } + + return null; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs index 1ad867bfd0..7503e6711f 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SimpleTinyMceValueConverter.cs @@ -1,43 +1,38 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. +/// +[DefaultPropertyValueConverter] +public class SimpleTinyMceValueConverter : PropertyValueConverterBase { - /// - /// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. - /// - [DefaultPropertyValueConverter] - public class SimpleTinyMceValueConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce; + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce; - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof(IHtmlEncodedString); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IHtmlEncodedString); - // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - // in xml a string is: string - // in the database a string is: string - // default value is: null - return source; - } + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - { - // source should come from ConvertSource and be a string (or null) already - return new HtmlEncodedString(inter == null ? string.Empty : (string)inter); - } + // in xml a string is: string + // in the database a string is: string + // default value is: null + source; - public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - { - // source should come from ConvertSource and be a string (or null) already - return inter; - } - } + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a string (or null) already + new HtmlEncodedString(inter == null ? string.Empty : (string)inter); + + public override object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a string (or null) already + inter; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs index 1da3458dab..76f5b62265 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -1,86 +1,79 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class SliderValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class SliderValueConverter : PropertyValueConverterBase + private static readonly ConcurrentDictionary Storages = new(); + private readonly IDataTypeService _dataTypeService; + + public SliderValueConverter(IDataTypeService dataTypeService) => _dataTypeService = + dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + + public static void ClearCaches() => Storages.Clear(); + + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => IsRangeDataType(propertyType.DataType.Id) ? typeof(Range) : typeof(decimal); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) { - private readonly IDataTypeService _dataTypeService; - - public SliderValueConverter(IDataTypeService dataTypeService) + if (source == null) { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => IsRangeDataType(propertyType.DataType.Id) ? typeof (Range) : typeof (decimal); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - if (source == null) - return null; - - if (IsRangeDataType(propertyType.DataType.Id)) - { - var rangeRawValues = source.ToString()!.Split(Constants.CharArrays.Comma); - var minimumAttempt = rangeRawValues[0].TryConvertTo(); - var maximumAttempt = rangeRawValues[1].TryConvertTo(); - - if ((minimumAttempt.Success) && (maximumAttempt.Success)) - { - return new Range { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result }; - } - } - - var valueAttempt = source.ToString().TryConvertTo(); - if (valueAttempt.Success) - return valueAttempt.Result; - - // Something failed in the conversion of the strings to decimals return null; - } - /// - /// Discovers if the slider is set to range mode. - /// - /// - /// The data type id. - /// - /// - /// The . - /// - private bool IsRangeDataType(int dataTypeId) + if (IsRangeDataType(propertyType.DataType.Id)) { - // GetPreValuesCollectionByDataTypeId is cached at repository level; - // still, the collection is deep-cloned so this is kinda expensive, - // better to cache here + trigger refresh in DataTypeCacheRefresher - // TODO: this is cheap now, remove the caching + var rangeRawValues = source.ToString()!.Split(Constants.CharArrays.Comma); + Attempt minimumAttempt = rangeRawValues[0].TryConvertTo(); + Attempt maximumAttempt = rangeRawValues[1].TryConvertTo(); - return Storages.GetOrAdd(dataTypeId, id => + if (minimumAttempt.Success && maximumAttempt.Success) { - var dataType = _dataTypeService.GetDataType(id); - var configuration = dataType?.ConfigurationAs(); - return configuration?.EnableRange ?? false; - }); + return new Range { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result }; + } } - private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); - - public static void ClearCaches() + Attempt valueAttempt = source.ToString().TryConvertTo(); + if (valueAttempt.Success) { - Storages.Clear(); + return valueAttempt.Result; } + + // Something failed in the conversion of the strings to decimals + return null; } + + /// + /// Discovers if the slider is set to range mode. + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool IsRangeDataType(int dataTypeId) => + + // GetPreValuesCollectionByDataTypeId is cached at repository level; + // still, the collection is deep-cloned so this is kinda expensive, + // better to cache here + trigger refresh in DataTypeCacheRefresher + // TODO: this is cheap now, remove the caching + Storages.GetOrAdd(dataTypeId, id => + { + IDataType? dataType = _dataTypeService.GetDataType(id); + SliderConfiguration? configuration = dataType?.ConfigurationAs(); + return configuration?.EnableRange ?? false; + }); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs index da5dfd5416..3afc5a6596 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -1,82 +1,75 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class TagsValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class TagsValueConverter : PropertyValueConverterBase + private static readonly ConcurrentDictionary Storages = new(); + private readonly IDataTypeService _dataTypeService; + private readonly IJsonSerializer _jsonSerializer; + + public TagsValueConverter(IDataTypeService dataTypeService, IJsonSerializer jsonSerializer) { - private readonly IDataTypeService _dataTypeService; - private readonly IJsonSerializer _jsonSerializer; - - public TagsValueConverter(IDataTypeService dataTypeService, IJsonSerializer jsonSerializer) - { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags); - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) return Array.Empty(); - - // if Json storage type deserialize and return as string array - if (JsonStorageType(propertyType.DataType.Id)) - { - var array = source.ToString() is not null ? _jsonSerializer.Deserialize(source.ToString()!) : null; - return array ?? Array.Empty(); - } - - // Otherwise assume CSV storage type and return as string array - return source.ToString()?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - } - - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - return (string[]?) source; - } - - /// - /// Discovers if the tags data type is storing its data in a Json format - /// - /// - /// The data type id. - /// - /// - /// The . - /// - private bool JsonStorageType(int dataTypeId) - { - // GetDataType(id) is cached at repository level; still, there is some - // deep-cloning involved (expensive) - better cache here + trigger - // refresh in DataTypeCacheRefresher - - return Storages.GetOrAdd(dataTypeId, id => - { - var configuration = _dataTypeService.GetDataType(id)?.ConfigurationAs(); - return configuration?.StorageType == TagsStorageType.Json; - }); - } - - private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); - - public static void ClearCaches() - { - Storages.Clear(); - } + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); } + + public static void ClearCaches() => Storages.Clear(); + + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(IEnumerable); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source == null) + { + return Array.Empty(); + } + + // if Json storage type deserialize and return as string array + if (JsonStorageType(propertyType.DataType.Id)) + { + var array = source.ToString() is not null + ? _jsonSerializer.Deserialize(source.ToString()!) + : null; + return array ?? Array.Empty(); + } + + // Otherwise assume CSV storage type and return as string array + return source.ToString()?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + } + + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) => (string[]?)source; + + /// + /// Discovers if the tags data type is storing its data in a Json format + /// + /// + /// The data type id. + /// + /// + /// The . + /// + private bool JsonStorageType(int dataTypeId) => + + // GetDataType(id) is cached at repository level; still, there is some + // deep-cloning involved (expensive) - better cache here + trigger + // refresh in DataTypeCacheRefresher + Storages.GetOrAdd(dataTypeId, id => + { + TagConfiguration? configuration = _dataTypeService.GetDataType(id)?.ConfigurationAs(); + return configuration?.StorageType == TagsStorageType.Json; + }); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs index a554e7d134..7a9ab907d8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs @@ -1,26 +1,21 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// The upload property value converter. +/// +[DefaultPropertyValueConverter] +public class UploadPropertyConverter : PropertyValueConverterBase { - /// - /// The upload property value converter. - /// - [DefaultPropertyValueConverter] - public class UploadPropertyConverter : PropertyValueConverterBase - { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.UploadField); + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.UploadField); - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - return source?.ToString() ?? ""; - } - } + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) => source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs index 6534ce3f14..ab7f99e7f8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -1,58 +1,63 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultPropertyValueConverter] +public class YesNoValueConverter : PropertyValueConverterBase { - [DefaultPropertyValueConverter] - public class YesNoValueConverter : PropertyValueConverterBase + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.Boolean; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(bool); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.Boolean; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (bool); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + // in xml a boolean is: string + // in the database a boolean is: string "1" or "0" or empty + // typically the converter does not need to handle anything else ("true"...) + // however there are cases where the value passed to the converter could be a non-string object, e.g. int, bool + if (source is string s) { - // in xml a boolean is: string - // in the database a boolean is: string "1" or "0" or empty - // typically the converter does not need to handle anything else ("true"...) - // however there are cases where the value passed to the converter could be a non-string object, e.g. int, bool - - if (source is string s) + if (s.Length == 0 || s == "0") { - if (s.Length == 0 || s == "0") - return false; - - if (s == "1") - return true; - - return bool.TryParse(s, out bool result) && result; + return false; } - if (source is int) - return (int)source == 1; + if (s == "1") + { + return true; + } - // this is required for correct true/false handling in nested content elements - if (source is long) - return (long)source == 1; - - if (source is bool) - return (bool)source; - - // default value is: false - return false; + return bool.TryParse(s, out var result) && result; } - // default ConvertSourceToObject just returns source ie a boolean value - - public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + if (source is int) { - // source should come from ConvertSource and be a boolean already - return (bool?)inter ?? false ? "1" : "0"; + return (int)source == 1; } + + // this is required for correct true/false handling in nested content elements + if (source is long) + { + return (long)source == 1; + } + + if (source is bool) + { + return (bool)source; + } + + // default value is: false + return false; } + + // default ConvertSourceToObject just returns source ie a boolean value + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => + + // source should come from ConvertSource and be a boolean already + (bool?)inter ?? false ? "1" : "0"; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs index 61b8a02f0e..ca727f7008 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueListConfiguration.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the ValueList editor configuration. +/// +public class ValueListConfiguration { - /// - /// Represents the ValueList editor configuration. - /// - public class ValueListConfiguration + [ConfigurationField("items", "Configure", "multivalues", Description = "Add, remove or sort values for the list.")] + public List Items { get; set; } = new(); + + [DataContract] + public class ValueListItem { - [ConfigurationField("items", "Configure", "multivalues", Description = "Add, remove or sort values for the list.")] - public List Items { get; set; } = new List(); + [DataMember(Name = "id")] + public int Id { get; set; } - [DataContract] - public class ValueListItem - { - [DataMember(Name = "id")] - public int Id { get; set; } - - [DataMember(Name = "value")] - public string? Value { get; set; } - } + [DataMember(Name = "value")] + public string? Value { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueTypes.cs b/src/Umbraco.Core/PropertyEditors/ValueTypes.cs index 3a99a70a14..ac6e6a9bb8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueTypes.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueTypes.cs @@ -1,113 +1,113 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents the types of the edited values. +/// +/// +/// +/// These types are used to determine the storage type, but also for +/// validation. Therefore, they are more detailed than the storage types. +/// +/// +public static class ValueTypes { /// - /// Represents the types of the edited values. + /// Date value. /// - /// - /// These types are used to determine the storage type, but also for - /// validation. Therefore, they are more detailed than the storage types. - /// - public static class ValueTypes + public const string Date = "DATE"; // Date + + /// + /// DateTime value. + /// + public const string DateTime = "DATETIME"; // Date + + /// + /// Decimal value. + /// + public const string Decimal = "DECIMAL"; // Decimal + + /// + /// Integer value. + /// + public const string Integer = "INT"; // Integer + + /// + /// Integer value. + /// + public const string Bigint = "BIGINT"; // String + + /// + /// Json value. + /// + public const string Json = "JSON"; // NText + + /// + /// Text value (maps to text database type). + /// + public const string Text = "TEXT"; // NText + + /// + /// Time value. + /// + public const string Time = "TIME"; // Date + + /// + /// Text value (maps to varchar database type). + /// + public const string String = "STRING"; // NVarchar + + /// + /// Xml value. + /// + public const string Xml = "XML"; // NText + + // the auto, static, set of valid values + private static readonly HashSet Values + = new(typeof(ValueTypes) + .GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(x => x.IsLiteral && !x.IsInitOnly) + .Select(x => (string?)x.GetRawConstantValue())); + + /// + /// Determines whether a string value is a valid ValueTypes value. + /// + public static bool IsValue(string s) + => Values.Contains(s); + + /// + /// Gets the value corresponding to a ValueTypes value. + /// + public static ValueStorageType ToStorageType(string valueType) { - // the auto, static, set of valid values - private static readonly HashSet Values - = new HashSet(typeof(ValueTypes) - .GetFields(BindingFlags.Static | BindingFlags.Public) - .Where(x => x.IsLiteral && !x.IsInitOnly) - .Select(x => (string?) x.GetRawConstantValue())); - - /// - /// Date value. - /// - public const string Date = "DATE"; // Date - - /// - /// DateTime value. - /// - public const string DateTime = "DATETIME"; // Date - - /// - /// Decimal value. - /// - public const string Decimal = "DECIMAL"; // Decimal - - /// - /// Integer value. - /// - public const string Integer = "INT"; // Integer - - /// - /// Integer value. - /// - public const string Bigint = "BIGINT"; // String - - /// - /// Json value. - /// - public const string Json = "JSON"; // NText - - /// - /// Text value (maps to text database type). - /// - public const string Text = "TEXT"; // NText - - /// - /// Time value. - /// - public const string Time = "TIME"; // Date - - /// - /// Text value (maps to varchar database type). - /// - public const string String = "STRING"; // NVarchar - - /// - /// Xml value. - /// - public const string Xml = "XML"; // NText - - /// - /// Determines whether a string value is a valid ValueTypes value. - /// - public static bool IsValue(string s) - => Values.Contains(s); - - /// - /// Gets the value corresponding to a ValueTypes value. - /// - public static ValueStorageType ToStorageType(string valueType) + switch (valueType.ToUpperInvariant()) { - switch (valueType.ToUpperInvariant()) - { - case Integer: - return ValueStorageType.Integer; + case Integer: + return ValueStorageType.Integer; - case Decimal: - return ValueStorageType.Decimal; + case Decimal: + return ValueStorageType.Decimal; - case String: - case Bigint: - return ValueStorageType.Nvarchar; + case String: + case Bigint: + return ValueStorageType.Nvarchar; - case Text: - case Json: - case Xml: - return ValueStorageType.Ntext; + case Text: + case Json: + case Xml: + return ValueStorageType.Ntext; - case DateTime: - case Date: - case Time: - return ValueStorageType.Date; + case DateTime: + case Date: + case Time: + return ValueStorageType.Date; - default: - throw new ArgumentOutOfRangeException(nameof(valueType), $"Value \"{valueType}\" is not a valid ValueTypes."); - } + default: + throw new ArgumentOutOfRangeException( + nameof(valueType), + $"Value \"{valueType}\" is not a valid ValueTypes."); } } } diff --git a/src/Umbraco.Core/PropertyEditors/VoidEditor.cs b/src/Umbraco.Core/PropertyEditors/VoidEditor.cs index 28a0afb6ce..f272dc49bd 100644 --- a/src/Umbraco.Core/PropertyEditors/VoidEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/VoidEditor.cs @@ -1,46 +1,49 @@ -using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a void editor. +/// +/// +/// Can be used in some places where an editor is needed but no actual +/// editor is available. Not to be used otherwise. Not discovered, and therefore +/// not part of the editors collection. +/// +[HideFromTypeFinder] +public class VoidEditor : DataEditor { /// - /// Represents a void editor. + /// Initializes a new instance of the class. /// - /// Can be used in some places where an editor is needed but no actual - /// editor is available. Not to be used otherwise. Not discovered, and therefore - /// not part of the editors collection. - [HideFromTypeFinder] - public class VoidEditor : DataEditor + /// An optional alias suffix. + /// A logger factory. + /// + /// The default alias of the editor is "Umbraco.Void". When a suffix is provided, + /// it is appended to the alias. Eg if the suffix is "Foo" the alias is "Umbraco.Void.Foo". + /// + public VoidEditor( + string? aliasSuffix, + IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) { - /// - /// Initializes a new instance of the class. - /// - /// An optional alias suffix. - /// A logger factory. - /// The default alias of the editor is "Umbraco.Void". When a suffix is provided, - /// it is appended to the alias. Eg if the suffix is "Foo" the alias is "Umbraco.Void.Foo". - public VoidEditor( - string? aliasSuffix, - IDataValueEditorFactory dataValueEditorFactory) - : base(dataValueEditorFactory) + Alias = "Umbraco.Void"; + if (string.IsNullOrWhiteSpace(aliasSuffix)) { - Alias = "Umbraco.Void"; - if (string.IsNullOrWhiteSpace(aliasSuffix)) return; - Alias += "." + aliasSuffix; + return; } - /// - /// Initializes a new instance of the class. - /// - /// A logger factory. - /// The alias of the editor is "Umbraco.Void". - public VoidEditor( - IDataValueEditorFactory dataValueEditorFactory) - : this(null, dataValueEditorFactory) - { } + Alias += "." + aliasSuffix; + } + + /// + /// Initializes a new instance of the class. + /// + /// A logger factory. + /// The alias of the editor is "Umbraco.Void". + public VoidEditor( + IDataValueEditorFactory dataValueEditorFactory) + : this(null, dataValueEditorFactory) + { } } diff --git a/src/Umbraco.Core/PublishedCache/DefaultCultureAccessor.cs b/src/Umbraco.Core/PublishedCache/DefaultCultureAccessor.cs index 648041a3a4..4068bc4477 100644 --- a/src/Umbraco.Core/PublishedCache/DefaultCultureAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/DefaultCultureAccessor.cs @@ -1,33 +1,31 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Provides the default implementation of . +/// +public class DefaultCultureAccessor : IDefaultCultureAccessor { + private readonly ILocalizationService _localizationService; + private readonly IRuntimeState _runtimeState; + private GlobalSettings _options; + /// - /// Provides the default implementation of . + /// Initializes a new instance of the class. /// - public class DefaultCultureAccessor : IDefaultCultureAccessor + public DefaultCultureAccessor(ILocalizationService localizationService, IRuntimeState runtimeState, IOptionsMonitor options) { - private readonly ILocalizationService _localizationService; - private readonly IRuntimeState _runtimeState; - private GlobalSettings _options; - - - /// - /// Initializes a new instance of the class. - /// - public DefaultCultureAccessor(ILocalizationService localizationService, IRuntimeState runtimeState, IOptionsMonitor options) - { - _localizationService = localizationService; - _runtimeState = runtimeState; - _options = options.CurrentValue; - options.OnChange(x => _options = x); - } - - /// - public string DefaultCulture => _runtimeState.Level == RuntimeLevel.Run - ? _localizationService.GetDefaultLanguageIsoCode() ?? "" // fast - : _options.DefaultUILanguage; // default for install and upgrade, when the service is n/a + _localizationService = localizationService; + _runtimeState = runtimeState; + _options = options.CurrentValue; + options.OnChange(x => _options = x); } + + /// + public string DefaultCulture => _runtimeState.Level == RuntimeLevel.Run + ? _localizationService.GetDefaultLanguageIsoCode() ?? string.Empty // fast + : _options.DefaultUILanguage; // default for install and upgrade, when the service is n/a } diff --git a/src/Umbraco.Core/PublishedCache/IDefaultCultureAccessor.cs b/src/Umbraco.Core/PublishedCache/IDefaultCultureAccessor.cs index 58844562a7..583daca2f3 100644 --- a/src/Umbraco.Core/PublishedCache/IDefaultCultureAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/IDefaultCultureAccessor.cs @@ -1,16 +1,15 @@ -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Gives access to the default culture. +/// +public interface IDefaultCultureAccessor { /// - /// Gives access to the default culture. + /// Gets the system default culture. /// - public interface IDefaultCultureAccessor - { - /// - /// Gets the system default culture. - /// - /// - /// Implementations must NOT return a null value. Return an empty string for the invariant culture. - /// - string DefaultCulture { get; } - } + /// + /// Implementations must NOT return a null value. Return an empty string for the invariant culture. + /// + string DefaultCulture { get; } } diff --git a/src/Umbraco.Core/PublishedCache/IDomainCache.cs b/src/Umbraco.Core/PublishedCache/IDomainCache.cs index 0555960dfa..41443ef1f6 100644 --- a/src/Umbraco.Core/PublishedCache/IDomainCache.cs +++ b/src/Umbraco.Core/PublishedCache/IDomainCache.cs @@ -1,34 +1,33 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IDomainCache { - public interface IDomainCache - { - /// - /// Gets all in the current domain cache, including any domains that may be referenced by documents that are no longer published. - /// - /// - /// - IEnumerable GetAll(bool includeWildcards); + /// + /// Gets the system default culture. + /// + string DefaultCulture { get; } - /// - /// Gets all assigned for specified document, even if it is not published. - /// - /// The document identifier. - /// A value indicating whether to consider wildcard domains. - IEnumerable GetAssigned(int documentId, bool includeWildcards = false); + /// + /// Gets all in the current domain cache, including any domains that may be referenced by + /// documents that are no longer published. + /// + /// + /// + IEnumerable GetAll(bool includeWildcards); - /// - /// Determines whether a document has domains. - /// - /// The document identifier. - /// A value indicating whether to consider wildcard domains. - bool HasAssigned(int documentId, bool includeWildcards = false); + /// + /// Gets all assigned for specified document, even if it is not published. + /// + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + IEnumerable GetAssigned(int documentId, bool includeWildcards = false); - /// - /// Gets the system default culture. - /// - string DefaultCulture { get; } - } + /// + /// Determines whether a document has domains. + /// + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + bool HasAssigned(int documentId, bool includeWildcards = false); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedCache.cs index 5a06d88ee5..0ee2ca38ed 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedCache.cs @@ -1,245 +1,244 @@ -using System; -using System.Collections.Generic; using System.Xml.XPath; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Provides access to cached contents. +/// +public interface IPublishedCache : IXPathNavigable { /// - /// Provides access to cached contents. + /// Gets a content identified by its unique identifier. /// - public interface IPublishedCache : IXPathNavigable - { - /// - /// Gets a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetById(bool preview, int contentId); + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetById(bool preview, int contentId); - /// - /// Gets a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetById(bool preview, Guid contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetById(bool preview, Guid contentId); - /// - /// Gets a content identified by its Udi identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content Udi identifier. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetById(bool preview, Udi contentId); + /// + /// Gets a content identified by its Udi identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content Udi identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetById(bool preview, Udi contentId); - /// - /// Gets a content identified by its unique identifier. - /// - /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetById(int contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetById(int contentId); - /// - /// Gets a content identified by its unique identifier. - /// - /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetById(Guid contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetById(Guid contentId); - /// - /// Gets a content identified by its unique identifier. - /// - /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetById(Udi contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetById(Udi contentId); - /// - /// Gets a value indicating whether the cache contains a specified content. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// A value indicating whether to the cache contains the specified content. - /// The value of overrides defaults. - bool HasById(bool preview, int contentId); + /// + /// Gets a value indicating whether the cache contains a specified content. + /// + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// A value indicating whether to the cache contains the specified content. + /// The value of overrides defaults. + bool HasById(bool preview, int contentId); - /// - /// Gets a value indicating whether the cache contains a specified content. - /// - /// The content unique identifier. - /// A value indicating whether to the cache contains the specified content. - /// Considers published or unpublished content depending on defaults. - bool HasById(int contentId); + /// + /// Gets a value indicating whether the cache contains a specified content. + /// + /// The content unique identifier. + /// A value indicating whether to the cache contains the specified content. + /// Considers published or unpublished content depending on defaults. + bool HasById(int contentId); - /// - /// Gets contents at root. - /// - /// A value indicating whether to consider unpublished content. - /// A culture. - /// The contents. - /// The value of overrides defaults. - IEnumerable GetAtRoot(bool preview, string? culture = null); + /// + /// Gets contents at root. + /// + /// A value indicating whether to consider unpublished content. + /// A culture. + /// The contents. + /// The value of overrides defaults. + IEnumerable GetAtRoot(bool preview, string? culture = null); - /// - /// Gets contents at root. - /// - /// A culture. - /// The contents. - /// Considers published or unpublished content depending on defaults. - IEnumerable GetAtRoot(string? culture = null); + /// + /// Gets contents at root. + /// + /// A culture. + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetAtRoot(string? culture = null); - /// - /// Gets a content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars); + /// + /// Gets a content resulting from an XPath query. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars); - /// - /// Gets a content resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetSingleByXPath(string xpath, params XPathVariable[] vars); + /// + /// Gets a content resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetSingleByXPath(string xpath, params XPathVariable[] vars); - /// - /// Gets a content resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// The value of overrides defaults. - IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); + /// + /// Gets a content resulting from an XPath query. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); - /// - /// Gets a content resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The content, or null. - /// Considers published or unpublished content depending on defaults. - IPublishedContent? GetSingleByXPath(XPathExpression xpath, params XPathVariable[] vars); + /// + /// Gets a content resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent? GetSingleByXPath(XPathExpression xpath, params XPathVariable[] vars); - /// - /// Gets contents resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// The value of overrides defaults. - IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars); + /// + /// Gets contents resulting from an XPath query. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// The value of overrides defaults. + IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars); - /// - /// Gets contents resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// Considers published or unpublished content depending on defaults. - IEnumerable GetByXPath(string xpath, params XPathVariable[] vars); + /// + /// Gets contents resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetByXPath(string xpath, params XPathVariable[] vars); - /// - /// Gets contents resulting from an XPath query. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// The value of overrides defaults. - IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); + /// + /// Gets contents resulting from an XPath query. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// The value of overrides defaults. + IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); - /// - /// Gets contents resulting from an XPath query. - /// - /// The XPath query. - /// Optional XPath variables. - /// The contents. - /// Considers published or unpublished content depending on defaults. - IEnumerable GetByXPath(XPathExpression xpath, params XPathVariable[] vars); + /// + /// Gets contents resulting from an XPath query. + /// + /// The XPath query. + /// Optional XPath variables. + /// The contents. + /// Considers published or unpublished content depending on defaults. + IEnumerable GetByXPath(XPathExpression xpath, params XPathVariable[] vars); - /// - /// Creates an XPath navigator that can be used to navigate contents. - /// - /// A value indicating whether to consider unpublished content. - /// The XPath navigator. - /// - /// The value of overrides the context. - /// The navigator is already a safe clone (no need to clone it again). - /// - XPathNavigator CreateNavigator(bool preview); + /// + /// Creates an XPath navigator that can be used to navigate contents. + /// + /// A value indicating whether to consider unpublished content. + /// The XPath navigator. + /// + /// The value of overrides the context. + /// The navigator is already a safe clone (no need to clone it again). + /// + XPathNavigator CreateNavigator(bool preview); - /// - /// Creates an XPath navigator that can be used to navigate one node. - /// - /// The node identifier. - /// A value indicating whether to consider unpublished content. - /// The XPath navigator, or null. - /// - /// The value of overrides the context. - /// The navigator is already a safe clone (no need to clone it again). - /// Navigates over the node - and only the node, ie no children. Exists only for backward - /// compatibility + transition reasons, we should obsolete that one as soon as possible. - /// If the node does not exist, returns null. - /// - XPathNavigator? CreateNodeNavigator(int id, bool preview); + /// + /// Creates an XPath navigator that can be used to navigate one node. + /// + /// The node identifier. + /// A value indicating whether to consider unpublished content. + /// The XPath navigator, or null. + /// + /// The value of overrides the context. + /// The navigator is already a safe clone (no need to clone it again). + /// + /// Navigates over the node - and only the node, ie no children. Exists only for backward + /// compatibility + transition reasons, we should obsolete that one as soon as possible. + /// + /// If the node does not exist, returns null. + /// + XPathNavigator? CreateNodeNavigator(int id, bool preview); - /// - /// Gets a value indicating whether the cache contains published content. - /// - /// A value indicating whether to consider unpublished content. - /// A value indicating whether the cache contains published content. - /// The value of overrides defaults. - bool HasContent(bool preview); + /// + /// Gets a value indicating whether the cache contains published content. + /// + /// A value indicating whether to consider unpublished content. + /// A value indicating whether the cache contains published content. + /// The value of overrides defaults. + bool HasContent(bool preview); - /// - /// Gets a value indicating whether the cache contains published content. - /// - /// A value indicating whether the cache contains published content. - /// Considers published or unpublished content depending on defaults. - bool HasContent(); + /// + /// Gets a value indicating whether the cache contains published content. + /// + /// A value indicating whether the cache contains published content. + /// Considers published or unpublished content depending on defaults. + bool HasContent(); - /// - /// Gets a content type identified by its unique identifier. - /// - /// The content type unique identifier. - /// The content type, or null. - IPublishedContentType? GetContentType(int id); + /// + /// Gets a content type identified by its unique identifier. + /// + /// The content type unique identifier. + /// The content type, or null. + IPublishedContentType? GetContentType(int id); - /// - /// Gets a content type identified by its alias. - /// - /// The content type alias. - /// The content type, or null. - /// The alias is case-insensitive. - IPublishedContentType? GetContentType(string alias); + /// + /// Gets a content type identified by its alias. + /// + /// The content type alias. + /// The content type, or null. + /// The alias is case-insensitive. + IPublishedContentType? GetContentType(string alias); - /// - /// Gets contents of a given content type. - /// - /// The content type. - /// The contents. - IEnumerable GetByContentType(IPublishedContentType contentType); + /// + /// Gets contents of a given content type. + /// + /// The content type. + /// The contents. + IEnumerable GetByContentType(IPublishedContentType contentType); - /// - /// Gets a content type identified by its alias. - /// - /// The content type key. - /// The content type, or null. - IPublishedContentType? GetContentType(Guid key); - } + /// + /// Gets a content type identified by its alias. + /// + /// The content type key. + /// The content type, or null. + IPublishedContentType? GetContentType(Guid key); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs index 4621adcb82..7526226302 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs @@ -1,61 +1,80 @@ -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IPublishedContentCache : IPublishedCache { - public interface IPublishedContentCache : IPublishedCache - { - /// - /// Gets content identified by a route. - /// - /// A value indicating whether to consider unpublished content. - /// The route - /// A value forcing the HideTopLevelNode setting. - /// The content, or null. - /// - /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg 123/foo/bar/nil. - /// If is null then the settings value is used. - /// The value of overrides defaults. - /// - IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null); + /// + /// Gets content identified by a route. + /// + /// A value indicating whether to consider unpublished content. + /// The route + /// A value forcing the HideTopLevelNode setting. + /// the culture + /// The content, or null. + /// + /// + /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg + /// 123/foo/bar/nil. + /// + /// + /// If + /// + /// is null then the settings value is used. + /// + /// The value of overrides defaults. + /// + IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null); - /// - /// Gets content identified by a route. - /// - /// The route - /// A value forcing the HideTopLevelNode setting. - /// The content, or null. - /// - /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg 123/foo/bar/nil. - /// If is null then the settings value is used. - /// Considers published or unpublished content depending on defaults. - /// - IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null); + /// + /// Gets content identified by a route. + /// + /// The route + /// A value forcing the HideTopLevelNode setting. + /// The culture + /// The content, or null. + /// + /// + /// A valid route is either a simple path eg /foo/bar/nil or a root node id and a path, eg + /// 123/foo/bar/nil. + /// + /// + /// If + /// + /// is null then the settings value is used. + /// + /// Considers published or unpublished content depending on defaults. + /// + IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null); - /// - /// Gets the route for a content identified by its unique identifier. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// A special string formatted route path. - /// - /// - /// The resulting string is a special encoded route string that may contain the domain ID - /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: {domainId}/route-path-of-item - /// - /// The value of overrides defaults. - /// - string? GetRouteById(bool preview, int contentId, string? culture = null); + /// + /// Gets the route for a content identified by its unique identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content unique identifier. + /// The culture + /// A special string formatted route path. + /// + /// + /// The resulting string is a special encoded route string that may contain the domain ID + /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: + /// {domainId}/route-path-of-item + /// + /// The value of overrides defaults. + /// + string? GetRouteById(bool preview, int contentId, string? culture = null); - /// - /// Gets the route for a content identified by its unique identifier. - /// - /// The content unique identifier. - /// A special string formatted route path. - /// Considers published or unpublished content depending on defaults. - /// - /// The resulting string is a special encoded route string that may contain the domain ID - /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: {domainId}/route-path-of-item - /// - string? GetRouteById(int contentId, string? culture = null); - } + /// + /// Gets the route for a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The culture + /// A special string formatted route path. + /// Considers published or unpublished content depending on defaults. + /// + /// The resulting string is a special encoded route string that may contain the domain ID + /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: + /// {domainId}/route-path-of-item + /// + string? GetRouteById(int contentId, string? culture = null); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs index 1c10776d11..b0fd46748e 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IPublishedMediaCache : IPublishedCache { - public interface IPublishedMediaCache : IPublishedCache - { } } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs index 1f5344df4c..43e6291701 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshot.cs @@ -1,61 +1,67 @@ -using System; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Specifies a published snapshot. +/// +/// +/// A published snapshot is a point-in-time capture of the current state of +/// everything that is "published". +/// +public interface IPublishedSnapshot : IDisposable { /// - /// Specifies a published snapshot. + /// Gets the . /// - /// A published snapshot is a point-in-time capture of the current state of - /// everything that is "published". - public interface IPublishedSnapshot : IDisposable - { - /// - /// Gets the . - /// - IPublishedContentCache? Content { get; } + IPublishedContentCache? Content { get; } - /// - /// Gets the . - /// - IPublishedMediaCache? Media { get; } + /// + /// Gets the . + /// + IPublishedMediaCache? Media { get; } - /// - /// Gets the . - /// - IPublishedMemberCache? Members { get; } + /// + /// Gets the . + /// + IPublishedMemberCache? Members { get; } - /// - /// Gets the . - /// - IDomainCache? Domains { get; } + /// + /// Gets the . + /// + IDomainCache? Domains { get; } - /// - /// Gets the snapshot-level cache. - /// - /// - /// The snapshot-level cache belongs to this snapshot only. - /// - IAppCache? SnapshotCache { get; } + /// + /// Gets the snapshot-level cache. + /// + /// + /// The snapshot-level cache belongs to this snapshot only. + /// + IAppCache? SnapshotCache { get; } - /// - /// Gets the elements-level cache. - /// - /// - /// The elements-level cache is shared by all snapshots relying on the same elements, - /// ie all snapshots built on top of unchanging content / media / etc. - /// - IAppCache? ElementsCache { get; } + /// + /// Gets the elements-level cache. + /// + /// + /// + /// The elements-level cache is shared by all snapshots relying on the same elements, + /// ie all snapshots built on top of unchanging content / media / etc. + /// + /// + IAppCache? ElementsCache { get; } - /// - /// Forces the preview mode. - /// - /// The forced preview mode. - /// A callback to execute when reverting to previous preview. - /// - /// Forcing to false means no preview. Forcing to true means 'full' preview if the snapshot is not already previewing; - /// otherwise the snapshot keeps previewing according to whatever settings it is using already. - /// Stops forcing preview when disposed. - IDisposable ForcedPreview(bool preview, Action? callback = null); - } + /// + /// Forces the preview mode. + /// + /// The forced preview mode. + /// A callback to execute when reverting to previous preview. + /// + /// + /// Forcing to false means no preview. Forcing to true means 'full' preview if the snapshot is not already + /// previewing; + /// otherwise the snapshot keeps previewing according to whatever settings it is using already. + /// + /// Stops forcing preview when disposed. + /// + IDisposable ForcedPreview(bool preview, Action? callback = null); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs index 3a4b5a24b0..0f9cc8fca9 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs @@ -1,10 +1,10 @@ -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Provides access to a TryGetPublishedSnapshot bool method that will return true if the "current" +/// is not null. +/// +public interface IPublishedSnapshotAccessor { - /// - /// Provides access to a TryGetPublishedSnapshot bool method that will return true if the "current" is not null. - /// - public interface IPublishedSnapshotAccessor - { - bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot); - } + bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index 210739c6a2..f8d158dce9 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -1,102 +1,112 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Creates and manages instances. +/// +public interface IPublishedSnapshotService : IDisposable { + /* Various places (such as Node) want to access the XML content, today as an XmlDocument + * but to migrate to a new cache, they're migrating to an XPathNavigator. Still, they need + * to find out how to get that navigator. + * + * Because a cache such as NuCache is contextual i.e. it has a "snapshot" thing and remains + * consistent over the snapshot, the navigator has to come from the "current" snapshot. + * + * So although everything should be injected... we also need a notion of "the current published + * snapshot". This is provided by the IPublishedSnapshotAccessor. + * + */ /// - /// Creates and manages instances. + /// Creates a published snapshot. /// - public interface IPublishedSnapshotService : IDisposable - { - /* Various places (such as Node) want to access the XML content, today as an XmlDocument - * but to migrate to a new cache, they're migrating to an XPathNavigator. Still, they need - * to find out how to get that navigator. - * - * Because a cache such as NuCache is contextual i.e. it has a "snapshot" thing and remains - * consistent over the snapshot, the navigator has to come from the "current" snapshot. - * - * So although everything should be injected... we also need a notion of "the current published - * snapshot". This is provided by the IPublishedSnapshotAccessor. - * - */ + /// A preview token, or null if not previewing. + /// A published snapshot. + /// + /// If is null, the snapshot is not previewing, else it + /// is previewing, and what is or is not visible in preview depends on the content of the token, + /// which is not specified and depends on the actual published snapshot service implementation. + /// + IPublishedSnapshot CreatePublishedSnapshot(string? previewToken); - /// - /// Creates a published snapshot. - /// - /// A preview token, or null if not previewing. - /// A published snapshot. - /// If is null, the snapshot is not previewing, else it - /// is previewing, and what is or is not visible in preview depends on the content of the token, - /// which is not specified and depends on the actual published snapshot service implementation. - IPublishedSnapshot CreatePublishedSnapshot(string? previewToken); + /// + /// Rebuilds internal database caches (but does not reload). + /// + /// + /// If not null will process content for the matching content types, if empty will process all + /// content + /// + /// + /// If not null will process content for the matching media types, if empty will process all + /// media + /// + /// + /// If not null will process content for the matching members types, if empty will process all + /// members + /// + /// + /// + /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches + /// may rely on a database table to store pre-serialized version of documents. + /// + /// + /// This does *not* reload the caches. Caches need to be reloaded, for instance via + /// RefreshAllPublishedSnapshot method. + /// + /// + void Rebuild( + IReadOnlyCollection? contentTypeIds = null, + IReadOnlyCollection? mediaTypeIds = null, + IReadOnlyCollection? memberTypeIds = null); - /// - /// Rebuilds internal database caches (but does not reload). - /// - /// If not null will process content for the matching content types, if empty will process all content - /// If not null will process content for the matching media types, if empty will process all media - /// If not null will process content for the matching members types, if empty will process all members - /// - /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches - /// may rely on a database table to store pre-serialized version of documents. - /// This does *not* reload the caches. Caches need to be reloaded, for instance via - /// RefreshAllPublishedSnapshot method. - /// - void Rebuild( - IReadOnlyCollection? contentTypeIds = null, - IReadOnlyCollection? mediaTypeIds = null, - IReadOnlyCollection? memberTypeIds = null); + /* An IPublishedCachesService implementation can rely on transaction-level events to update + * its internal, database-level data, as these events are purely internal. However, it cannot + * rely on cache refreshers CacheUpdated events to update itself, as these events are external + * and the order-of-execution of the handlers cannot be guaranteed, which means that some + * user code may run before Umbraco is finished updating itself. Instead, the cache refreshers + * explicitly notify the service of changes. + * + */ - /* An IPublishedCachesService implementation can rely on transaction-level events to update - * its internal, database-level data, as these events are purely internal. However, it cannot - * rely on cache refreshers CacheUpdated events to update itself, as these events are external - * and the order-of-execution of the handlers cannot be guaranteed, which means that some - * user code may run before Umbraco is finished updating itself. Instead, the cache refreshers - * explicitly notify the service of changes. - * - */ + /// + /// Notifies of content cache refresher changes. + /// + /// The changes. + /// A value indicating whether draft contents have been changed in the cache. + /// A value indicating whether published contents have been changed in the cache. + void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); - /// - /// Notifies of content cache refresher changes. - /// - /// The changes. - /// A value indicating whether draft contents have been changed in the cache. - /// A value indicating whether published contents have been changed in the cache. - void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); + /// + /// Notifies of media cache refresher changes. + /// + /// The changes. + /// A value indicating whether medias have been changed in the cache. + void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged); - /// - /// Notifies of media cache refresher changes. - /// - /// The changes. - /// A value indicating whether medias have been changed in the cache. - void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged); + // there is no NotifyChanges for MemberCacheRefresher because we're not caching members. - // there is no NotifyChanges for MemberCacheRefresher because we're not caching members. + /// + /// Notifies of content type refresher changes. + /// + /// The changes. + void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads); - /// - /// Notifies of content type refresher changes. - /// - /// The changes. - void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads); + /// + /// Notifies of data type refresher changes. + /// + /// The changes. + void Notify(DataTypeCacheRefresher.JsonPayload[] payloads); - /// - /// Notifies of data type refresher changes. - /// - /// The changes. - void Notify(DataTypeCacheRefresher.JsonPayload[] payloads); + /// + /// Notifies of domain refresher changes. + /// + /// The changes. + void Notify(DomainCacheRefresher.JsonPayload[] payloads); - /// - /// Notifies of domain refresher changes. - /// - /// The changes. - void Notify(DomainCacheRefresher.JsonPayload[] payloads); - - /// - /// Cleans up unused snapshots - /// - Task CollectAsync(); - } + /// + /// Cleans up unused snapshots + /// + Task CollectAsync(); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs index 5695f03377..1eb09c8144 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotStatus.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +/// +/// Returns the currents status for nucache +/// +public interface IPublishedSnapshotStatus { /// - /// Returns the currents status for nucache + /// Gets the URL used to retreive the status /// - public interface IPublishedSnapshotStatus - { - /// - /// Gets the status report as a string - /// - string GetStatus(); + string StatusUrl { get; } - /// - /// Gets the URL used to retreive the status - /// - string StatusUrl { get; } - } + /// + /// Gets the status report as a string + /// + string GetStatus(); } diff --git a/src/Umbraco.Core/PublishedCache/ITagQuery.cs b/src/Umbraco.Core/PublishedCache/ITagQuery.cs index 9a59cac9d6..2deaf75108 100644 --- a/src/Umbraco.Core/PublishedCache/ITagQuery.cs +++ b/src/Umbraco.Core/PublishedCache/ITagQuery.cs @@ -1,59 +1,57 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public interface ITagQuery { - public interface ITagQuery - { - /// - /// Gets all documents tagged with the specified tag. - /// - IEnumerable GetContentByTag(string tag, string? group = null, string? culture = null); + /// + /// Gets all documents tagged with the specified tag. + /// + IEnumerable GetContentByTag(string tag, string? group = null, string? culture = null); - /// - /// Gets all documents tagged with any tag in the specified group. - /// - IEnumerable GetContentByTagGroup(string group, string? culture = null); + /// + /// Gets all documents tagged with any tag in the specified group. + /// + IEnumerable GetContentByTagGroup(string group, string? culture = null); - /// - /// Gets all media tagged with the specified tag. - /// - IEnumerable GetMediaByTag(string tag, string? group = null, string? culture = null); + /// + /// Gets all media tagged with the specified tag. + /// + IEnumerable GetMediaByTag(string tag, string? group = null, string? culture = null); - /// - /// Gets all media tagged with any tag in the specified group. - /// - IEnumerable GetMediaByTagGroup(string group, string? culture = null); + /// + /// Gets all media tagged with any tag in the specified group. + /// + IEnumerable GetMediaByTagGroup(string group, string? culture = null); - /// - /// Gets all tags. - /// - IEnumerable GetAllTags(string? group = null, string? culture = null); + /// + /// Gets all tags. + /// + IEnumerable GetAllTags(string? group = null, string? culture = null); - /// - /// Gets all document tags. - /// - IEnumerable GetAllContentTags(string? group = null, string? culture = null); + /// + /// Gets all document tags. + /// + IEnumerable GetAllContentTags(string? group = null, string? culture = null); - /// - /// Gets all media tags. - /// - IEnumerable GetAllMediaTags(string? group = null, string? culture = null); + /// + /// Gets all media tags. + /// + IEnumerable GetAllMediaTags(string? group = null, string? culture = null); - /// - /// Gets all member tags. - /// - IEnumerable GetAllMemberTags(string? group = null, string? culture = null); + /// + /// Gets all member tags. + /// + IEnumerable GetAllMemberTags(string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity via a property. - /// - IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null); + /// + /// Gets all tags attached to an entity via a property. + /// + IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null); - /// - /// Gets all tags attached to an entity. - /// - IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null); - } + /// + /// Gets all tags attached to an entity. + /// + IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null); } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs index 557d5469b6..0659e835a3 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs @@ -1,107 +1,107 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class InternalPublishedContent : IPublishedContent { - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class InternalPublishedContent : IPublishedContent + private Dictionary? _cultures; + + public InternalPublishedContent(IPublishedContentType contentType) { - public InternalPublishedContent(IPublishedContentType contentType) - { - // initialize boring stuff - TemplateId = 0; - WriterId = CreatorId = 0; - CreateDate = UpdateDate = DateTime.Now; - Version = Guid.Empty; - Path = string.Empty; - ContentType = contentType; - Properties = Enumerable.Empty(); - } + // initialize boring stuff + TemplateId = 0; + WriterId = CreatorId = 0; + CreateDate = UpdateDate = DateTime.Now; + Version = Guid.Empty; + Path = string.Empty; + ContentType = contentType; + Properties = Enumerable.Empty(); + } - private Dictionary? _cultures; + public Guid Version { get; set; } - private Dictionary GetCultures() => new Dictionary { { string.Empty, new PublishedCultureInfo(string.Empty, Name, UrlSegment, UpdateDate) } }; + public int ParentId { get; set; } - public int Id { get; set; } + public IEnumerable? ChildIds { get; set; } - public Guid Key { get; set; } + public int Id { get; set; } - public int? TemplateId { get; set; } - - public int SortOrder { get; set; } - - public string? Name { get; set; } - - public IReadOnlyDictionary Cultures => _cultures ??= GetCultures(); - - public string? UrlSegment { get; set; } - - public int WriterId { get; set; } - - public int CreatorId { get; set; } - - public string Path { get; set; } - - public DateTime CreateDate { get; set; } - - public DateTime UpdateDate { get; set; } - - public Guid Version { get; set; } - - public int Level { get; set; } - - public PublishedItemType ItemType => PublishedItemType.Content; - - public bool IsDraft(string? culture = null) => false; - - public bool IsPublished(string? culture = null) => true; - - public int ParentId { get; set; } - - public IEnumerable? ChildIds { get; set; } - - public IPublishedContent? Parent { get; set; } - - public IEnumerable? Children { get; set; } - - public IEnumerable? ChildrenForAllCultures => Children; - - public IPublishedContentType ContentType { get; set; } - - public IEnumerable Properties { get; set; } - - public IPublishedProperty? GetProperty(string alias) => Properties?.FirstOrDefault(p => p.Alias.InvariantEquals(alias)); - - public IPublishedProperty? GetProperty(string alias, bool recurse) + public object? this[string alias] + { + get { IPublishedProperty? property = GetProperty(alias); - if (recurse == false) - { - return property; - } + return property == null || property.HasValue() == false ? null : property.GetValue(); + } + } - IPublishedContent? content = this; - while (content != null && (property == null || property.HasValue() == false)) - { - content = content.Parent; - property = content?.GetProperty(alias); - } + public Guid Key { get; set; } + public int? TemplateId { get; set; } + + public int SortOrder { get; set; } + + public string? Name { get; set; } + + public IReadOnlyDictionary Cultures => _cultures ??= GetCultures(); + + public string? UrlSegment { get; set; } + + public int WriterId { get; set; } + + public int CreatorId { get; set; } + + public string Path { get; set; } + + public DateTime CreateDate { get; set; } + + public DateTime UpdateDate { get; set; } + + public int Level { get; set; } + + public PublishedItemType ItemType => PublishedItemType.Content; + + public IPublishedContent? Parent { get; set; } + + public bool IsDraft(string? culture = null) => false; + + public bool IsPublished(string? culture = null) => true; + + public IEnumerable? Children { get; set; } + + public IEnumerable? ChildrenForAllCultures => Children; + + public IPublishedContentType ContentType { get; set; } + + public IEnumerable Properties { get; set; } + + public IPublishedProperty? GetProperty(string alias) => + Properties?.FirstOrDefault(p => p.Alias.InvariantEquals(alias)); + + public IPublishedProperty? GetProperty(string alias, bool recurse) + { + IPublishedProperty? property = GetProperty(alias); + if (recurse == false) + { return property; } - public object? this[string alias] + IPublishedContent? content = this; + while (content != null && (property == null || property.HasValue() == false)) { - get - { - var property = GetProperty(alias); - return property == null || property.HasValue() == false ? null : property.GetValue(); - } + content = content.Parent; + property = content?.GetProperty(alias); } + + return property; } + + private Dictionary GetCultures() => new() + { + { string.Empty, new PublishedCultureInfo(string.Empty, Name, UrlSegment, UpdateDate) }, + }; } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs index abeb19e4ec..e4e9010f5b 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs @@ -1,65 +1,70 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; +using System.Xml.XPath; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache { - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache + private readonly Dictionary _content = new(); + + public InternalPublishedContentCache() + : base(false) { - private readonly Dictionary _content = new Dictionary(); - - public InternalPublishedContentCache() - : base(false) - { - } - - //public void Add(InternalPublishedContent content) => _content[content.Id] = content.CreateModel(Mock.Of()); - - public void Clear() => _content.Clear(); - - public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); - - public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); - - public string GetRouteById(bool preview, int contentId, string? culture = null) => throw new NotImplementedException(); - - public string GetRouteById(int contentId, string? culture = null) => throw new NotImplementedException(); - - public override IPublishedContent? GetById(bool preview, int contentId) => _content.ContainsKey(contentId) ? _content[contentId] : null; - - public override IPublishedContent GetById(bool preview, Guid contentId) => throw new NotImplementedException(); - - public override IPublishedContent GetById(bool preview, Udi nodeId) => throw new NotSupportedException(); - - public override bool HasById(bool preview, int contentId) => _content.ContainsKey(contentId); - - public override IEnumerable GetAtRoot(bool preview, string? culture = null) => _content.Values.Where(x => x.Parent == null); - - public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars) => throw new NotImplementedException(); - - public override IPublishedContent GetSingleByXPath(bool preview, System.Xml.XPath.XPathExpression xpath, XPathVariable[] vars) => throw new NotImplementedException(); - - public override IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars) => throw new NotImplementedException(); - - public override IEnumerable GetByXPath(bool preview, System.Xml.XPath.XPathExpression xpath, XPathVariable[] vars) => throw new NotImplementedException(); - - public override System.Xml.XPath.XPathNavigator CreateNavigator(bool preview) => throw new NotImplementedException(); - - public override System.Xml.XPath.XPathNavigator CreateNodeNavigator(int id, bool preview) => throw new NotImplementedException(); - - public override bool HasContent(bool preview) => _content.Count > 0; - - public override IPublishedContentType GetContentType(int id) => throw new NotImplementedException(); - - public override IPublishedContentType GetContentType(string alias) => throw new NotImplementedException(); - - public override IPublishedContentType GetContentType(Guid key) => throw new NotImplementedException(); - - public override IEnumerable GetByContentType(IPublishedContentType contentType) => throw new NotImplementedException(); } + + public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); + + public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => + throw new NotImplementedException(); + + public string GetRouteById(bool preview, int contentId, string? culture = null) => + throw new NotImplementedException(); + + public string GetRouteById(int contentId, string? culture = null) => throw new NotImplementedException(); + + public override IPublishedContent? GetById(bool preview, int contentId) => + _content.ContainsKey(contentId) ? _content[contentId] : null; + + public override IPublishedContent GetById(bool preview, Guid contentId) => throw new NotImplementedException(); + + public override IPublishedContent GetById(bool preview, Udi nodeId) => throw new NotSupportedException(); + + public override bool HasById(bool preview, int contentId) => _content.ContainsKey(contentId); + + public override IEnumerable GetAtRoot(bool preview, string? culture = null) => + _content.Values.Where(x => x.Parent == null); + + public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars) => + throw new NotImplementedException(); + + public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) => + throw new NotImplementedException(); + + public override IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars) => + throw new NotImplementedException(); + + public override IEnumerable + GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars) => throw new NotImplementedException(); + + public override XPathNavigator CreateNavigator(bool preview) => throw new NotImplementedException(); + + public override XPathNavigator CreateNodeNavigator(int id, bool preview) => throw new NotImplementedException(); + + public override bool HasContent(bool preview) => _content.Count > 0; + + public override IPublishedContentType GetContentType(int id) => throw new NotImplementedException(); + + public override IPublishedContentType GetContentType(string alias) => throw new NotImplementedException(); + + public override IPublishedContentType GetContentType(Guid key) => throw new NotImplementedException(); + + public override IEnumerable GetByContentType(IPublishedContentType contentType) => + throw new NotImplementedException(); + + // public void Add(InternalPublishedContent content) => _content[content.Id] = content.CreateModel(Mock.Of()); + public void Clear() => _content.Clear(); } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs index 0e7280d443..d9437e6b8c 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs @@ -1,30 +1,29 @@ using System.ComponentModel; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public class InternalPublishedProperty : IPublishedProperty { - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public class InternalPublishedProperty : IPublishedProperty - { - public IPublishedPropertyType PropertyType { get; set; } = null!; + public object? SolidSourceValue { get; set; } - public string Alias { get; set; } = string.Empty; + public object? SolidValue { get; set; } - public object? SolidSourceValue { get; set; } + public bool SolidHasValue { get; set; } - public object? SolidValue { get; set; } + public object? SolidXPathValue { get; set; } - public bool SolidHasValue { get; set; } + public IPublishedPropertyType PropertyType { get; set; } = null!; - public object? SolidXPathValue { get; set; } + public string Alias { get; set; } = string.Empty; - public virtual object? GetSourceValue(string? culture = null, string? segment = null) => SolidSourceValue; + public virtual object? GetSourceValue(string? culture = null, string? segment = null) => SolidSourceValue; - public virtual object? GetValue(string? culture = null, string? segment = null) => SolidValue; + public virtual object? GetValue(string? culture = null, string? segment = null) => SolidValue; - public virtual object? GetXPathValue(string? culture = null, string? segment = null) => SolidXPathValue; + public virtual object? GetXPathValue(string? culture = null, string? segment = null) => SolidXPathValue; - public virtual bool HasValue(string? culture = null, string? segment = null) => SolidHasValue; - } + public virtual bool HasValue(string? culture = null, string? segment = null) => SolidHasValue; } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs index 0516edc47b..015962b5aa 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs @@ -1,37 +1,36 @@ -using System; using System.ComponentModel; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class InternalPublishedSnapshot : IPublishedSnapshot { + public InternalPublishedContentCache InnerContentCache { get; } = new(); - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class InternalPublishedSnapshot : IPublishedSnapshot + public InternalPublishedContentCache InnerMediaCache { get; } = new(); + + public IPublishedContentCache Content => InnerContentCache; + + public IPublishedMediaCache Media => InnerMediaCache; + + public IPublishedMemberCache? Members => null; + + public IDomainCache? Domains => null; + + public IAppCache? SnapshotCache => null; + + public IDisposable ForcedPreview(bool forcedPreview, Action? callback = null) => + throw new NotImplementedException(); + + public IAppCache? ElementsCache => null; + + public void Dispose() { - public InternalPublishedContentCache InnerContentCache { get; } = new InternalPublishedContentCache(); - public InternalPublishedContentCache InnerMediaCache { get; } = new InternalPublishedContentCache(); + } - public IPublishedContentCache Content => InnerContentCache; - - public IPublishedMediaCache Media => InnerMediaCache; - - public IPublishedMemberCache? Members => null; - - public IDomainCache? Domains => null; - - public IDisposable ForcedPreview(bool forcedPreview, Action? callback = null) => throw new NotImplementedException(); - - public void Resync() - { - } - - public IAppCache? SnapshotCache => null; - - public IAppCache? ElementsCache => null; - - public void Dispose() - { - } + public void Resync() + { } } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs index bbf121b457..09de76ace5 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs @@ -1,63 +1,55 @@ -using System.Collections.Generic; using System.ComponentModel; -using System.Threading.Tasks; using Umbraco.Cms.Core.Cache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PublishedCache.Internal +namespace Umbraco.Cms.Core.PublishedCache.Internal; + +// TODO: Only used in unit tests, needs to be moved to test project +[EditorBrowsable(EditorBrowsableState.Never)] +public class InternalPublishedSnapshotService : IPublishedSnapshotService { - // TODO: Only used in unit tests, needs to be moved to test project - [EditorBrowsable(EditorBrowsableState.Never)] - public class InternalPublishedSnapshotService : IPublishedSnapshotService + private InternalPublishedSnapshot? _previewSnapshot; + private InternalPublishedSnapshot? _snapshot; + + public Task CollectAsync() => Task.CompletedTask; + + public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) { - private InternalPublishedSnapshot? _snapshot; - private InternalPublishedSnapshot? _previewSnapshot; - - public Task CollectAsync() => Task.CompletedTask; - - public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) + if (previewToken.IsNullOrWhiteSpace()) { - if (previewToken.IsNullOrWhiteSpace()) - { - return _snapshot ??= new InternalPublishedSnapshot(); - } - else - { - return _previewSnapshot ??= new InternalPublishedSnapshot(); - } + return _snapshot ??= new InternalPublishedSnapshot(); } - public void Dispose() - { - _snapshot?.Dispose(); - _previewSnapshot?.Dispose(); - } + return _previewSnapshot ??= new InternalPublishedSnapshot(); + } - public void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) - { - draftChanged = false; - publishedChanged = false; - } + public void Dispose() + { + _snapshot?.Dispose(); + _previewSnapshot?.Dispose(); + } - public void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) - { - anythingChanged = false; - } + public void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) + { + draftChanged = false; + publishedChanged = false; + } - public void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) - { - } + public void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) => anythingChanged = false; - public void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) - { - } + public void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) + { + } - public void Notify(DomainCacheRefresher.JsonPayload[] payloads) - { - } + public void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) + { + } - public void Rebuild(IReadOnlyCollection? contentTypeIds = null, IReadOnlyCollection? mediaTypeIds = null, IReadOnlyCollection? memberTypeIds = null) - { - } + public void Notify(DomainCacheRefresher.JsonPayload[] payloads) + { + } + + public void Rebuild(IReadOnlyCollection? contentTypeIds = null, IReadOnlyCollection? mediaTypeIds = null, IReadOnlyCollection? memberTypeIds = null) + { } } diff --git a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs index b374424b8b..3e961ce434 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedCacheBase.cs @@ -1,111 +1,87 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Xml.XPath; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +public abstract class PublishedCacheBase : IPublishedCache { - public abstract class PublishedCacheBase : IPublishedCache - { - private readonly IVariationContextAccessor? _variationContextAccessor; + private readonly IVariationContextAccessor? _variationContextAccessor; - public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) - { - _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) => _variationContextAccessor = + variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - } - public bool PreviewDefault { get; } + protected PublishedCacheBase(bool previewDefault) => PreviewDefault = previewDefault; - protected PublishedCacheBase(bool previewDefault) - { - PreviewDefault = previewDefault; - } + public bool PreviewDefault { get; } - public abstract IPublishedContent? GetById(bool preview, int contentId); + public abstract IPublishedContent? GetById(bool preview, int contentId); - public IPublishedContent? GetById(int contentId) - => GetById(PreviewDefault, contentId); + public IPublishedContent? GetById(int contentId) + => GetById(PreviewDefault, contentId); - public abstract IPublishedContent? GetById(bool preview, Guid contentId); + public abstract IPublishedContent? GetById(bool preview, Guid contentId); - public IPublishedContent? GetById(Guid contentId) - => GetById(PreviewDefault, contentId); + public IPublishedContent? GetById(Guid contentId) + => GetById(PreviewDefault, contentId); - public abstract IPublishedContent? GetById(bool preview, Udi contentId); + public abstract IPublishedContent? GetById(bool preview, Udi contentId); - public IPublishedContent? GetById(Udi contentId) - => GetById(PreviewDefault, contentId); + public IPublishedContent? GetById(Udi contentId) + => GetById(PreviewDefault, contentId); - public abstract bool HasById(bool preview, int contentId); + public abstract bool HasById(bool preview, int contentId); - public bool HasById(int contentId) - => HasById(PreviewDefault, contentId); + public bool HasById(int contentId) + => HasById(PreviewDefault, contentId); - public abstract IEnumerable GetAtRoot(bool preview, string? culture = null); + public abstract IEnumerable GetAtRoot(bool preview, string? culture = null); - public IEnumerable GetAtRoot(string? culture = null) - { - return GetAtRoot(PreviewDefault, culture); - } + public IEnumerable GetAtRoot(string? culture = null) => GetAtRoot(PreviewDefault, culture); - public abstract IPublishedContent? GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); + public abstract IPublishedContent? GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); - public IPublishedContent? GetSingleByXPath(string xpath, XPathVariable[] vars) - { - return GetSingleByXPath(PreviewDefault, xpath, vars); - } + public IPublishedContent? GetSingleByXPath(string xpath, XPathVariable[] vars) => + GetSingleByXPath(PreviewDefault, xpath, vars); - public abstract IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); + public abstract IPublishedContent? GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); - public IPublishedContent? GetSingleByXPath(XPathExpression xpath, XPathVariable[] vars) - { - return GetSingleByXPath(PreviewDefault, xpath, vars); - } + public IPublishedContent? GetSingleByXPath(XPathExpression xpath, XPathVariable[] vars) => + GetSingleByXPath(PreviewDefault, xpath, vars); - public abstract IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars); + public abstract IEnumerable GetByXPath(bool preview, string xpath, XPathVariable[] vars); - public IEnumerable GetByXPath(string xpath, XPathVariable[] vars) - { - return GetByXPath(PreviewDefault, xpath, vars); - } + public IEnumerable GetByXPath(string xpath, XPathVariable[] vars) => + GetByXPath(PreviewDefault, xpath, vars); - public abstract IEnumerable GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); + public abstract IEnumerable + GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars); - public IEnumerable GetByXPath(XPathExpression xpath, XPathVariable[] vars) - { - return GetByXPath(PreviewDefault, xpath, vars); - } + public IEnumerable GetByXPath(XPathExpression xpath, XPathVariable[] vars) => + GetByXPath(PreviewDefault, xpath, vars); - public abstract XPathNavigator CreateNavigator(bool preview); + public abstract XPathNavigator CreateNavigator(bool preview); - public XPathNavigator CreateNavigator() - { - return CreateNavigator(PreviewDefault); - } + public XPathNavigator CreateNavigator() => CreateNavigator(PreviewDefault); - public abstract XPathNavigator? CreateNodeNavigator(int id, bool preview); + public abstract XPathNavigator? CreateNodeNavigator(int id, bool preview); - public abstract bool HasContent(bool preview); + public abstract bool HasContent(bool preview); - public bool HasContent() - { - return HasContent(PreviewDefault); - } + public bool HasContent() => HasContent(PreviewDefault); - public abstract IPublishedContentType? GetContentType(int id); - public abstract IPublishedContentType? GetContentType(string alias); - public abstract IPublishedContentType? GetContentType(Guid key); + public abstract IPublishedContentType? GetContentType(int id); - public virtual IEnumerable GetByContentType(IPublishedContentType contentType) - { - // this is probably not super-efficient, but works - // some cache implementation may want to override it, though - return GetAtRoot() - .SelectMany(x => x.DescendantsOrSelf(_variationContextAccessor!)) - .Where(x => x.ContentType.Id == contentType.Id); - } - } + public abstract IPublishedContentType? GetContentType(string alias); + + public abstract IPublishedContentType? GetContentType(Guid key); + + public virtual IEnumerable GetByContentType(IPublishedContentType contentType) => + + // this is probably not super-efficient, but works + // some cache implementation may want to override it, though + GetAtRoot() + .SelectMany(x => x.DescendantsOrSelf(_variationContextAccessor!)) + .Where(x => x.ContentType.Id == contentType.Id); } diff --git a/src/Umbraco.Core/PublishedCache/PublishedElement.cs b/src/Umbraco.Core/PublishedCache/PublishedElement.cs index c67e3b0e40..297a62b589 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedElement.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedElement.cs @@ -1,89 +1,88 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +// notes: +// a published element does NOT manage any tree-like elements, neither the +// original NestedContent (from Lee) nor the DetachedPublishedContent POC did. +// +// at the moment we do NOT support models for sets - that would require +// an entirely new models factory + not even sure it makes sense at all since +// sets are created manually todo yes it does! - what does this all mean? +// +public class PublishedElement : IPublishedElement { - // notes: - // a published element does NOT manage any tree-like elements, neither the - // original NestedContent (from Lee) nor the DetachedPublishedContent POC did. - // - // at the moment we do NOT support models for sets - that would require - // an entirely new models factory + not even sure it makes sense at all since - // sets are created manually todo yes it does! - what does this all mean? - // - public class PublishedElement : IPublishedElement + + private readonly IPublishedProperty[] _propertiesArray; + + // initializes a new instance of the PublishedElement class + // within the context of a published snapshot service (eg a published content property value) + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, PropertyCacheLevel referenceCacheLevel, IPublishedSnapshotAccessor? publishedSnapshotAccessor) { - // initializes a new instance of the PublishedElement class - // within the context of a published snapshot service (eg a published content property value) - public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary? values, bool previewing, - PropertyCacheLevel referenceCacheLevel, IPublishedSnapshotAccessor? publishedSnapshotAccessor) + if (key == Guid.Empty) { - if (key == Guid.Empty) throw new ArgumentException("Empty guid."); - if (values == null) throw new ArgumentNullException(nameof(values)); - if (referenceCacheLevel != PropertyCacheLevel.None && publishedSnapshotAccessor == null) - throw new ArgumentNullException("A published snapshot accessor is required when referenceCacheLevel != None.", nameof(publishedSnapshotAccessor)); - - ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); - Key = key; - - values = GetCaseInsensitiveValueDictionary(values); - - _propertiesArray = contentType - .PropertyTypes? - .Select(propertyType => - { - values.TryGetValue(propertyType.Alias, out var value); - return (IPublishedProperty)new PublishedElementPropertyBase(propertyType, this, previewing, referenceCacheLevel, value, publishedSnapshotAccessor); - }) - .ToArray() - ?? new IPublishedProperty[0]; + throw new ArgumentException("Empty guid."); } - // initializes a new instance of the PublishedElement class - // without any context, so it's purely 'standalone' and should NOT interfere with the published snapshot service - // + using an initial reference cache level of .None ensures that everything will be - // cached at .Content level - and that reference cache level will propagate to all - // properties - public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing) - : this(contentType, key, values, previewing, PropertyCacheLevel.None, null) - { } - - private static Dictionary GetCaseInsensitiveValueDictionary(Dictionary values) + if (values == null) { - // ensure we ignore case for property aliases - var comparer = values.Comparer; - var ignoreCase = Equals(comparer, StringComparer.OrdinalIgnoreCase) || Equals(comparer, StringComparer.InvariantCultureIgnoreCase) || Equals(comparer, StringComparer.CurrentCultureIgnoreCase); - return ignoreCase ? values : new Dictionary(values, StringComparer.OrdinalIgnoreCase); + throw new ArgumentNullException(nameof(values)); } - #region ContentType - - public IPublishedContentType ContentType { get; } - - #endregion - - #region PublishedElement - - public Guid Key { get; } - - #endregion - - #region Properties - - private readonly IPublishedProperty[] _propertiesArray; - - public IEnumerable Properties => _propertiesArray; - - public IPublishedProperty? GetProperty(string alias) + if (referenceCacheLevel != PropertyCacheLevel.None && publishedSnapshotAccessor == null) { - var index = ContentType.GetPropertyIndex(alias); - var property = index < 0 ? null : _propertiesArray?[index]; - return property; + throw new ArgumentNullException( + "A published snapshot accessor is required when referenceCacheLevel != None.", + nameof(publishedSnapshotAccessor)); } - #endregion + ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); + Key = key; + + values = GetCaseInsensitiveValueDictionary(values); + + _propertiesArray = contentType + .PropertyTypes? + .Select(propertyType => + { + values.TryGetValue(propertyType.Alias, out var value); + return (IPublishedProperty)new PublishedElementPropertyBase(propertyType, this, previewing, referenceCacheLevel, value, publishedSnapshotAccessor); + }) + .ToArray() + ?? new IPublishedProperty[0]; + } + + // initializes a new instance of the PublishedElement class + // without any context, so it's purely 'standalone' and should NOT interfere with the published snapshot service + // + using an initial reference cache level of .None ensures that everything will be + // cached at .Content level - and that reference cache level will propagate to all + // properties + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing) + : this(contentType, key, values, previewing, PropertyCacheLevel.None, null) + { + } + + public IPublishedContentType ContentType { get; } + + public Guid Key { get; } + + private static Dictionary GetCaseInsensitiveValueDictionary(Dictionary values) + { + // ensure we ignore case for property aliases + IEqualityComparer comparer = values.Comparer; + var ignoreCase = Equals(comparer, StringComparer.OrdinalIgnoreCase) || + Equals(comparer, StringComparer.InvariantCultureIgnoreCase) || + Equals(comparer, StringComparer.CurrentCultureIgnoreCase); + return ignoreCase ? values : new Dictionary(values, StringComparer.OrdinalIgnoreCase); + } + + public IEnumerable Properties => _propertiesArray; + + public IPublishedProperty? GetProperty(string alias) + { + var index = ContentType.GetPropertyIndex(alias); + IPublishedProperty? property = index < 0 ? null : _propertiesArray?[index]; + return property; } } diff --git a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs index c6fe365be8..6beb094bef 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs @@ -1,197 +1,224 @@ -using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +internal class PublishedElementPropertyBase : PublishedPropertyBase { - internal class PublishedElementPropertyBase : PublishedPropertyBase + protected readonly IPublishedElement Element; + + // define constant - determines whether to use cache when previewing + // to store eg routes, property converted values, anything - caching + // means faster execution, but uses memory - not sure if we want it + // so making it configurable. + private const bool FullCacheWhenPreviewing = true; + private readonly object _locko = new(); + private readonly IPublishedSnapshotAccessor? _publishedSnapshotAccessor; + private readonly object? _sourceValue; + protected readonly bool IsMember; + protected readonly bool IsPreviewing; + private CacheValues? _cacheValues; + + private bool _interInitialized; + private object? _interValue; + private string? _valuesCacheKey; + + public PublishedElementPropertyBase( + IPublishedPropertyType propertyType, + IPublishedElement element, + bool previewing, + PropertyCacheLevel referenceCacheLevel, + object? sourceValue = null, + IPublishedSnapshotAccessor? publishedSnapshotAccessor = null) + : base(propertyType, referenceCacheLevel) { - private readonly object _locko = new object(); - private readonly object? _sourceValue; - private readonly IPublishedSnapshotAccessor? _publishedSnapshotAccessor; + _sourceValue = sourceValue; + _publishedSnapshotAccessor = publishedSnapshotAccessor; + Element = element; + IsPreviewing = previewing; + IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member; + } - protected readonly IPublishedElement Element; - protected readonly bool IsPreviewing; - protected readonly bool IsMember; + // used to cache the CacheValues of this property + // ReSharper disable InconsistentlySynchronizedField + internal string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValues(Element.Key, Alias, IsPreviewing); - private bool _interInitialized; - private object? _interValue; - private CacheValues? _cacheValues; - private string? _valuesCacheKey; + public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) => + "PublishedSnapshot.Property.CacheValues[" + (previewing ? "D:" : "P:") + contentUid + ":" + typeAlias + "]"; - // define constant - determines whether to use cache when previewing - // to store eg routes, property converted values, anything - caching - // means faster execution, but uses memory - not sure if we want it - // so making it configurable. - private const bool FullCacheWhenPreviewing = true; - - public PublishedElementPropertyBase(IPublishedPropertyType propertyType, IPublishedElement element, bool previewing, PropertyCacheLevel referenceCacheLevel, object? sourceValue = null, IPublishedSnapshotAccessor? publishedSnapshotAccessor = null) - : base(propertyType, referenceCacheLevel) + // ReSharper restore InconsistentlySynchronizedField + public override bool HasValue(string? culture = null, string? segment = null) + { + var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source); + if (hasValue.HasValue) { - _sourceValue = sourceValue; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - Element = element; - IsPreviewing = previewing; - IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member; + return hasValue.Value; } - public override bool HasValue(string? culture = null, string? segment = null) + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); + + lock (_locko) { - var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source); - if (hasValue.HasValue) return hasValue.Value; - - GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); - - lock (_locko) + var value = GetInterValue(); + hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); + if (hasValue.HasValue) { - var value = GetInterValue(); - hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); - if (hasValue.HasValue) return hasValue.Value; - - var cacheValues = GetCacheValues(cacheLevel); - if (!cacheValues.ObjectInitialized) - { - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing); - cacheValues.ObjectInitialized = true; - } - value = cacheValues.ObjectValue; - return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; + return hasValue.Value; } + + CacheValues cacheValues = GetCacheValues(cacheLevel); + if (!cacheValues.ObjectInitialized) + { + cacheValues.ObjectValue = + PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing); + cacheValues.ObjectInitialized = true; + } + + value = cacheValues.ObjectValue; + return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; + } + } + + public override object? GetSourceValue(string? culture = null, string? segment = null) => _sourceValue; + + private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) + { + // based upon the current reference cache level (ReferenceCacheLevel) and this property + // cache level (PropertyType.CacheLevel), determines both the actual cache level for the + // property, and the new reference cache level. + + // if the property cache level is 'shorter-termed' that the reference + // then use it and it becomes the new reference, else use Content and + // don't change the reference. + // + // examples: + // currently (reference) caching at published snapshot, property specifies + // elements, ok to use element. OTOH, currently caching at elements, + // property specifies snapshot, need to use snapshot. + if (PropertyType.CacheLevel > ReferenceCacheLevel || PropertyType.CacheLevel == PropertyCacheLevel.None) + { + cacheLevel = PropertyType.CacheLevel; + referenceCacheLevel = cacheLevel; + } + else + { + cacheLevel = PropertyCacheLevel.Element; + referenceCacheLevel = ReferenceCacheLevel; + } + } + + private IAppCache? GetSnapshotCache() + { + // cache within the snapshot cache, unless previewing, then use the snapshot or + // elements cache (if we don't want to pollute the elements cache with short-lived + // data) depending on settings + // for members, always cache in the snapshot cache - never pollute elements cache + if (_publishedSnapshotAccessor is null) + { + return null; } - // used to cache the CacheValues of this property - // ReSharper disable InconsistentlySynchronizedField - internal string ValuesCacheKey => _valuesCacheKey - ?? (_valuesCacheKey = PropertyCacheValues(Element.Key, Alias, IsPreviewing)); - // ReSharper restore InconsistentlySynchronizedField - - protected class CacheValues + if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) { - public bool ObjectInitialized; - public object? ObjectValue; - public bool XPathInitialized; - public object? XPathValue; + return null; } - public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) => "PublishedSnapshot.Property.CacheValues[" + (previewing ? "D:" : "P:") + contentUid + ":" + typeAlias + "]"; + return (IsPreviewing == false || FullCacheWhenPreviewing) && IsMember == false + ? publishedSnapshot!.ElementsCache + : publishedSnapshot!.SnapshotCache; + } - private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) + private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) + { + CacheValues cacheValues; + switch (cacheLevel) { - // based upon the current reference cache level (ReferenceCacheLevel) and this property - // cache level (PropertyType.CacheLevel), determines both the actual cache level for the - // property, and the new reference cache level. + case PropertyCacheLevel.None: + // never cache anything + cacheValues = new CacheValues(); + break; + case PropertyCacheLevel.Element: + // cache within the property object itself, ie within the content object + cacheValues = _cacheValues ??= new CacheValues(); + break; + case PropertyCacheLevel.Elements: + // cache within the elements cache, depending... + IAppCache? snapshotCache = GetSnapshotCache(); + cacheValues = (CacheValues?)snapshotCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? + new CacheValues(); + break; + case PropertyCacheLevel.Snapshot: + IPublishedSnapshot? publishedSnapshot = _publishedSnapshotAccessor?.GetRequiredPublishedSnapshot(); - // if the property cache level is 'shorter-termed' that the reference - // then use it and it becomes the new reference, else use Content and - // don't change the reference. - // - // examples: - // currently (reference) caching at published snapshot, property specifies - // elements, ok to use element. OTOH, currently caching at elements, - // property specifies snapshot, need to use snapshot. - // - if (PropertyType.CacheLevel > ReferenceCacheLevel || PropertyType.CacheLevel == PropertyCacheLevel.None) - { - cacheLevel = PropertyType.CacheLevel; - referenceCacheLevel = cacheLevel; - } - else - { - cacheLevel = PropertyCacheLevel.Element; - referenceCacheLevel = ReferenceCacheLevel; - } + // cache within the snapshot cache + IAppCache? facadeCache = publishedSnapshot?.SnapshotCache; + cacheValues = (CacheValues?)facadeCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? + new CacheValues(); + break; + default: + throw new InvalidOperationException("Invalid cache level."); } - private IAppCache? GetSnapshotCache() + return cacheValues; + } + + private object? GetInterValue() + { + if (_interInitialized) { - // cache within the snapshot cache, unless previewing, then use the snapshot or - // elements cache (if we don't want to pollute the elements cache with short-lived - // data) depending on settings - // for members, always cache in the snapshot cache - never pollute elements cache - if (_publishedSnapshotAccessor is null) - { - return null; - } - - if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out var publishedSnapshot)) - { - return null; - } - - return (IsPreviewing == false || FullCacheWhenPreviewing) && IsMember == false - ? publishedSnapshot!.ElementsCache - : publishedSnapshot!.SnapshotCache; - } - - private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) - { - CacheValues cacheValues; - switch (cacheLevel) - { - case PropertyCacheLevel.None: - // never cache anything - cacheValues = new CacheValues(); - break; - case PropertyCacheLevel.Element: - // cache within the property object itself, ie within the content object - cacheValues = _cacheValues ?? (_cacheValues = new CacheValues()); - break; - case PropertyCacheLevel.Elements: - // cache within the elements cache, depending... - var snapshotCache = GetSnapshotCache(); - cacheValues = (CacheValues?) snapshotCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? new CacheValues(); - break; - case PropertyCacheLevel.Snapshot: - var publishedSnapshot = _publishedSnapshotAccessor?.GetRequiredPublishedSnapshot(); - // cache within the snapshot cache - var facadeCache = publishedSnapshot?.SnapshotCache; - cacheValues = (CacheValues?) facadeCache?.Get(ValuesCacheKey, () => new CacheValues()) ?? new CacheValues(); - break; - default: - throw new InvalidOperationException("Invalid cache level."); - } - return cacheValues; - } - - private object? GetInterValue() - { - if (_interInitialized) return _interValue; - - _interValue = PropertyType.ConvertSourceToInter(Element, _sourceValue, IsPreviewing); - _interInitialized = true; return _interValue; } - public override object? GetSourceValue(string? culture = null, string? segment = null) => _sourceValue; + _interValue = PropertyType.ConvertSourceToInter(Element, _sourceValue, IsPreviewing); + _interInitialized = true; + return _interValue; + } - public override object? GetValue(string? culture = null, string? segment = null) + public override object? GetValue(string? culture = null, string? segment = null) + { + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); + + lock (_locko) { - GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); - - lock (_locko) + CacheValues cacheValues = GetCacheValues(cacheLevel); + if (cacheValues.ObjectInitialized) { - var cacheValues = GetCacheValues(cacheLevel); - if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue; - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); - cacheValues.ObjectInitialized = true; return cacheValues.ObjectValue; } - } - public override object? GetXPathValue(string? culture = null, string? segment = null) - { - GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); - - lock (_locko) - { - var cacheValues = GetCacheValues(cacheLevel); - if (cacheValues.XPathInitialized) return cacheValues.XPathValue; - cacheValues.XPathValue = PropertyType.ConvertInterToXPath(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); - cacheValues.XPathInitialized = true; - return cacheValues.XPathValue; - } + cacheValues.ObjectValue = + PropertyType.ConvertInterToObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); + cacheValues.ObjectInitialized = true; + return cacheValues.ObjectValue; } } + + public override object? GetXPathValue(string? culture = null, string? segment = null) + { + GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); + + lock (_locko) + { + CacheValues cacheValues = GetCacheValues(cacheLevel); + if (cacheValues.XPathInitialized) + { + return cacheValues.XPathValue; + } + + cacheValues.XPathValue = + PropertyType.ConvertInterToXPath(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); + cacheValues.XPathInitialized = true; + return cacheValues.XPathValue; + } + } + + protected class CacheValues + { + public bool ObjectInitialized; + public object? ObjectValue; + public bool XPathInitialized; + public object? XPathValue; + } } diff --git a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs index 7f81d066f2..8f3e4fe827 100644 --- a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs @@ -1,46 +1,44 @@ -using System; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.PublishedCache +namespace Umbraco.Cms.Core.PublishedCache; + +// TODO: This is a mess. This is a circular reference: +// IPublishedSnapshotAccessor -> PublishedSnapshotService -> UmbracoContext -> PublishedSnapshotService -> IPublishedSnapshotAccessor +// Injecting IPublishedSnapshotAccessor into PublishedSnapshotService seems pretty strange +// The underlying reason for this mess is because IPublishedContent is both a service and a model. +// Until that is fixed, IPublishedContent will need to have a IPublishedSnapshotAccessor +public class UmbracoContextPublishedSnapshotAccessor : IPublishedSnapshotAccessor { - // TODO: This is a mess. This is a circular reference: - // IPublishedSnapshotAccessor -> PublishedSnapshotService -> UmbracoContext -> PublishedSnapshotService -> IPublishedSnapshotAccessor - // Injecting IPublishedSnapshotAccessor into PublishedSnapshotService seems pretty strange - // The underlying reason for this mess is because IPublishedContent is both a service and a model. - // Until that is fixed, IPublishedContent will need to have a IPublishedSnapshotAccessor - public class UmbracoContextPublishedSnapshotAccessor : IPublishedSnapshotAccessor + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public UmbracoContextPublishedSnapshotAccessor(IUmbracoContextAccessor umbracoContextAccessor) => + _umbracoContextAccessor = umbracoContextAccessor; + + public IPublishedSnapshot? PublishedSnapshot { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - - public UmbracoContextPublishedSnapshotAccessor(IUmbracoContextAccessor umbracoContextAccessor) + get { - _umbracoContextAccessor = umbracoContextAccessor; - } - - public IPublishedSnapshot? PublishedSnapshot - { - get + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return null; - } - return umbracoContext?.PublishedSnapshot; + return null; } - set => throw new NotSupportedException(); // not ok to set + return umbracoContext?.PublishedSnapshot; } - public bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) + set => throw new NotSupportedException(); // not ok to set + } + + public bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - publishedSnapshot = null; - return false; - } - publishedSnapshot = umbracoContext?.PublishedSnapshot; - - return publishedSnapshot is not null; + publishedSnapshot = null; + return false; } + + publishedSnapshot = umbracoContext?.PublishedSnapshot; + + return publishedSnapshot is not null; } } diff --git a/src/Umbraco.Core/ReflectionUtilities.cs b/src/Umbraco.Core/ReflectionUtilities.cs index 982e0835fb..a6c58466d2 100644 --- a/src/Umbraco.Core/ReflectionUtilities.cs +++ b/src/Umbraco.Core/ReflectionUtilities.cs @@ -1,919 +1,1187 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; +using System.Reflection; using System.Reflection.Emit; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Provides utilities to simplify reflection. +/// +/// +/// +/// Readings: +/// * CIL instructions: https://en.wikipedia.org/wiki/List_of_CIL_instructions +/// * ECMA 335: https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf +/// * MSIL programming: http://www.blackbeltcoder.com/Articles/net/msil-programming-part-1 +/// +/// +/// Supports emitting constructors, instance and static methods, instance property getters and +/// setters. Does not support static properties yet. +/// +/// +public static class ReflectionUtilities { + #region Fields + /// - /// Provides utilities to simplify reflection. + /// Emits a field getter. /// - /// - /// Readings: - /// * CIL instructions: https://en.wikipedia.org/wiki/List_of_CIL_instructions - /// * ECMA 335: https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf - /// * MSIL programming: http://www.blackbeltcoder.com/Articles/net/msil-programming-part-1 - /// - /// Supports emitting constructors, instance and static methods, instance property getters and - /// setters. Does not support static properties yet. - /// - public static class ReflectionUtilities + /// The declaring type. + /// The field type. + /// The name of the field. + /// + /// A field getter function. + /// + /// fieldName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . + /// type. + /// + /// + /// Could not find field . + /// . + /// + public static Func EmitFieldGetter(string fieldName) { - #region Fields - - /// - /// Emits a field getter. - /// - /// The declaring type. - /// The field type. - /// The name of the field. - /// - /// A field getter function. - /// - /// fieldName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match field . type. - /// Could not find field .. - public static Func EmitFieldGetter(string fieldName) - { - var field = GetField(fieldName); - return EmitFieldGetter(field); - } - - /// - /// Emits a field setter. - /// - /// The declaring type. - /// The field type. - /// The name of the field. - /// - /// A field setter action. - /// - /// fieldName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match field . type. - /// Could not find field .. - public static Action EmitFieldSetter(string fieldName) - { - var field = GetField(fieldName); - return EmitFieldSetter(field); - } - - /// - /// Emits a field getter and setter. - /// - /// The declaring type. - /// The field type. - /// The name of the field. - /// - /// A field getter and setter functions. - /// - /// fieldName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match field . type. - /// Could not find field .. - public static (Func, Action) EmitFieldGetterAndSetter(string fieldName) - { - var field = GetField(fieldName); - return (EmitFieldGetter(field), EmitFieldSetter(field)); - } - - /// - /// Gets the field. - /// - /// The type of the declaring. - /// The type of the value. - /// Name of the field. - /// - /// fieldName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match field . type. - /// Could not find field .. - private static FieldInfo GetField(string fieldName) - { - if (fieldName == null) throw new ArgumentNullException(nameof(fieldName)); - if (string.IsNullOrWhiteSpace(fieldName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(fieldName)); - - // get the field - var field = typeof(TDeclaring).GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (field == null) throw new InvalidOperationException($"Could not find field {typeof(TDeclaring)}.{fieldName}."); - - // validate field type - if (field.FieldType != typeof(TValue)) // strict - throw new ArgumentException($"Value type {typeof(TValue)} does not match field {typeof(TDeclaring)}.{fieldName} type {field.FieldType}."); - - return field; - } - - private static Func EmitFieldGetter(FieldInfo field) - { - // emit - var (dm, ilgen) = CreateIlGenerator(field.DeclaringType?.Module, new [] { typeof(TDeclaring) }, typeof(TValue)); - ilgen.Emit(OpCodes.Ldarg_0); - ilgen.Emit(OpCodes.Ldfld, field); - ilgen.Return(); - - return (Func) (object) dm.CreateDelegate(typeof(Func)); - } - - private static Action EmitFieldSetter(FieldInfo field) - { - // emit - var (dm, ilgen) = CreateIlGenerator(field.DeclaringType?.Module, new [] { typeof(TDeclaring), typeof(TValue) }, typeof(void)); - ilgen.Emit(OpCodes.Ldarg_0); - ilgen.Emit(OpCodes.Ldarg_1); - ilgen.Emit(OpCodes.Stfld, field); - ilgen.Return(); - - return (Action) (object) dm.CreateDelegate(typeof(Action)); - } - - #endregion - - #region Properties - - /// - /// Emits a property getter. - /// - /// The declaring type. - /// The property type. - /// The name of the property. - /// A value indicating whether the property and its getter must exist. - /// - /// A property getter function. If is false, returns null when the property or its getter does not exist. - /// - /// propertyName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match property . type. - /// Could not find property getter for .. - public static Func? EmitPropertyGetter(string propertyName, bool mustExist = true) - { - if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); - - var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - if (property?.GetMethod != null) - return EmitMethod>(property.GetMethod); - - if (!mustExist) - return default; - - throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}."); - } - - /// - /// Emits a property setter. - /// - /// The declaring type. - /// The property type. - /// The name of the property. - /// A value indicating whether the property and its setter must exist. - /// - /// A property setter function. If is false, returns null when the property or its setter does not exist. - /// - /// propertyName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match property . type. - /// Could not find property setter for .. - public static Action? EmitPropertySetter(string propertyName, bool mustExist = true) - { - if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); - - var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - if (property?.SetMethod != null) - return EmitMethod>(property.SetMethod); - - if (!mustExist) - return default; - - throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}."); - } - - /// - /// Emits a property getter and setter. - /// - /// The declaring type. - /// The property type. - /// The name of the property. - /// A value indicating whether the property and its getter and setter must exist. - /// - /// A property getter and setter functions. If is false, returns null when the property or its getter or setter does not exist. - /// - /// propertyName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Value type does not match property . type. - /// Could not find property getter and setter for .. - public static (Func, Action) EmitPropertyGetterAndSetter(string propertyName, bool mustExist = true) - { - if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); - if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); - - var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - if (property?.GetMethod != null && property.SetMethod != null) - return ( - EmitMethod>(property.GetMethod), - EmitMethod>(property.SetMethod)); - - if (!mustExist) - return default; - - throw new InvalidOperationException($"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}."); - } - - /// - /// Emits a property getter. - /// - /// The declaring type. - /// The property type. - /// The property info. - /// A property getter function. - /// Occurs when is null. - /// Occurs when the property has no getter. - /// Occurs when does not match the type of the property. - public static Func EmitPropertyGetter(PropertyInfo propertyInfo) - { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); - - if (propertyInfo.GetMethod == null) - throw new ArgumentException("Property has no getter.", nameof(propertyInfo)); - - return EmitMethod>(propertyInfo.GetMethod); - } - - /// - /// Emits a property setter. - /// - /// The declaring type. - /// The property type. - /// The property info. - /// A property setter function. - /// Occurs when is null. - /// Occurs when the property has no setter. - /// Occurs when does not match the type of the property. - public static Action EmitPropertySetter(PropertyInfo propertyInfo) - { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); - - if (propertyInfo.SetMethod == null) - throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); - - return EmitMethod>(propertyInfo.SetMethod); - } - - /// - /// Emits a property getter and setter. - /// - /// The declaring type. - /// The property type. - /// The property info. - /// A property getter and setter functions. - /// Occurs when is null. - /// Occurs when the property has no getter or no setter. - /// Occurs when does not match the type of the property. - public static (Func, Action) EmitPropertyGetterAndSetter(PropertyInfo propertyInfo) - { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); - - if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null) - throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo)); - - return ( - EmitMethod>(propertyInfo.GetMethod), - EmitMethod>(propertyInfo.SetMethod)); - } - - /// - /// Emits a property setter. - /// - /// The declaring type. - /// The property type. - /// The property info. - /// A property setter function. - /// Occurs when is null. - /// Occurs when the property has no setter. - /// Occurs when does not match the type of the property. - public static Action EmitPropertySetterUnsafe(PropertyInfo propertyInfo) - { - if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); - - if (propertyInfo.SetMethod == null) - throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); - - return EmitMethodUnsafe>(propertyInfo.SetMethod); - } - - #endregion - - #region Constructors - - /// - /// Emits a constructor. - /// - /// A lambda representing the constructor. - /// A value indicating whether the constructor must exist. - /// The optional type of the class to construct. - /// A constructor function. If is false, returns null when the constructor does not exist. - /// - /// When is not specified, it is the type returned by . - /// The constructor arguments are determined by generic arguments. - /// The type returned by does not need to be exactly , - /// when e.g. that type is not known at compile time, but it has to be a parent type (eg an interface, or object). - /// - /// Occurs when the constructor does not exist and is true. - /// Occurs when is not a Func or when - /// is specified and does not match the function's returned type. - public static TLambda? EmitConstructor(bool mustExist = true, Type? declaring = null) - { - var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); - - // determine returned / declaring type - if (declaring == null) declaring = lambdaReturned; - else if (!lambdaReturned.IsAssignableFrom(declaring)) - throw new ArgumentException($"Type {lambdaReturned} is not assignable from type {declaring}.", nameof(declaring)); - - // get the constructor infos - var ctor = declaring.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); - if (ctor == null) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable) lambdaParameters)})."); - } - - // emit - return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); - } - - /// - /// Emits a constructor. - /// - /// A lambda representing the constructor. - /// The constructor info. - /// A constructor function. - /// Occurs when is not a Func or when its generic - /// arguments do not match those of . - /// Occurs when is null. - public static TLambda EmitConstructor(ConstructorInfo ctor) - { - if (ctor == null) throw new ArgumentNullException(nameof(ctor)); - - var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); - - return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); - } - - private static TLambda EmitConstructorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor) - { - // get type and args - var ctorDeclaring = ctor.DeclaringType; - var ctorParameters = ctor.GetParameters().Select(x => x.ParameterType).ToArray(); - - // validate arguments - if (lambdaParameters.Length != ctorParameters.Length) - ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); - for (var i = 0; i < lambdaParameters.Length; i++) - if (lambdaParameters[i] != ctorParameters[i]) // note: relax the constraint with IsAssignableFrom? - ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); - if (!returned.IsAssignableFrom(ctorDeclaring)) - ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); - - // emit - return EmitConstructor(ctorDeclaring, ctorParameters, ctor); - } - - /// - /// Emits a constructor. - /// - /// A lambda representing the constructor. - /// The constructor info. - /// A constructor function. - /// - /// The constructor is emitted in an unsafe way, using the lambda arguments without verifying - /// them at all. This assumes that the calling code is taking care of all verifications, in order - /// to avoid cast errors. - /// - /// Occurs when is not a Func or when its generic - /// arguments do not match those of . - /// Occurs when is null. - public static TLambda EmitConstructorUnsafe(ConstructorInfo ctor) - { - if (ctor == null) throw new ArgumentNullException(nameof(ctor)); - - var (_, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, true); - - // emit - unsafe - use lambda's args and assume they are correct - return EmitConstructor(lambdaReturned, lambdaParameters, ctor); - } - - private static TLambda EmitConstructor(Type? declaring, Type[] lambdaParameters, ConstructorInfo ctor) - { - // gets the method argument types - var ctorParameters = GetParameters(ctor); - - // emit - var (dm, ilgen) = CreateIlGenerator(ctor.DeclaringType?.Module, lambdaParameters, declaring); - EmitLdargs(ilgen, lambdaParameters, ctorParameters); - ilgen.Emit(OpCodes.Newobj, ctor); // ok to just return, it's only objects - ilgen.Return(); - - return (TLambda) (object) dm.CreateDelegate(typeof(TLambda)); - } - - #endregion - - #region Methods - - /// - /// Emits a static method. - /// - /// The declaring type. - /// A lambda representing the method. - /// The name of the method. - /// A value indicating whether the constructor must exist. - /// - /// The method. If is false, returns null when the method does not exist. - /// - /// methodName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Occurs when does not match the method signature.. - /// Occurs when no proper method with name could be found. - /// - /// The method arguments are determined by generic arguments. - /// - public static TLambda? EmitMethod(string methodName, bool mustExist = true) - { - return EmitMethod(typeof(TDeclaring), methodName, mustExist); - } - - /// - /// Emits a static method. - /// - /// A lambda representing the method. - /// The declaring type. - /// The name of the method. - /// A value indicating whether the constructor must exist. - /// - /// The method. If is false, returns null when the method does not exist. - /// - /// methodName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Occurs when does not match the method signature.. - /// Occurs when no proper method with name could be found. - /// - /// The method arguments are determined by generic arguments. - /// - public static TLambda? EmitMethod(Type declaring, string methodName, bool mustExist = true) - { - if (methodName == null) throw new ArgumentNullException(nameof(methodName)); - if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); - - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(true, out var isFunction); - - // get the method infos - var method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, lambdaParameters, null); - if (method == null || isFunction && !lambdaReturned.IsAssignableFrom(method.ReturnType)) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find static method {declaring}.{methodName}({string.Join(", ", (IEnumerable) lambdaParameters)})."); - } - - // emit - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); - } - - /// - /// Emits a method. - /// - /// A lambda representing the method. - /// The method info. - /// The method. - /// Occurs when is null. - /// Occurs when Occurs when does not match the method signature. - public static TLambda EmitMethod(MethodInfo method) - { - if (method == null) throw new ArgumentNullException(nameof(method)); - - // get type and args - var methodDeclaring = method.DeclaringType; - var methodReturned = method.ReturnType; - var methodParameters = method.GetParameters().Select(x => x.ParameterType).ToArray(); - - var isStatic = method.IsStatic; - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(isStatic, out var isFunction); - - // if not static, then the first lambda arg must be the method declaring type - if (!isStatic && (methodDeclaring == null || !methodDeclaring.IsAssignableFrom(lambdaDeclaring))) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - - if (methodParameters.Length != lambdaParameters.Length) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - - for (var i = 0; i < methodParameters.Length; i++) - if (!methodParameters[i].IsAssignableFrom(lambdaParameters[i])) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - - // if it's a function then the last lambda arg must match the method returned type - if (isFunction && !lambdaReturned.IsAssignableFrom(methodReturned)) - ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - - // emit - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); - } - - /// - /// Emits a method. - /// - /// A lambda representing the method. - /// The method info. - /// The method. - /// Occurs when is null. - /// Occurs when Occurs when does not match the method signature. - public static TLambda EmitMethodUnsafe(MethodInfo method) - { - if (method == null) throw new ArgumentNullException(nameof(method)); - - var isStatic = method.IsStatic; - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(isStatic, out _); - - // emit - unsafe - use lambda's args and assume they are correct - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); - } - - /// - /// Emits an instance method. - /// - /// A lambda representing the method. - /// The name of the method. - /// A value indicating whether the constructor must exist. - /// - /// The method. If is false, returns null when the method does not exist. - /// - /// methodName - /// Value can't be empty or consist only of white-space characters. - - /// or - /// Occurs when does not match the method signature.. - /// Occurs when no proper method with name could be found. - /// - /// The method arguments are determined by generic arguments. - /// - public static TLambda? EmitMethod(string methodName, bool mustExist = true) - { - if (methodName == null) throw new ArgumentNullException(nameof(methodName)); - if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); - - // validate lambda type - var (lambdaDeclaring, lambdaParameters, lambdaReturned) = AnalyzeLambda(false, out var isFunction); - - // get the method infos - var method = lambdaDeclaring?.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); - if (method == null || isFunction && method.ReturnType != lambdaReturned) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find method {lambdaDeclaring}.{methodName}({string.Join(", ", (IEnumerable) lambdaParameters)})."); - } - - // emit - return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); - } - - // lambdaReturned = the lambda returned type (can be void) - // lambdaArgTypes = the lambda argument types - private static TLambda EmitMethod(Type? lambdaDeclaring, Type lambdaReturned, Type[] lambdaParameters, MethodInfo method) - { - // non-static methods need the declaring type as first arg - var parameters = lambdaParameters; - if (!method.IsStatic) - { - parameters = new Type[lambdaParameters.Length + 1]; - parameters[0] = lambdaDeclaring ?? method.DeclaringType!; - Array.Copy(lambdaParameters, 0, parameters, 1, lambdaParameters.Length); - } - - // gets the method argument types - var methodArgTypes = GetParameters(method, withDeclaring: !method.IsStatic); - - // emit IL - var (dm, ilgen) = CreateIlGenerator(method.DeclaringType?.Module, parameters, lambdaReturned); - EmitLdargs(ilgen, parameters, methodArgTypes); - ilgen.CallMethod(method); - EmitOutputAdapter(ilgen, lambdaReturned, method.ReturnType); - ilgen.Return(); - - // create - return (TLambda) (object) dm.CreateDelegate(typeof(TLambda)); - } - - #endregion - - #region Utilities - - // when !isStatic, the first generic argument of the lambda is the declaring type - // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument - // when isFunction, the last generic argument of the lambda is the returned type - // everything in between is parameters - private static (Type? Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, bool isFunction) - { - var typeLambda = typeof(TLambda); - - var (declaring, parameters, returned) = AnalyzeLambda(isStatic, out var maybeFunction); - - if (isFunction) - { - if (!maybeFunction) - throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda)); - } - else - { - if (maybeFunction) - throw new ArgumentException($"Lambda {typeLambda} is a Func, an Action was expected.", nameof(TLambda)); - } - - return (declaring, parameters, returned); - } - - // when !isStatic, the first generic argument of the lambda is the declaring type - // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument - // when isFunction, the last generic argument of the lambda is the returned type - // everything in between is parameters - private static (Type? Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, out bool isFunction) - { - isFunction = false; - - var typeLambda = typeof(TLambda); - - var isAction = typeLambda.FullName == "System.Action"; - if (isAction) - { - if (!isStatic) - throw new ArgumentException($"Lambda {typeLambda} is an Action and can be used for static methods exclusively.", nameof(TLambda)); - - return (null, Array.Empty(), typeof(void)); - } - - var genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null; - var name = genericDefinition?.FullName; - - if (name == null) - throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); - - var isActionOf = name.StartsWith("System.Action`"); - isFunction = name.StartsWith("System.Func`"); - - if (!isActionOf && !isFunction) - throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); - - var genericArgs = typeLambda.GetGenericArguments(); - if (genericArgs.Length == 0) - throw new Exception("Panic: Func<> or Action<> has zero generic arguments."); - - var i = 0; - var declaring = isStatic ? typeof(void) : genericArgs[i++]; - - var parameterCount = genericArgs.Length - (isStatic ? 0 : 1) - (isFunction ? 1 : 0); - if (parameterCount < 0) - throw new ArgumentException($"Lambda {typeLambda} is a Func and requires at least two arguments (declaring type and returned type).", nameof(TLambda)); - - var parameters = new Type[parameterCount]; - for (var j = 0; j < parameterCount; j++) - parameters[j] = genericArgs[i++]; - - var returned = isFunction ? genericArgs[i] : typeof(void); - - return (declaring, parameters, returned); - } - - private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module? module, Type[] arguments, Type? returned) - { - if (module == null) throw new ArgumentNullException(nameof(module)); - var dm = new DynamicMethod(string.Empty, returned, arguments, module, true); - return (dm, dm.GetILGenerator()); - } - - private static Type[] GetParameters(ConstructorInfo ctor) - { - var parameters = ctor.GetParameters(); - var types = new Type[parameters.Length]; - var i = 0; - foreach (var parameter in parameters) - types[i++] = parameter.ParameterType; - return types; - } - - private static Type[] GetParameters(MethodInfo method, bool withDeclaring) - { - var parameters = method.GetParameters(); - var types = new Type[parameters.Length + (withDeclaring ? 1 : 0)]; - var i = 0; - if (withDeclaring) - types[i++] = method.DeclaringType!; - foreach (var parameter in parameters) - types[i++] = parameter.ParameterType; - return types; - } - - // emits args - private static void EmitLdargs(ILGenerator ilgen, Type[] lambdaArgTypes, Type[] methodArgTypes) - { - var ldargOpCodes = new[] { OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3 }; - - if (lambdaArgTypes.Length != methodArgTypes.Length) - throw new Exception("Panic: inconsistent number of args."); - - for (var i = 0; i < lambdaArgTypes.Length; i++) - { - if (lambdaArgTypes.Length < 5) - ilgen.Emit(ldargOpCodes[i]); - else - ilgen.Emit(OpCodes.Ldarg, i); - - //var local = false; - EmitInputAdapter(ilgen, lambdaArgTypes[i], methodArgTypes[i]/*, ref local*/); - } - } - - // emits adapter opcodes after OpCodes.Ldarg - // inputType is the lambda input type - // methodParamType is the actual type expected by the actual method - // adding code to do inputType -> methodParamType - // valueType -> valueType : not supported ('cos, why?) - // valueType -> !valueType : not supported ('cos, why?) - // !valueType -> valueType : unbox and convert - // !valueType -> !valueType : cast (could throw) - private static void EmitInputAdapter(ILGenerator ilgen, Type inputType, Type methodParamType /*, ref bool local*/) - { - if (inputType == methodParamType) return; - - if (methodParamType.IsValueType) - { - if (inputType.IsValueType) - { - // both input and parameter are value types - // not supported, use proper input - // (otherwise, would require converting) - throw new NotSupportedException("ValueTypes conversion."); - } - - // parameter is value type, but input is reference type - // unbox the input to the parameter value type - // this is more or less equivalent to the ToT method below - - var unbox = ilgen.DefineLabel(); - - //if (!local) - //{ - // ilgen.DeclareLocal(typeof(object)); // declare local var for st/ld loc_0 - // local = true; - //} - - // stack: value - - // following code can be replaced with .Dump (and then we don't need the local variable anymore) - //ilgen.Emit(OpCodes.Stloc_0); // pop value into loc.0 - //// stack: - //ilgen.Emit(OpCodes.Ldloc_0); // push loc.0 - //ilgen.Emit(OpCodes.Ldloc_0); // push loc.0 - - ilgen.Emit(OpCodes.Dup); // duplicate top of stack - - // stack: value ; value - - ilgen.Emit(OpCodes.Isinst, methodParamType); // test, pops value, and pushes either a null ref, or an instance of the type - - // stack: inst|null ; value - - ilgen.Emit(OpCodes.Ldnull); // push null - - // stack: null ; inst|null ; value - - ilgen.Emit(OpCodes.Cgt_Un); // compare what isInst returned to null - pops 2 values, and pushes 1 if greater else 0 - - // stack: 0|1 ; value - - ilgen.Emit(OpCodes.Brtrue_S, unbox); // pops value, branches to unbox if true, ie nonzero - - // stack: value - - ilgen.Convert(methodParamType); // convert - - // stack: value|converted - - ilgen.MarkLabel(unbox); - ilgen.Emit(OpCodes.Unbox_Any, methodParamType); - } - else - { - // parameter is reference type, but input is value type - // not supported, input should always be less constrained - // (otherwise, would require boxing and converting) - if (inputType.IsValueType) - throw new NotSupportedException("ValueType boxing."); - - // both input and parameter are reference types - // cast the input to the parameter type - ilgen.Emit(OpCodes.Castclass, methodParamType); - } - } - - //private static T ToT(object o) - //{ - // return o is T t ? t : (T) System.Convert.ChangeType(o, typeof(T)); - //} - - private static MethodInfo? _convertMethod; - private static MethodInfo? _getTypeFromHandle; - - private static void Convert(this ILGenerator ilgen, Type type) - { - - if (_getTypeFromHandle == null) - _getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(RuntimeTypeHandle) }, null); - - if (_convertMethod == null) - _convertMethod = typeof(Convert).GetMethod("ChangeType", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(object), typeof(Type) }, null); - - ilgen.Emit(OpCodes.Ldtoken, type); - ilgen.CallMethod(_getTypeFromHandle); - ilgen.CallMethod(_convertMethod); - } - - // emits adapter code before OpCodes.Ret - // outputType is the lambda output type - // methodReturnedType is the actual type returned by the actual method - // adding code to do methodReturnedType -> outputType - // valueType -> valueType : not supported ('cos, why?) - // valueType -> !valueType : box - // !valueType -> valueType : not supported ('cos, why?) - // !valueType -> !valueType : implicit cast (could throw) - private static void EmitOutputAdapter(ILGenerator ilgen, Type outputType, Type methodReturnedType) - { - if (outputType == methodReturnedType) return; - - // note: the only important thing to support here, is returning a specific type - // as an object, when emitting the method as a Func<..., object> - anything else - // is pointless really - so we box value types, and ensure that non value types - // can be assigned - - if (methodReturnedType.IsValueType) - { - if (outputType.IsValueType) - { - // both returned and output are value types - // not supported, use proper output - // (otherwise, would require converting) - throw new NotSupportedException("ValueTypes conversion."); - } - - // returned is value type, but output is reference type - // box the returned value - ilgen.Emit(OpCodes.Box, methodReturnedType); - } - else - { - // returned is reference type, but output is value type - // not supported, output should always be less constrained - // (otherwise, would require boxing and converting) - if (outputType.IsValueType) - throw new NotSupportedException("ValueType boxing."); - - // both output and returned are reference types - // as long as returned can be assigned to output, good - if (!outputType.IsAssignableFrom(methodReturnedType)) - throw new NotSupportedException("Invalid cast."); - } - } - - private static void ThrowInvalidLambda(string methodName, Type? returned, Type[] args) - { - throw new ArgumentException($"Lambda {typeof(TLambda)} does not match {methodName}({string.Join(", ", (IEnumerable) args)}):{returned}.", nameof(TLambda)); - } - - private static void CallMethod(this ILGenerator ilgen, MethodInfo? method) - { - if (method is not null) - { - var virt = !method.IsStatic && (method.IsVirtual || !method.IsFinal); - ilgen.Emit(virt ? OpCodes.Callvirt : OpCodes.Call, method); - } - } - - private static void Return(this ILGenerator ilgen) - { - ilgen.Emit(OpCodes.Ret); - } - - #endregion + FieldInfo field = GetField(fieldName); + return EmitFieldGetter(field); } + + /// + /// Emits a field setter. + /// + /// The declaring type. + /// The field type. + /// The name of the field. + /// + /// A field setter action. + /// + /// fieldName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . + /// type. + /// + /// + /// Could not find field . + /// . + /// + public static Action EmitFieldSetter(string fieldName) + { + FieldInfo field = GetField(fieldName); + return EmitFieldSetter(field); + } + + /// + /// Emits a field getter and setter. + /// + /// The declaring type. + /// The field type. + /// The name of the field. + /// + /// A field getter and setter functions. + /// + /// fieldName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . + /// type. + /// + /// + /// Could not find field . + /// . + /// + public static (Func, Action) EmitFieldGetterAndSetter( + string fieldName) + { + FieldInfo field = GetField(fieldName); + return (EmitFieldGetter(field), EmitFieldSetter(field)); + } + + /// + /// Gets the field. + /// + /// The type of the declaring. + /// The type of the value. + /// Name of the field. + /// + /// fieldName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match field . + /// type. + /// + /// + /// Could not find field . + /// . + /// + private static FieldInfo GetField(string fieldName) + { + if (fieldName == null) + { + throw new ArgumentNullException(nameof(fieldName)); + } + + if (string.IsNullOrWhiteSpace(fieldName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(fieldName)); + } + + // get the field + FieldInfo? field = typeof(TDeclaring).GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field == null) + { + throw new InvalidOperationException($"Could not find field {typeof(TDeclaring)}.{fieldName}."); + } + + // validate field type + if (field.FieldType != typeof(TValue)) + { + throw new ArgumentException( + $"Value type {typeof(TValue)} does not match field {typeof(TDeclaring)}.{fieldName} type {field.FieldType}."); + } + + return field; + } + + private static Func EmitFieldGetter(FieldInfo field) + { + // emit + (DynamicMethod dm, ILGenerator ilgen) = + CreateIlGenerator(field.DeclaringType?.Module, new[] { typeof(TDeclaring) }, typeof(TValue)); + ilgen.Emit(OpCodes.Ldarg_0); + ilgen.Emit(OpCodes.Ldfld, field); + ilgen.Return(); + + return (Func)dm.CreateDelegate(typeof(Func)); + } + + private static Action EmitFieldSetter(FieldInfo field) + { + // emit + (DynamicMethod dm, ILGenerator ilgen) = CreateIlGenerator(field.DeclaringType?.Module, new[] { typeof(TDeclaring), typeof(TValue) }, typeof(void)); + ilgen.Emit(OpCodes.Ldarg_0); + ilgen.Emit(OpCodes.Ldarg_1); + ilgen.Emit(OpCodes.Stfld, field); + ilgen.Return(); + + return (Action)dm.CreateDelegate(typeof(Action)); + } + + #endregion + + #region Properties + + /// + /// Emits a property getter. + /// + /// The declaring type. + /// The property type. + /// The name of the property. + /// A value indicating whether the property and its getter must exist. + /// + /// A property getter function. If is false, returns null when the property or its + /// getter does not exist. + /// + /// propertyName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . + /// type. + /// + /// + /// Could not find property getter for . + /// . + /// + public static Func? EmitPropertyGetter(string propertyName, bool mustExist = true) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); + } + + PropertyInfo? property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (property?.GetMethod != null) + { + return EmitMethod>(property.GetMethod); + } + + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}."); + } + + /// + /// Emits a property setter. + /// + /// The declaring type. + /// The property type. + /// The name of the property. + /// A value indicating whether the property and its setter must exist. + /// + /// A property setter function. If is false, returns null when the property or its + /// setter does not exist. + /// + /// propertyName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . + /// type. + /// + /// + /// Could not find property setter for . + /// . + /// + public static Action? EmitPropertySetter(string propertyName, bool mustExist = true) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); + } + + PropertyInfo? property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (property?.SetMethod != null) + { + return EmitMethod>(property.SetMethod); + } + + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}."); + } + + /// + /// Emits a property getter and setter. + /// + /// The declaring type. + /// The property type. + /// The name of the property. + /// A value indicating whether the property and its getter and setter must exist. + /// + /// A property getter and setter functions. If is false, returns null when the + /// property or its getter or setter does not exist. + /// + /// propertyName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Value type does not match property . + /// type. + /// + /// + /// Could not find property getter and setter for + /// .. + /// + public static (Func, Action) + EmitPropertyGetterAndSetter(string propertyName, bool mustExist = true) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyName)); + } + + PropertyInfo? property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (property?.GetMethod != null && property.SetMethod != null) + { + return ( + EmitMethod>(property.GetMethod), + EmitMethod>(property.SetMethod)); + } + + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException( + $"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}."); + } + + /// + /// Emits a property getter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property getter function. + /// Occurs when is null. + /// Occurs when the property has no getter. + /// Occurs when does not match the type of the property. + public static Func EmitPropertyGetter(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (propertyInfo.GetMethod == null) + { + throw new ArgumentException("Property has no getter.", nameof(propertyInfo)); + } + + return EmitMethod>(propertyInfo.GetMethod); + } + + /// + /// Emits a property setter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property setter function. + /// Occurs when is null. + /// Occurs when the property has no setter. + /// Occurs when does not match the type of the property. + public static Action EmitPropertySetter(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); + } + + return EmitMethod>(propertyInfo.SetMethod); + } + + /// + /// Emits a property getter and setter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property getter and setter functions. + /// Occurs when is null. + /// Occurs when the property has no getter or no setter. + /// Occurs when does not match the type of the property. + public static (Func, Action) + EmitPropertyGetterAndSetter(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo)); + } + + return ( + EmitMethod>(propertyInfo.GetMethod), + EmitMethod>(propertyInfo.SetMethod)); + } + + /// + /// Emits a property setter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property setter function. + /// Occurs when is null. + /// Occurs when the property has no setter. + /// Occurs when does not match the type of the property. + public static Action EmitPropertySetterUnsafe(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (propertyInfo.SetMethod == null) + { + throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); + } + + return EmitMethodUnsafe>(propertyInfo.SetMethod); + } + + #endregion + + #region Constructors + + /// + /// Emits a constructor. + /// + /// A lambda representing the constructor. + /// A value indicating whether the constructor must exist. + /// The optional type of the class to construct. + /// + /// A constructor function. If is false, returns null when the constructor + /// does not exist. + /// + /// + /// + /// When is not specified, it is the type returned by + /// . + /// + /// The constructor arguments are determined by generic arguments. + /// + /// The type returned by does not need to be exactly , + /// when e.g. that type is not known at compile time, but it has to be a parent type (eg an interface, or + /// object). + /// + /// + /// + /// Occurs when the constructor does not exist and + /// is true. + /// + /// + /// Occurs when is not a Func or when + /// is specified and does not match the function's returned type. + /// + public static TLambda? EmitConstructor(bool mustExist = true, Type? declaring = null) + { + (_, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(true, true); + + // determine returned / declaring type + if (declaring == null) + { + declaring = lambdaReturned; + } + else if (!lambdaReturned.IsAssignableFrom(declaring)) + { + throw new ArgumentException($"Type {lambdaReturned} is not assignable from type {declaring}.", nameof(declaring)); + } + + // get the constructor infos + ConstructorInfo? ctor = declaring.GetConstructor( + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); + if (ctor == null) + { + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException( + $"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable)lambdaParameters)})."); + } + + // emit + return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); + } + + /// + /// Emits a constructor. + /// + /// A lambda representing the constructor. + /// The constructor info. + /// A constructor function. + /// + /// Occurs when is not a Func or when its generic + /// arguments do not match those of . + /// + /// Occurs when is null. + public static TLambda EmitConstructor(ConstructorInfo ctor) + { + if (ctor == null) + { + throw new ArgumentNullException(nameof(ctor)); + } + + (_, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(true, true); + + return EmitConstructorSafe(lambdaParameters, lambdaReturned, ctor); + } + + private static TLambda EmitConstructorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor) + { + // get type and args + Type? ctorDeclaring = ctor.DeclaringType; + Type[] ctorParameters = ctor.GetParameters().Select(x => x.ParameterType).ToArray(); + + // validate arguments + if (lambdaParameters.Length != ctorParameters.Length) + { + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + } + + for (var i = 0; i < lambdaParameters.Length; i++) + { + // note: relax the constraint with IsAssignableFrom? + if (lambdaParameters[i] != ctorParameters[i]) + { + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + } + } + + if (!returned.IsAssignableFrom(ctorDeclaring)) + { + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + } + + // emit + return EmitConstructor(ctorDeclaring, ctorParameters, ctor); + } + + /// + /// Emits a constructor. + /// + /// A lambda representing the constructor. + /// The constructor info. + /// A constructor function. + /// + /// + /// The constructor is emitted in an unsafe way, using the lambda arguments without verifying + /// them at all. This assumes that the calling code is taking care of all verifications, in order + /// to avoid cast errors. + /// + /// + /// + /// Occurs when is not a Func or when its generic + /// arguments do not match those of . + /// + /// Occurs when is null. + public static TLambda EmitConstructorUnsafe(ConstructorInfo ctor) + { + if (ctor == null) + { + throw new ArgumentNullException(nameof(ctor)); + } + + (_, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(true, true); + + // emit - unsafe - use lambda's args and assume they are correct + return EmitConstructor(lambdaReturned, lambdaParameters, ctor); + } + + private static TLambda EmitConstructor(Type? declaring, Type[] lambdaParameters, ConstructorInfo ctor) + { + // gets the method argument types + Type[] ctorParameters = GetParameters(ctor); + + // emit + (DynamicMethod dm, ILGenerator ilgen) = + CreateIlGenerator(ctor.DeclaringType?.Module, lambdaParameters, declaring); + EmitLdargs(ilgen, lambdaParameters, ctorParameters); + ilgen.Emit(OpCodes.Newobj, ctor); // ok to just return, it's only objects + ilgen.Return(); + + return (TLambda)(object)dm.CreateDelegate(typeof(TLambda)); + } + + #endregion + + #region Methods + + /// + /// Emits a static method. + /// + /// The declaring type. + /// A lambda representing the method. + /// The name of the method. + /// A value indicating whether the constructor must exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// + /// + /// Occurs when no proper method with name could + /// be found. + /// + /// + /// The method arguments are determined by generic arguments. + /// + public static TLambda? EmitMethod(string methodName, bool mustExist = true) => + EmitMethod(typeof(TDeclaring), methodName, mustExist); + + /// + /// Emits a static method. + /// + /// A lambda representing the method. + /// The declaring type. + /// The name of the method. + /// A value indicating whether the constructor must exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// + /// + /// Occurs when no proper method with name could + /// be found. + /// + /// + /// The method arguments are determined by generic arguments. + /// + public static TLambda? EmitMethod(Type declaring, string methodName, bool mustExist = true) + { + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + if (string.IsNullOrWhiteSpace(methodName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); + } + + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = + AnalyzeLambda(true, out var isFunction); + + // get the method infos + MethodInfo? method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, lambdaParameters, null); + if (method == null || (isFunction && !lambdaReturned.IsAssignableFrom(method.ReturnType))) + { + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException( + $"Could not find static method {declaring}.{methodName}({string.Join(", ", (IEnumerable)lambdaParameters)})."); + } + + // emit + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } + + /// + /// Emits a method. + /// + /// A lambda representing the method. + /// The method info. + /// The method. + /// Occurs when is null. + /// + /// Occurs when Occurs when does not match the method + /// signature. + /// + public static TLambda EmitMethod(MethodInfo method) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + // get type and args + Type? methodDeclaring = method.DeclaringType; + Type methodReturned = method.ReturnType; + Type[] methodParameters = method.GetParameters().Select(x => x.ParameterType).ToArray(); + + var isStatic = method.IsStatic; + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = + AnalyzeLambda(isStatic, out var isFunction); + + // if not static, then the first lambda arg must be the method declaring type + if (!isStatic && (methodDeclaring == null || !methodDeclaring.IsAssignableFrom(lambdaDeclaring))) + { + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } + + if (methodParameters.Length != lambdaParameters.Length) + { + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } + + for (var i = 0; i < methodParameters.Length; i++) + { + if (!methodParameters[i].IsAssignableFrom(lambdaParameters[i])) + { + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } + } + + // if it's a function then the last lambda arg must match the method returned type + if (isFunction && !lambdaReturned.IsAssignableFrom(methodReturned)) + { + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); + } + + // emit + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } + + /// + /// Emits a method. + /// + /// A lambda representing the method. + /// The method info. + /// The method. + /// Occurs when is null. + /// + /// Occurs when Occurs when does not match the method + /// signature. + /// + public static TLambda EmitMethodUnsafe(MethodInfo method) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + var isStatic = method.IsStatic; + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = AnalyzeLambda(isStatic, out _); + + // emit - unsafe - use lambda's args and assume they are correct + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } + + /// + /// Emits an instance method. + /// + /// A lambda representing the method. + /// The name of the method. + /// A value indicating whether the constructor must exist. + /// + /// The method. If is false, returns null when the method does not exist. + /// + /// methodName + /// + /// Value can't be empty or consist only of white-space characters. - + /// or + /// Occurs when does not match the method signature.. + /// + /// + /// Occurs when no proper method with name could + /// be found. + /// + /// + /// The method arguments are determined by generic arguments. + /// + public static TLambda? EmitMethod(string methodName, bool mustExist = true) + { + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + if (string.IsNullOrWhiteSpace(methodName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(methodName)); + } + + // validate lambda type + (Type? lambdaDeclaring, Type[] lambdaParameters, Type lambdaReturned) = + AnalyzeLambda(false, out var isFunction); + + // get the method infos + MethodInfo? method = lambdaDeclaring?.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); + if (method == null || (isFunction && method.ReturnType != lambdaReturned)) + { + if (!mustExist) + { + return default; + } + + throw new InvalidOperationException( + $"Could not find method {lambdaDeclaring}.{methodName}({string.Join(", ", (IEnumerable)lambdaParameters)})."); + } + + // emit + return EmitMethod(lambdaDeclaring, lambdaReturned, lambdaParameters, method); + } + + // lambdaReturned = the lambda returned type (can be void) + // lambdaArgTypes = the lambda argument types + private static TLambda EmitMethod(Type? lambdaDeclaring, Type lambdaReturned, Type[] lambdaParameters, MethodInfo method) + { + // non-static methods need the declaring type as first arg + Type[] parameters = lambdaParameters; + if (!method.IsStatic) + { + parameters = new Type[lambdaParameters.Length + 1]; + parameters[0] = lambdaDeclaring ?? method.DeclaringType!; + Array.Copy(lambdaParameters, 0, parameters, 1, lambdaParameters.Length); + } + + // gets the method argument types + Type[] methodArgTypes = GetParameters(method, !method.IsStatic); + + // emit IL + (DynamicMethod dm, ILGenerator ilgen) = + CreateIlGenerator(method.DeclaringType?.Module, parameters, lambdaReturned); + EmitLdargs(ilgen, parameters, methodArgTypes); + ilgen.CallMethod(method); + EmitOutputAdapter(ilgen, lambdaReturned, method.ReturnType); + ilgen.Return(); + + // create + return (TLambda)(object)dm.CreateDelegate(typeof(TLambda)); + } + + #endregion + + #region Utilities + + // when !isStatic, the first generic argument of the lambda is the declaring type + // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument + // when isFunction, the last generic argument of the lambda is the returned type + // everything in between is parameters + private static (Type? Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, bool isFunction) + { + Type typeLambda = typeof(TLambda); + + (Type? declaring, Type[] parameters, Type returned) = AnalyzeLambda(isStatic, out var maybeFunction); + + if (isFunction) + { + if (!maybeFunction) + { + throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda)); + } + } + else + { + if (maybeFunction) + { + throw new ArgumentException($"Lambda {typeLambda} is a Func, an Action was expected.", nameof(TLambda)); + } + } + + return (declaring, parameters, returned); + } + + // when !isStatic, the first generic argument of the lambda is the declaring type + // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument + // when isFunction, the last generic argument of the lambda is the returned type + // everything in between is parameters + private static (Type? Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, out bool isFunction) + { + isFunction = false; + + Type typeLambda = typeof(TLambda); + + var isAction = typeLambda.FullName == "System.Action"; + if (isAction) + { + if (!isStatic) + { + throw new ArgumentException( + $"Lambda {typeLambda} is an Action and can be used for static methods exclusively.", + nameof(TLambda)); + } + + return (null, Array.Empty(), typeof(void)); + } + + Type? genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null; + var name = genericDefinition?.FullName; + + if (name == null) + { + throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); + } + + var isActionOf = name.StartsWith("System.Action`"); + isFunction = name.StartsWith("System.Func`"); + + if (!isActionOf && !isFunction) + { + throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); + } + + Type[] genericArgs = typeLambda.GetGenericArguments(); + if (genericArgs.Length == 0) + { + throw new Exception("Panic: Func<> or Action<> has zero generic arguments."); + } + + var i = 0; + Type declaring = isStatic ? typeof(void) : genericArgs[i++]; + + var parameterCount = genericArgs.Length - (isStatic ? 0 : 1) - (isFunction ? 1 : 0); + if (parameterCount < 0) + { + throw new ArgumentException( + $"Lambda {typeLambda} is a Func and requires at least two arguments (declaring type and returned type).", + nameof(TLambda)); + } + + var parameters = new Type[parameterCount]; + for (var j = 0; j < parameterCount; j++) + { + parameters[j] = genericArgs[i++]; + } + + Type returned = isFunction ? genericArgs[i] : typeof(void); + + return (declaring, parameters, returned); + } + + private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module? module, Type[] arguments, Type? returned) + { + if (module == null) + { + throw new ArgumentNullException(nameof(module)); + } + + var dm = new DynamicMethod(string.Empty, returned, arguments, module, true); + return (dm, dm.GetILGenerator()); + } + + private static Type[] GetParameters(ConstructorInfo ctor) + { + ParameterInfo[] parameters = ctor.GetParameters(); + var types = new Type[parameters.Length]; + var i = 0; + foreach (ParameterInfo parameter in parameters) + { + types[i++] = parameter.ParameterType; + } + + return types; + } + + private static Type[] GetParameters(MethodInfo method, bool withDeclaring) + { + ParameterInfo[] parameters = method.GetParameters(); + var types = new Type[parameters.Length + (withDeclaring ? 1 : 0)]; + var i = 0; + if (withDeclaring) + { + types[i++] = method.DeclaringType!; + } + + foreach (ParameterInfo parameter in parameters) + { + types[i++] = parameter.ParameterType; + } + + return types; + } + + // emits args + private static void EmitLdargs(ILGenerator ilgen, Type[] lambdaArgTypes, Type[] methodArgTypes) + { + OpCode[] ldargOpCodes = new[] { OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3 }; + + if (lambdaArgTypes.Length != methodArgTypes.Length) + { + throw new Exception("Panic: inconsistent number of args."); + } + + for (var i = 0; i < lambdaArgTypes.Length; i++) + { + if (lambdaArgTypes.Length < 5) + { + ilgen.Emit(ldargOpCodes[i]); + } + else + { + ilgen.Emit(OpCodes.Ldarg, i); + } + + // var local = false; + EmitInputAdapter(ilgen, lambdaArgTypes[i], methodArgTypes[i] /*, ref local*/); + } + } + + // emits adapter opcodes after OpCodes.Ldarg + // inputType is the lambda input type + // methodParamType is the actual type expected by the actual method + // adding code to do inputType -> methodParamType + // valueType -> valueType : not supported ('cos, why?) + // valueType -> !valueType : not supported ('cos, why?) + // !valueType -> valueType : unbox and convert + // !valueType -> !valueType : cast (could throw) + private static void EmitInputAdapter(ILGenerator ilgen, Type inputType, Type methodParamType /*, ref bool local*/) + { + if (inputType == methodParamType) + { + return; + } + + if (methodParamType.IsValueType) + { + if (inputType.IsValueType) + { + // both input and parameter are value types + // not supported, use proper input + // (otherwise, would require converting) + throw new NotSupportedException("ValueTypes conversion."); + } + + // parameter is value type, but input is reference type + // unbox the input to the parameter value type + // this is more or less equivalent to the ToT method below + Label unbox = ilgen.DefineLabel(); + + // if (!local) + // { + // ilgen.DeclareLocal(typeof(object)); // declare local var for st/ld loc_0 + // local = true; + // } + + // stack: value + + // following code can be replaced with .Dump (and then we don't need the local variable anymore) + // ilgen.Emit(OpCodes.Stloc_0); // pop value into loc.0 + //// stack: + // ilgen.Emit(OpCodes.Ldloc_0); // push loc.0 + // ilgen.Emit(OpCodes.Ldloc_0); // push loc.0 + ilgen.Emit(OpCodes.Dup); // duplicate top of stack + + // stack: value ; value + ilgen.Emit(OpCodes.Isinst, methodParamType); // test, pops value, and pushes either a null ref, or an instance of the type + + // stack: inst|null ; value + ilgen.Emit(OpCodes.Ldnull); // push null + + // stack: null ; inst|null ; value + ilgen.Emit(OpCodes.Cgt_Un); // compare what isInst returned to null - pops 2 values, and pushes 1 if greater else 0 + + // stack: 0|1 ; value + ilgen.Emit(OpCodes.Brtrue_S, unbox); // pops value, branches to unbox if true, ie nonzero + + // stack: value + ilgen.Convert(methodParamType); // convert + + // stack: value|converted + ilgen.MarkLabel(unbox); + ilgen.Emit(OpCodes.Unbox_Any, methodParamType); + } + else + { + // parameter is reference type, but input is value type + // not supported, input should always be less constrained + // (otherwise, would require boxing and converting) + if (inputType.IsValueType) + { + throw new NotSupportedException("ValueType boxing."); + } + + // both input and parameter are reference types + // cast the input to the parameter type + ilgen.Emit(OpCodes.Castclass, methodParamType); + } + } + + // private static T ToT(object o) + // { + // return o is T t ? t : (T) System.Convert.ChangeType(o, typeof(T)); + // } + private static MethodInfo? _convertMethod; + private static MethodInfo? _getTypeFromHandle; + + private static void Convert(this ILGenerator ilgen, Type type) + { + if (_getTypeFromHandle == null) + { + _getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(RuntimeTypeHandle) }, null); + } + + if (_convertMethod == null) + { + _convertMethod = typeof(Convert).GetMethod("ChangeType", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(object), typeof(Type) }, null); + } + + ilgen.Emit(OpCodes.Ldtoken, type); + ilgen.CallMethod(_getTypeFromHandle); + ilgen.CallMethod(_convertMethod); + } + + // emits adapter code before OpCodes.Ret + // outputType is the lambda output type + // methodReturnedType is the actual type returned by the actual method + // adding code to do methodReturnedType -> outputType + // valueType -> valueType : not supported ('cos, why?) + // valueType -> !valueType : box + // !valueType -> valueType : not supported ('cos, why?) + // !valueType -> !valueType : implicit cast (could throw) + private static void EmitOutputAdapter(ILGenerator ilgen, Type outputType, Type methodReturnedType) + { + if (outputType == methodReturnedType) + { + return; + } + + // note: the only important thing to support here, is returning a specific type + // as an object, when emitting the method as a Func<..., object> - anything else + // is pointless really - so we box value types, and ensure that non value types + // can be assigned + if (methodReturnedType.IsValueType) + { + if (outputType.IsValueType) + { + // both returned and output are value types + // not supported, use proper output + // (otherwise, would require converting) + throw new NotSupportedException("ValueTypes conversion."); + } + + // returned is value type, but output is reference type + // box the returned value + ilgen.Emit(OpCodes.Box, methodReturnedType); + } + else + { + // returned is reference type, but output is value type + // not supported, output should always be less constrained + // (otherwise, would require boxing and converting) + if (outputType.IsValueType) + { + throw new NotSupportedException("ValueType boxing."); + } + + // both output and returned are reference types + // as long as returned can be assigned to output, good + if (!outputType.IsAssignableFrom(methodReturnedType)) + { + throw new NotSupportedException("Invalid cast."); + } + } + } + + private static void ThrowInvalidLambda(string methodName, Type? returned, Type[] args) => + throw new ArgumentException( + $"Lambda {typeof(TLambda)} does not match {methodName}({string.Join(", ", (IEnumerable)args)}):{returned}.", + nameof(TLambda)); + + private static void CallMethod(this ILGenerator ilgen, MethodInfo? method) + { + if (method is not null) + { + var virt = !method.IsStatic && (method.IsVirtual || !method.IsFinal); + ilgen.Emit(virt ? OpCodes.Callvirt : OpCodes.Call, method); + } + } + + private static void Return(this ILGenerator ilgen) => ilgen.Emit(OpCodes.Ret); + + #endregion } diff --git a/src/Umbraco.Core/Routing/AliasUrlProvider.cs b/src/Umbraco.Core/Routing/AliasUrlProvider.cs index 21fb3e9832..d47680905a 100644 --- a/src/Umbraco.Core/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Core/Routing/AliasUrlProvider.cs @@ -1,149 +1,167 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides URLs using the umbracoUrlAlias property. +/// +public class AliasUrlProvider : IUrlProvider { - /// - /// Provides URLs using the umbracoUrlAlias property. - /// - public class AliasUrlProvider : IUrlProvider + private readonly IPublishedValueFallback _publishedValueFallback; + private readonly ISiteDomainMapper _siteDomainMapper; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly UriUtility _uriUtility; + private RequestHandlerSettings _requestConfig; + + public AliasUrlProvider( + IOptionsMonitor requestConfig, + ISiteDomainMapper siteDomainMapper, + UriUtility uriUtility, + IPublishedValueFallback publishedValueFallback, + IUmbracoContextAccessor umbracoContextAccessor) { - private RequestHandlerSettings _requestConfig; - private readonly ISiteDomainMapper _siteDomainMapper; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly UriUtility _uriUtility; - private readonly IPublishedValueFallback _publishedValueFallback; + _requestConfig = requestConfig.CurrentValue; + _siteDomainMapper = siteDomainMapper; + _uriUtility = uriUtility; + _publishedValueFallback = publishedValueFallback; + _umbracoContextAccessor = umbracoContextAccessor; - public AliasUrlProvider(IOptionsMonitor requestConfig, ISiteDomainMapper siteDomainMapper, UriUtility uriUtility, IPublishedValueFallback publishedValueFallback, IUmbracoContextAccessor umbracoContextAccessor) + requestConfig.OnChange(x => _requestConfig = x); + } + + // note - at the moment we seem to accept pretty much anything as an alias + // without any form of validation ... could even prob. kill the XPath ... + // ok, this is somewhat experimental and is NOT enabled by default + #region GetUrl + + /// + public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) => + null; // we have nothing to say + + #endregion + + #region GetOtherUrls + + /// + /// Gets the other URLs of a published content. + /// + /// The published content id. + /// The current absolute URL. + /// The other URLs for the published content. + /// + /// + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + public IEnumerable GetOtherUrls(int id, Uri current) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContent? node = umbracoContext.Content?.GetById(id); + if (node == null) { - _requestConfig = requestConfig.CurrentValue; - _siteDomainMapper = siteDomainMapper; - _uriUtility = uriUtility; - _publishedValueFallback = publishedValueFallback; - _umbracoContextAccessor = umbracoContextAccessor; - - requestConfig.OnChange(x => _requestConfig = x); + yield break; } - // note - at the moment we seem to accept pretty much anything as an alias - // without any form of validation ... could even prob. kill the XPath ... - // ok, this is somewhat experimental and is NOT enabled by default - - #region GetUrl - - /// - public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + if (!node.HasProperty(Constants.Conventions.Content.UrlAlias)) { - return null; // we have nothing to say + yield break; } - #endregion + // look for domains, walking up the tree + IPublishedContent? n = node; + IEnumerable? domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); - #region GetOtherUrls - - /// - /// Gets the other URLs of a published content. - /// - /// The Umbraco context. - /// The published content id. - /// The current absolute URL. - /// The other URLs for the published content. - /// - /// Other URLs are those that GetUrl would not return in the current context, but would be valid - /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// - public IEnumerable GetOtherUrls(int id, Uri current) + // n is null at root + while (domainUris == null && n != null) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var node = umbracoContext.Content?.GetById(id); - if (node == null) - yield break; + // move to parent node + n = n.Parent; + domainUris = n == null + ? null + : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); + } - if (!node.HasProperty(Constants.Conventions.Content.UrlAlias)) - yield break; + // determine whether the alias property varies + var varies = node.GetProperty(Constants.Conventions.Content.UrlAlias)!.PropertyType.VariesByCulture(); - // look for domains, walking up the tree - var n = node; - var domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); - while (domainUris == null && n != null) // n is null at root + if (domainUris == null) + { + // no domain + // if the property is invariant, then URL "/" is ok + // if the property varies, then what are we supposed to do? + // the content finder may work, depending on the 'current' culture, + // but there's no way we can return something meaningful here + if (varies) { - // move to parent node - n = n.Parent; - domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, excludeDefault: false); + yield break; } - // determine whether the alias property varies - var varies = node.GetProperty(Constants.Conventions.Content.UrlAlias)!.PropertyType.VariesByCulture(); + var umbracoUrlName = node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); + var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (domainUris == null) + if (aliases == null || aliases.Any() == false) { - // no domain - // if the property is invariant, then URL "/" is ok - // if the property varies, then what are we supposed to do? - // the content finder may work, depending on the 'current' culture, - // but there's no way we can return something meaningful here - if (varies) - yield break; + yield break; + } + + foreach (var alias in aliases.Distinct()) + { + var path = "/" + alias; + var uri = new Uri(path, UriKind.Relative); + yield return UrlInfo.Url(_uriUtility.UriFromUmbraco(uri, _requestConfig).ToString()); + } + } + else + { + // some domains: one URL per domain, which is "/" + foreach (DomainAndUri domainUri in domainUris) + { + // if the property is invariant, get the invariant value, URL is "/" + // if the property varies, get the variant value, URL is "/" + + // but! only if the culture is published, else ignore + if (varies && !node.HasCulture(domainUri.Culture)) + { + continue; + } + + var umbracoUrlName = varies + ? node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias, domainUri.Culture) + : node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); - var umbracoUrlName = node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); if (aliases == null || aliases.Any() == false) - yield break; + { + continue; + } foreach (var alias in aliases.Distinct()) { var path = "/" + alias; - var uri = new Uri(path, UriKind.Relative); - yield return UrlInfo.Url(_uriUtility.UriFromUmbraco(uri, _requestConfig).ToString()); - } - } - else - { - // some domains: one URL per domain, which is "/" - foreach (var domainUri in domainUris) - { - // if the property is invariant, get the invariant value, URL is "/" - // if the property varies, get the variant value, URL is "/" - - // but! only if the culture is published, else ignore - if (varies && !node.HasCulture(domainUri.Culture)) continue; - - var umbracoUrlName = varies - ? node.Value(_publishedValueFallback,Constants.Conventions.Content.UrlAlias, culture: domainUri.Culture) - : node.Value(_publishedValueFallback, Constants.Conventions.Content.UrlAlias); - - var aliases = umbracoUrlName?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - - if (aliases == null || aliases.Any() == false) - continue; - - foreach(var alias in aliases.Distinct()) - { - var path = "/" + alias; - var uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); - yield return UrlInfo.Url(_uriUtility.UriFromUmbraco(uri, _requestConfig).ToString(), domainUri.Culture); - } + var uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); + yield return UrlInfo.Url( + _uriUtility.UriFromUmbraco(uri, _requestConfig).ToString(), + domainUri.Culture); } } } - - #endregion - - #region Utilities - - string CombinePaths(string path1, string path2) - { - string path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; - return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); - } - - #endregion } + + #endregion + + #region Utilities + + private string CombinePaths(string path1, string path2) + { + var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; + return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); + } + + #endregion } diff --git a/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs index 380d7459ed..c7089f0824 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs @@ -1,116 +1,117 @@ using System.Globalization; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides an implementation of that handles page identifiers. +/// +/// +/// Handles /1234 where 1234 is the identified of a document. +/// +public class ContentFinderByIdPath : IContentFinder { + private readonly ILogger _logger; + private readonly IRequestAccessor _requestAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private WebRoutingSettings _webRoutingSettings; + /// - /// Provides an implementation of that handles page identifiers. + /// Initializes a new instance of the class. /// - /// - /// Handles /1234 where 1234 is the identified of a document. - /// - public class ContentFinderByIdPath : IContentFinder + public ContentFinderByIdPath( + IOptionsMonitor webRoutingSettings, + ILogger logger, + IRequestAccessor requestAccessor, + IUmbracoContextAccessor umbracoContextAccessor) { - private readonly ILogger _logger; - private readonly IRequestAccessor _requestAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private WebRoutingSettings _webRoutingSettings; + _webRoutingSettings = webRoutingSettings.CurrentValue ?? + throw new ArgumentNullException(nameof(webRoutingSettings)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(requestAccessor)); + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByIdPath( - IOptionsMonitor webRoutingSettings, - ILogger logger, - IRequestAccessor requestAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - _webRoutingSettings = webRoutingSettings.CurrentValue ?? throw new System.ArgumentNullException(nameof(webRoutingSettings)); - _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); - _requestAccessor = requestAccessor ?? throw new System.ArgumentNullException(nameof(requestAccessor)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); - - webRoutingSettings.OnChange(x => _webRoutingSettings = x); + return Task.FromResult(false); } - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - public async Task TryFindContent(IPublishedRequestBuilder frequest) + if (umbracoContext.InPreviewMode == false && _webRoutingSettings.DisableFindContentByIdPath) { - if(!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) + return Task.FromResult(false); + } + + IPublishedContent? node = null; + var path = frequest.AbsolutePathDecoded; + + var nodeId = -1; + + // no id if "/" + if (path != "/") + { + var noSlashPath = path.Substring(1); + + if (int.TryParse(noSlashPath, NumberStyles.Integer, CultureInfo.InvariantCulture, out nodeId) == false) { - return false; - } - if (umbracoContext == null || (umbracoContext != null && umbracoContext.InPreviewMode == false && _webRoutingSettings.DisableFindContentByIdPath)) - { - return false; + nodeId = -1; } - IPublishedContent? node = null; - var path = frequest.AbsolutePathDecoded; - - var nodeId = -1; - - // no id if "/" - if (path != "/") - { - var noSlashPath = path.Substring(1); - - if (int.TryParse(noSlashPath, NumberStyles.Integer, CultureInfo.InvariantCulture, out nodeId) == false) - { - nodeId = -1; - } - - if (nodeId > 0) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Id={NodeId}", nodeId); - } - node = umbracoContext?.Content?.GetById(nodeId); - - if (node != null) - { - - var cultureFromQuerystring = _requestAccessor.GetQueryStringValue("culture"); - - // if we have a node, check if we have a culture in the query string - if (!string.IsNullOrEmpty(cultureFromQuerystring)) - { - // we're assuming it will match a culture, if an invalid one is passed in, an exception will throw (there is no TryGetCultureInfo method), i think this is ok though - frequest.SetCulture(cultureFromQuerystring); - } - - frequest.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Found node with id={PublishedContentId}", node.Id); - } - } - else - { - nodeId = -1; // trigger message below - } - } - } - - if (nodeId == -1) + if (nodeId > 0) { if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Not a node id"); + _logger.LogDebug("Id={NodeId}", nodeId); + } + + node = umbracoContext.Content?.GetById(nodeId); + + if (node != null) + { + var cultureFromQuerystring = _requestAccessor.GetQueryStringValue("culture"); + + // if we have a node, check if we have a culture in the query string + if (!string.IsNullOrEmpty(cultureFromQuerystring)) + { + // we're assuming it will match a culture, if an invalid one is passed in, an exception will throw (there is no TryGetCultureInfo method), i think this is ok though + frequest.SetCulture(cultureFromQuerystring); + } + + frequest.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Found node with id={PublishedContentId}", node.Id); + } + } + else + { + nodeId = -1; // trigger message below } } - - return node != null; } + + if (nodeId == -1) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Not a node id"); + } + } + + return Task.FromResult(node != null); } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByPageIdQuery.cs b/src/Umbraco.Core/Routing/ContentFinderByPageIdQuery.cs index 646d091ebb..7721551777 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByPageIdQuery.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByPageIdQuery.cs @@ -1,51 +1,50 @@ using System.Globalization; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// This looks up a document by checking for the umbPageId of a request/query string +/// +/// +/// This is used by library.RenderTemplate and also some of the macro rendering functionality like in +/// macroResultWrapper.aspx +/// +public class ContentFinderByPageIdQuery : IContentFinder { + private readonly IRequestAccessor _requestAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + /// - /// This looks up a document by checking for the umbPageId of a request/query string + /// Initializes a new instance of the class. /// - /// - /// This is used by library.RenderTemplate and also some of the macro rendering functionality like in - /// macroResultWrapper.aspx - /// - public class ContentFinderByPageIdQuery : IContentFinder + public ContentFinderByPageIdQuery(IRequestAccessor requestAccessor, IUmbracoContextAccessor umbracoContextAccessor) { - private readonly IRequestAccessor _requestAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + _requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(requestAccessor)); + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + } - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByPageIdQuery(IRequestAccessor requestAccessor, IUmbracoContextAccessor umbracoContextAccessor) + /// + public Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - _requestAccessor = requestAccessor ?? throw new System.ArgumentNullException(nameof(requestAccessor)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + return Task.FromResult(false); } - /// - public async Task TryFindContent(IPublishedRequestBuilder frequest) + if (int.TryParse(_requestAccessor.GetRequestValue("umbPageID"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var pageId)) { - if(!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return false; - } - if (int.TryParse(_requestAccessor.GetRequestValue("umbPageID"), NumberStyles.Integer, CultureInfo.InvariantCulture, out int pageId)) - { - IPublishedContent? doc = umbracoContext.Content?.GetById(pageId); + IPublishedContent? doc = umbracoContext.Content?.GetById(pageId); - if (doc != null) - { - frequest.SetPublishedContent(doc); - return true; - } + if (doc != null) + { + frequest.SetPublishedContent(doc); + return Task.FromResult(true); } - - return false; } + + return Task.FromResult(false); } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs index a200afec67..99103d8cb6 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -7,95 +5,100 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing -{ - /// - /// Provides an implementation of that handles page URL rewrites - /// that are stored when moving, saving, or deleting a node. - /// - /// - /// Assigns a permanent redirect notification to the request. - /// - public class ContentFinderByRedirectUrl : IContentFinder - { - private readonly IRedirectUrlService _redirectUrlService; - private readonly ILogger _logger; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; +namespace Umbraco.Cms.Core.Routing; - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByRedirectUrl( - IRedirectUrlService redirectUrlService, - ILogger logger, - IPublishedUrlProvider publishedUrlProvider, - IUmbracoContextAccessor umbracoContextAccessor) +/// +/// Provides an implementation of that handles page URL rewrites +/// that are stored when moving, saving, or deleting a node. +/// +/// +/// Assigns a permanent redirect notification to the request. +/// +public class ContentFinderByRedirectUrl : IContentFinder +{ + private readonly ILogger _logger; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IRedirectUrlService _redirectUrlService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + public ContentFinderByRedirectUrl( + IRedirectUrlService redirectUrlService, + ILogger logger, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoContextAccessor umbracoContextAccessor) + { + _redirectUrlService = redirectUrlService; + _logger = logger; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + /// + /// Optionally, can also assign the template or anything else on the document request, although that is not + /// required. + /// + public Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - _redirectUrlService = redirectUrlService; - _logger = logger; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; + return Task.FromResult(false); } - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - /// Optionally, can also assign the template or anything else on the document request, although that is not required. - public async Task TryFindContent(IPublishedRequestBuilder frequest) + var route = frequest.Domain != null + ? frequest.Domain.ContentId + + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded) + : frequest.AbsolutePathDecoded; + + IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(route, frequest.Culture); + + if (redirectUrl == null) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return false; - } - - var route = frequest.Domain != null - ? frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded) - : frequest.AbsolutePathDecoded; - - IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(route, frequest.Culture); - - if (redirectUrl == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No match for route: {Route}", route); - } - return false; - } - - IPublishedContent? content = umbracoContext.Content?.GetById(redirectUrl.ContentId); - var url = content == null ? "#" : content.Url(_publishedUrlProvider, redirectUrl.Culture); - if (url.StartsWith("#")) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Route {Route} matches content {ContentId} which has no URL.", route, redirectUrl.ContentId); - } - return false; - } - - // Appending any querystring from the incoming request to the redirect URL - url = string.IsNullOrEmpty(frequest.Uri.Query) ? url : url + frequest.Uri.Query; if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Route {Route} matches content {ContentId} with URL '{Url}', redirecting.", route, content?.Id, url); + _logger.LogDebug("No match for route: {Route}", route); } - frequest - .SetRedirectPermanent(url) - - // From: http://stackoverflow.com/a/22468386/5018 - // See http://issues.umbraco.org/issue/U4-8361#comment=67-30532 - // Setting automatic 301 redirects to not be cached because browsers cache these very aggressively which then leads - // to problems if you rename a page back to it's original name or create a new page with the original name - .SetNoCacheHeader(true) - .SetCacheExtensions(new List { "no-store, must-revalidate" }) - .SetHeaders(new Dictionary { { "Pragma", "no-cache" }, { "Expires", "0" } }); - - return true; + return Task.FromResult(false); } + + IPublishedContent? content = umbracoContext.Content?.GetById(redirectUrl.ContentId); + var url = content == null ? "#" : content.Url(_publishedUrlProvider, redirectUrl.Culture); + if (url.StartsWith("#")) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Route {Route} matches content {ContentId} which has no URL.", route, redirectUrl.ContentId); + } + + return Task.FromResult(false); + } + + // Appending any querystring from the incoming request to the redirect URL + url = string.IsNullOrEmpty(frequest.Uri.Query) ? url : url + frequest.Uri.Query; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Route {Route} matches content {ContentId} with URL '{Url}', redirecting.", route, content?.Id, url); + } + + frequest + .SetRedirectPermanent(url) + + // From: http://stackoverflow.com/a/22468386/5018 + // See http://issues.umbraco.org/issue/U4-8361#comment=67-30532 + // Setting automatic 301 redirects to not be cached because browsers cache these very aggressively which then leads + // to problems if you rename a page back to it's original name or create a new page with the original name + .SetNoCacheHeader(true) + .SetCacheExtensions(new List { "no-store, must-revalidate" }) + .SetHeaders(new Dictionary { { "Pragma", "no-cache" }, { "Expires", "0" } }); + + return Task.FromResult(true); } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrl.cs b/src/Umbraco.Core/Routing/ContentFinderByUrl.cs index e95a036215..d2b2a564a7 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByUrl.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByUrl.cs @@ -1,98 +1,100 @@ -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides an implementation of that handles page nice URLs. +/// +/// +/// Handles /foo/bar where /foo/bar is the nice URL of a document. +/// +public class ContentFinderByUrl : IContentFinder { + private readonly ILogger _logger; + /// - /// Provides an implementation of that handles page nice URLs. + /// Initializes a new instance of the class. /// - /// - /// Handles /foo/bar where /foo/bar is the nice URL of a document. - /// - public class ContentFinderByUrl : IContentFinder + public ContentFinderByUrl(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) { - private readonly ILogger _logger; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + UmbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + } - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrl(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) + /// + /// Gets the + /// + protected IUmbracoContextAccessor UmbracoContextAccessor { get; } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public virtual Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? _)) { - _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); - UmbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); + return Task.FromResult(false); } - /// - /// Gets the - /// - protected IUmbracoContextAccessor UmbracoContextAccessor { get; } - - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - public virtual async Task TryFindContent(IPublishedRequestBuilder frequest) + string route; + if (frequest.Domain != null) { - if (!UmbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return false; - } - - string route; - if (frequest.Domain != null) - { - route = frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded); - } - else - { - route = frequest.AbsolutePathDecoded; - } - - IPublishedContent? node = FindContent(frequest, route); - return node != null; + route = frequest.Domain.ContentId + + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded); + } + else + { + route = frequest.AbsolutePathDecoded; } - /// - /// Tries to find an Umbraco document for a PublishedRequest and a route. - /// - /// The document node, or null. - protected IPublishedContent? FindContent(IPublishedRequestBuilder docreq, string route) - { - if (!UmbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - return null; - } + IPublishedContent? node = FindContent(frequest, route); + return Task.FromResult(node != null); + } - if (docreq == null) - { - throw new System.ArgumentNullException(nameof(docreq)); - } + /// + /// Tries to find an Umbraco document for a PublishedRequest and a route. + /// + /// The document node, or null. + protected IPublishedContent? FindContent(IPublishedRequestBuilder docreq, string route) + { + if (!UmbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) + { + return null; + } + + if (docreq == null) + { + throw new ArgumentNullException(nameof(docreq)); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Test route {Route}", route); + } + + IPublishedContent? node = + umbracoContext.Content?.GetByRoute(umbracoContext.InPreviewMode, route, culture: docreq.Culture); + if (node != null) + { + docreq.SetPublishedContent(node); if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Test route {Route}", route); + _logger.LogDebug("Got content, id={NodeId}", node.Id); } - - IPublishedContent? node = umbracoContext.Content?.GetByRoute(umbracoContext.InPreviewMode, route, culture: docreq.Culture); - if (node != null) - { - docreq.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Got content, id={NodeId}", node.Id); - } - } - else - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No match."); - } - } - - return node; } + else + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("No match."); + } + } + + return node; } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs index 5a8f6e16fe..3a04c2cb5b 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs @@ -1,157 +1,159 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing -{ - /// - /// Provides an implementation of that handles page aliases. - /// - /// - /// Handles /just/about/anything where /just/about/anything is contained in the umbracoUrlAlias property of a document. - /// The alias is the full path to the document. There can be more than one alias, separated by commas. - /// - public class ContentFinderByUrlAlias : IContentFinder - { - private readonly IPublishedValueFallback _publishedValueFallback; - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ILogger _logger; +namespace Umbraco.Cms.Core.Routing; - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrlAlias( - ILogger logger, - IPublishedValueFallback publishedValueFallback, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) +/// +/// Provides an implementation of that handles page aliases. +/// +/// +/// +/// Handles /just/about/anything where /just/about/anything is contained in the +/// umbracoUrlAlias property of a document. +/// +/// The alias is the full path to the document. There can be more than one alias, separated by commas. +/// +public class ContentFinderByUrlAlias : IContentFinder +{ + private readonly ILogger _logger; + private readonly IPublishedValueFallback _publishedValueFallback; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IVariationContextAccessor _variationContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + public ContentFinderByUrlAlias( + ILogger logger, + IPublishedValueFallback publishedValueFallback, + IVariationContextAccessor variationContextAccessor, + IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedValueFallback = publishedValueFallback; + _variationContextAccessor = variationContextAccessor; + _umbracoContextAccessor = umbracoContextAccessor; + _logger = logger; + } + + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + public Task TryFindContent(IPublishedRequestBuilder frequest) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - _publishedValueFallback = publishedValueFallback; - _variationContextAccessor = variationContextAccessor; - _umbracoContextAccessor = umbracoContextAccessor; - _logger = logger; + return Task.FromResult(false); } - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - public async Task TryFindContent(IPublishedRequestBuilder frequest) + IPublishedContent? node = null; + + // no alias if "/" + if (frequest.Uri.AbsolutePath != "/") { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) + node = FindContentByAlias( + umbracoContext.Content, + frequest.Domain != null ? frequest.Domain.ContentId : 0, + frequest.Culture, + frequest.AbsolutePathDecoded); + + if (node != null) + { + frequest.SetPublishedContent(node); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, node.Id); + } + } + } + + return Task.FromResult(node != null); + } + + private IPublishedContent? FindContentByAlias(IPublishedContentCache? cache, int rootNodeId, string? culture, string alias) + { + if (alias == null) + { + throw new ArgumentNullException(nameof(alias)); + } + + // the alias may be "foo/bar" or "/foo/bar" + // there may be spaces as in "/foo/bar, /foo/nil" + // these should probably be taken care of earlier on + + // TODO: can we normalize the values so that they contain no whitespaces, and no leading slashes? + // and then the comparisons in IsMatch can be way faster - and allocate way less strings + const string propertyAlias = Constants.Conventions.Content.UrlAlias; + + var test1 = alias.TrimStart(Constants.CharArrays.ForwardSlash) + ","; + var test2 = ",/" + test1; // test2 is ",/alias," + test1 = "," + test1; // test1 is ",alias," + + bool IsMatch(IPublishedContent c, string a1, string a2) + { + // this basically implements the original XPath query ;-( + // + // "//* [@isDoc and (" + + // "contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',{0},')" + + // " or contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',/{0},')" + + // ")]" + if (!c.HasProperty(propertyAlias)) { return false; } - IPublishedContent? node = null; - // no alias if "/" - if (frequest.Uri.AbsolutePath != "/") + IPublishedProperty? p = c.GetProperty(propertyAlias); + var varies = p?.PropertyType?.VariesByCulture(); + string? v; + if (varies ?? false) { - node = FindContentByAlias( - umbracoContext!.Content, - frequest.Domain != null ? frequest.Domain.ContentId : 0, - frequest.Culture, - frequest.AbsolutePathDecoded); - - if (node != null) + if (!c.HasCulture(culture)) { - frequest.SetPublishedContent(node); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Path '{UriAbsolutePath}' is an alias for id={PublishedContentId}", frequest.Uri.AbsolutePath, node.Id); - } + return false; } + + v = c.Value(_publishedValueFallback, propertyAlias, culture); + } + else + { + v = c.Value(_publishedValueFallback, propertyAlias); } - return node != null; + if (string.IsNullOrWhiteSpace(v)) + { + return false; + } + + v = "," + v.Replace(" ", string.Empty) + ","; + return v.InvariantContains(a1) || v.InvariantContains(a2); } - private IPublishedContent? FindContentByAlias(IPublishedContentCache? cache, int rootNodeId, string? culture, string alias) + // TODO: even with Linq, what happens below has to be horribly slow + // but the only solution is to entirely refactor URL providers to stop being dynamic + if (rootNodeId > 0) { - if (alias == null) - { - throw new ArgumentNullException(nameof(alias)); - } - - // the alias may be "foo/bar" or "/foo/bar" - // there may be spaces as in "/foo/bar, /foo/nil" - // these should probably be taken care of earlier on - - // TODO: can we normalize the values so that they contain no whitespaces, and no leading slashes? - // and then the comparisons in IsMatch can be way faster - and allocate way less strings - const string propertyAlias = Constants.Conventions.Content.UrlAlias; - - var test1 = alias.TrimStart(Constants.CharArrays.ForwardSlash) + ","; - var test2 = ",/" + test1; // test2 is ",/alias," - test1 = "," + test1; // test1 is ",alias," - - bool IsMatch(IPublishedContent c, string a1, string a2) - { - // this basically implements the original XPath query ;-( - // - // "//* [@isDoc and (" + - // "contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',{0},')" + - // " or contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',/{0},')" + - // ")]" - if (!c.HasProperty(propertyAlias)) - { - return false; - } - - IPublishedProperty? p = c.GetProperty(propertyAlias); - var varies = p!.PropertyType?.VariesByCulture(); - string? v; - if (varies ?? false) - { - if (!c.HasCulture(culture)) - { - return false; - } - - v = c.Value(_publishedValueFallback, propertyAlias, culture); - } - else - { - v = c.Value(_publishedValueFallback, propertyAlias); - } - - if (string.IsNullOrWhiteSpace(v)) - { - return false; - } - - v = "," + v.Replace(" ", string.Empty) + ","; - return v.InvariantContains(a1) || v.InvariantContains(a2); - } - - // TODO: even with Linq, what happens below has to be horribly slow - // but the only solution is to entirely refactor URL providers to stop being dynamic - if (rootNodeId > 0) - { - IPublishedContent? rootNode = cache?.GetById(rootNodeId); - return rootNode?.Descendants(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); - } - - if (cache is not null) - { - foreach (IPublishedContent rootContent in cache.GetAtRoot()) - { - IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); - if (c != null) - { - return c; - } - } - } - - return null; + IPublishedContent? rootNode = cache?.GetById(rootNodeId); + return rootNode?.Descendants(_variationContextAccessor).FirstOrDefault(x => IsMatch(x, test1, test2)); } + + if (cache is not null) + { + foreach (IPublishedContent rootContent in cache.GetAtRoot()) + { + IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor) + .FirstOrDefault(x => IsMatch(x, test1, test2)); + if (c != null) + { + return c; + } + } + } + + return null; } } diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs index f059850086..39fc468cee 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByUrlAndTemplate.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -8,111 +7,121 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides an implementation of that handles page nice URLs and a template. +/// +/// +/// +/// This finder allows for an odd routing pattern similar to altTemplate, probably only use case is if there is +/// an alternative mime type template and it should be routable by something like "/hello/world/json" where the +/// JSON template is to be used for the "world" page +/// +/// +/// Handles /foo/bar/template where /foo/bar is the nice URL of a document, and template a +/// template alias. +/// +/// If successful, then the template of the document request is also assigned. +/// +public class ContentFinderByUrlAndTemplate : ContentFinderByUrl { + private readonly IContentTypeService _contentTypeService; + private readonly IFileService _fileService; + private readonly ILogger _logger; + private WebRoutingSettings _webRoutingSettings; + /// - /// Provides an implementation of that handles page nice URLs and a template. + /// Initializes a new instance of the class. /// - /// - /// This finder allows for an odd routing pattern similar to altTemplate, probably only use case is if there is an alternative mime type template and it should be routable by something like "/hello/world/json" where the JSON template is to be used for the "world" page - /// Handles /foo/bar/template where /foo/bar is the nice URL of a document, and template a template alias. - /// If successful, then the template of the document request is also assigned. - /// - public class ContentFinderByUrlAndTemplate : ContentFinderByUrl + public ContentFinderByUrlAndTemplate( + ILogger logger, + IFileService fileService, + IContentTypeService contentTypeService, + IUmbracoContextAccessor umbracoContextAccessor, + IOptionsMonitor webRoutingSettings) + : base(logger, umbracoContextAccessor) { - private readonly ILogger _logger; - private readonly IFileService _fileService; + _logger = logger; + _fileService = fileService; + _contentTypeService = contentTypeService; + _webRoutingSettings = webRoutingSettings.CurrentValue; + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } - private readonly IContentTypeService _contentTypeService; - private WebRoutingSettings _webRoutingSettings; + /// + /// Tries to find and assign an Umbraco document to a PublishedRequest. + /// + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + /// If successful, also assigns the template. + public override Task TryFindContent(IPublishedRequestBuilder frequest) + { + var path = frequest.AbsolutePathDecoded; - /// - /// Initializes a new instance of the class. - /// - public ContentFinderByUrlAndTemplate( - ILogger logger, - IFileService fileService, - IContentTypeService contentTypeService, - IUmbracoContextAccessor umbracoContextAccessor, - IOptionsMonitor webRoutingSettings) - : base(logger, umbracoContextAccessor) + if (frequest.Domain != null) { - _logger = logger; - _fileService = fileService; - _contentTypeService = contentTypeService; - _webRoutingSettings = webRoutingSettings.CurrentValue; - webRoutingSettings.OnChange(x => _webRoutingSettings = x); + path = DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, path); } - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - /// If successful, also assigns the template. - public override async Task TryFindContent(IPublishedRequestBuilder frequest) + // no template if "/" + if (path == "/") { - var path = frequest.AbsolutePathDecoded; - - if (frequest.Domain != null) - { - path = DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, path); - } - - // no template if "/" - if (path == "/") - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("No template in path '/'"); - } - return false; - } - - // look for template in last position - var pos = path.LastIndexOf('/'); - var templateAlias = path.Substring(pos + 1); - path = pos == 0 ? "/" : path.Substring(0, pos); - - ITemplate? template = _fileService.GetTemplate(templateAlias); - - if (template == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Not a valid template: '{TemplateAlias}'", templateAlias); - } - return false; - } if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Valid template: '{TemplateAlias}'", templateAlias); + _logger.LogDebug("No template in path '/'"); } - // look for node corresponding to the rest of the route - var route = frequest.Domain != null ? (frequest.Domain.ContentId + path) : path; - IPublishedContent? node = FindContent(frequest, route); - - if (node == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Not a valid route to node: '{Route}'", route); - } - return false; - } - - // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings - if (!node.IsAllowedTemplate(_contentTypeService, _webRoutingSettings, template.Id)) - { - _logger.LogWarning("Alternative template '{TemplateAlias}' is not allowed on node {NodeId}.", template.Alias, node.Id); - frequest.SetPublishedContent(null); // clear - return false; - } - - // got it - frequest.SetTemplate(template); - return true; + return Task.FromResult(false); } + + // look for template in last position + var pos = path.LastIndexOf('/'); + var templateAlias = path.Substring(pos + 1); + path = pos == 0 ? "/" : path.Substring(0, pos);; + + ITemplate? template = _fileService.GetTemplate(templateAlias); + + if (template == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Not a valid template: '{TemplateAlias}'", templateAlias); + } + + return Task.FromResult(false); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Valid template: '{TemplateAlias}'", templateAlias); + } + + // look for node corresponding to the rest of the route + var route = frequest.Domain != null ? frequest.Domain.ContentId + path : path; + IPublishedContent? node = FindContent(frequest, route); + + if (node == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Not a valid route to node: '{Route}'", route); + } + + return Task.FromResult(false); + } + + // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings + if (!node.IsAllowedTemplate(_contentTypeService, _webRoutingSettings, template.Id)) + { + _logger.LogWarning( + "Alternative template '{TemplateAlias}' is not allowed on node {NodeId}.", template.Alias, node.Id); + frequest.SetPublishedContent(null); // clear + return Task.FromResult(false); + } + + // got it + frequest.SetTemplate(template); + return Task.FromResult(true); } } diff --git a/src/Umbraco.Core/Routing/ContentFinderCollection.cs b/src/Umbraco.Core/Routing/ContentFinderCollection.cs index 8965d9d447..cc3b711d98 100644 --- a/src/Umbraco.Core/Routing/ContentFinderCollection.cs +++ b/src/Umbraco.Core/Routing/ContentFinderCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class ContentFinderCollection : BuilderCollectionBase { - public class ContentFinderCollection : BuilderCollectionBase + public ContentFinderCollection(Func> items) + : base(items) { - public ContentFinderCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Routing/ContentFinderCollectionBuilder.cs b/src/Umbraco.Core/Routing/ContentFinderCollectionBuilder.cs index d471acf60c..3c8a0e925d 100644 --- a/src/Umbraco.Core/Routing/ContentFinderCollectionBuilder.cs +++ b/src/Umbraco.Core/Routing/ContentFinderCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class ContentFinderCollectionBuilder : OrderedCollectionBuilderBase { - public class ContentFinderCollectionBuilder : OrderedCollectionBuilderBase - { - protected override ContentFinderCollectionBuilder This => this; - } + protected override ContentFinderCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultMediaUrlProvider.cs index 1afda0175c..d1c79783f0 100644 --- a/src/Umbraco.Core/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultMediaUrlProvider.cs @@ -1,75 +1,83 @@ -using System; -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Default media URL provider. +/// +public class DefaultMediaUrlProvider : IMediaUrlProvider { - /// - /// Default media URL provider. - /// - public class DefaultMediaUrlProvider : IMediaUrlProvider + private readonly MediaUrlGeneratorCollection _mediaPathGenerators; + private readonly UriUtility _uriUtility; + + public DefaultMediaUrlProvider(MediaUrlGeneratorCollection mediaPathGenerators, UriUtility uriUtility) { - private readonly UriUtility _uriUtility; - private readonly MediaUrlGeneratorCollection _mediaPathGenerators; + _mediaPathGenerators = mediaPathGenerators ?? throw new ArgumentNullException(nameof(mediaPathGenerators)); + _uriUtility = uriUtility; + } - public DefaultMediaUrlProvider(MediaUrlGeneratorCollection mediaPathGenerators, UriUtility uriUtility) + /// + public virtual UrlInfo? GetMediaUrl( + IPublishedContent content, + string propertyAlias, + UrlMode mode, + string? culture, + Uri current) + { + IPublishedProperty? prop = content.GetProperty(propertyAlias); + + // get the raw source value since this is what is used by IDataEditorWithMediaPath for processing + var value = prop?.GetSourceValue(culture); + if (value == null) { - _mediaPathGenerators = mediaPathGenerators ?? throw new ArgumentNullException(nameof(mediaPathGenerators)); - _uriUtility = uriUtility; - } - - /// - public virtual UrlInfo? GetMediaUrl(IPublishedContent content, - string propertyAlias, UrlMode mode, string? culture, Uri current) - { - var prop = content.GetProperty(propertyAlias); - - // get the raw source value since this is what is used by IDataEditorWithMediaPath for processing - var value = prop?.GetSourceValue(culture); - if (value == null) - { - return null; - } - - var propType = prop?.PropertyType; - - if (_mediaPathGenerators.TryGetMediaPath(propType?.EditorAlias, value, out var path)) - { - var url = AssembleUrl(path!, current, mode); - return UrlInfo.Url(url.ToString(), culture); - } - return null; } - private Uri AssembleUrl(string path, Uri current, UrlMode mode) + IPublishedPropertyType? propType = prop?.PropertyType; + + if (_mediaPathGenerators.TryGetMediaPath(propType?.EditorAlias, value, out var path)) { - if (string.IsNullOrWhiteSpace(path)) - throw new ArgumentException($"{nameof(path)} cannot be null or whitespace", nameof(path)); - - // the stored path is absolute so we just return it as is - if (Uri.IsWellFormedUriString(path, UriKind.Absolute)) - return new Uri(path); - - Uri uri; - - if (current == null) - mode = UrlMode.Relative; // best we can do - - switch (mode) - { - case UrlMode.Absolute: - uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); - break; - case UrlMode.Relative: - case UrlMode.Auto: - uri = new Uri(path, UriKind.Relative); - break; - default: - throw new ArgumentOutOfRangeException(nameof(mode)); - } - - return _uriUtility.MediaUriFromUmbraco(uri); + Uri url = AssembleUrl(path!, current, mode); + return UrlInfo.Url(url.ToString(), culture); } + + return null; + } + + private Uri AssembleUrl(string path, Uri current, UrlMode mode) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException($"{nameof(path)} cannot be null or whitespace", nameof(path)); + } + + // the stored path is absolute so we just return it as is + if (Uri.IsWellFormedUriString(path, UriKind.Absolute)) + { + return new Uri(path); + } + + Uri uri; + + if (current == null) + { + mode = UrlMode.Relative; // best we can do + } + + switch (mode) + { + case UrlMode.Absolute: + uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); + break; + case UrlMode.Relative: + case UrlMode.Auto: + uri = new Uri(path, UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + + return _uriUtility.MediaUriFromUmbraco(uri); } } diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index 25e0764349..d0a238dbb2 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -11,235 +9,252 @@ using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides urls. +/// +public class DefaultUrlProvider : IUrlProvider { - /// - /// Provides urls. - /// - public class DefaultUrlProvider : IUrlProvider + private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService? _localizedTextService; + private readonly ILogger _logger; + private readonly ISiteDomainMapper _siteDomainMapper; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly UriUtility _uriUtility; + private RequestHandlerSettings _requestSettings; + + [Obsolete("Use ctor with all parameters")] + public DefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility) + : this( + requestSettings, + logger, + siteDomainMapper, + umbracoContextAccessor, + uriUtility, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly ILocalizationService _localizationService; - private readonly ILocalizedTextService? _localizedTextService; - private readonly ILogger _logger; - private RequestHandlerSettings _requestSettings; - private readonly ISiteDomainMapper _siteDomainMapper; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly UriUtility _uriUtility; + } - [Obsolete("Use ctor with all parameters")] - public DefaultUrlProvider(IOptionsMonitor requestSettings, ILogger logger, - ISiteDomainMapper siteDomainMapper, IUmbracoContextAccessor umbracoContextAccessor, UriUtility uriUtility) - : this(requestSettings, logger, siteDomainMapper, umbracoContextAccessor, uriUtility, - StaticServiceProvider.Instance.GetRequiredService()) + public DefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility, + ILocalizationService localizationService) + { + _requestSettings = requestSettings.CurrentValue; + _logger = logger; + _siteDomainMapper = siteDomainMapper; + _umbracoContextAccessor = umbracoContextAccessor; + _uriUtility = uriUtility; + _localizationService = localizationService; + + requestSettings.OnChange(x => _requestSettings = x); + } + + #region GetOtherUrls + + /// + /// Gets the other URLs of a published content. + /// + /// The Umbraco context. + /// The published content id. + /// The current absolute URL. + /// The other URLs for the published content. + /// + /// + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + public virtual IEnumerable GetOtherUrls(int id, Uri current) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContent? node = umbracoContext.Content?.GetById(id); + if (node == null) { + yield break; } - public DefaultUrlProvider( - IOptionsMonitor requestSettings, - ILogger logger, - ISiteDomainMapper siteDomainMapper, - IUmbracoContextAccessor umbracoContextAccessor, - UriUtility uriUtility, - ILocalizationService localizationService) - { - _requestSettings = requestSettings.CurrentValue; - _logger = logger; - _siteDomainMapper = siteDomainMapper; - _umbracoContextAccessor = umbracoContextAccessor; - _uriUtility = uriUtility; - _localizationService = localizationService; + // look for domains, walking up the tree + IPublishedContent? n = node; + IEnumerable? domainUris = + DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false); - requestSettings.OnChange(x => _requestSettings = x); + // n is null at root + while (domainUris == null && n != null) + { + n = n.Parent; // move to parent node + domainUris = n == null + ? null + : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current); } - #region GetOtherUrls - - /// - /// Gets the other URLs of a published content. - /// - /// The Umbraco context. - /// The published content id. - /// The current absolute URL. - /// The other URLs for the published content. - /// - /// - /// Other URLs are those that GetUrl would not return in the current context, but would be valid - /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// - /// - public virtual IEnumerable GetOtherUrls(int id, Uri current) + // no domains = exit + if (domainUris == null) { - IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - IPublishedContent? node = umbracoContext.Content?.GetById(id); - if (node == null) - { - yield break; - } - - // look for domains, walking up the tree - IPublishedContent? n = node; - IEnumerable? domainUris = - DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, - current, false); - while (domainUris == null && n != null) // n is null at root - { - n = n.Parent; // move to parent node - domainUris = n == null - ? null - : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, - current); - } - - // no domains = exit - if (domainUris == null) - { - yield break; - } - - foreach (DomainAndUri d in domainUris) - { - var culture = d.Culture; - - // although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok - var route = umbracoContext.Content?.GetRouteById(id, culture); - if (route == null) - { - continue; - } - - // need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) - var pos = route.IndexOf('/'); - var path = pos == 0 ? route : route.Substring(pos); - - var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); - uri = _uriUtility.UriFromUmbraco(uri, _requestSettings); - yield return UrlInfo.Url(uri.ToString(), culture); - } + yield break; } - #endregion - - #region GetUrl - - /// - public virtual UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + foreach (DomainAndUri d in domainUris) { - if (!current.IsAbsoluteUri) + var culture = d.Culture; + + // although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok + var route = umbracoContext.Content?.GetRouteById(id, culture); + if (route == null) { - throw new ArgumentException("Current URL must be absolute.", nameof(current)); + continue; } - IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - // will not use cache if previewing - var route = umbracoContext.Content?.GetRouteById(content.Id, culture); - - return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); - } - - internal UrlInfo? GetUrlFromRoute( - string? route, - IUmbracoContext umbracoContext, - int id, - Uri current, - UrlMode mode, - string? culture) - { - if (string.IsNullOrWhiteSpace(route)) - { - _logger.LogDebug( - "Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", - id); - return null; - } - - // extract domainUri and path - // route is / or / + // need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); - DomainAndUri? domainUri = pos == 0 - ? null - : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture); - var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); - if (domainUri is not null || string.IsNullOrEmpty(culture) || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) - { - var url = AssembleUrl(domainUri, path, current, mode).ToString(); - return UrlInfo.Url(url, culture); - } + var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); + uri = _uriUtility.UriFromUmbraco(uri, _requestSettings); + yield return UrlInfo.Url(uri.ToString(), culture); + } + } + #endregion + + #region GetUrl + + /// + public virtual UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current) + { + if (!current.IsAbsoluteUri) + { + throw new ArgumentException("Current URL must be absolute.", nameof(current)); + } + + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + // will not use cache if previewing + var route = umbracoContext.Content?.GetRouteById(content.Id, culture); + + return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); + } + + internal UrlInfo? GetUrlFromRoute( + string? route, + IUmbracoContext umbracoContext, + int id, + Uri current, + UrlMode mode, + string? culture) + { + if (string.IsNullOrWhiteSpace(route)) + { + _logger.LogDebug( + "Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", + id); return null; } - #endregion + // extract domainUri and path + // route is / or / + var pos = route.IndexOf('/'); + var path = pos == 0 ? route : route[pos..]; + DomainAndUri? domainUri = pos == 0 + ? null + : DomainUtilities.DomainForNode( + umbracoContext.PublishedSnapshot.Domains, + _siteDomainMapper, + int.Parse(route[..pos], CultureInfo.InvariantCulture), + current, + culture); - #region Utilities - - private Uri AssembleUrl(DomainAndUri? domainUri, string path, Uri current, UrlMode mode) + var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); + if (domainUri is not null || string.IsNullOrEmpty(culture) || + culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) { - Uri uri; - - // ignore vdir at that point, UriFromUmbraco will do it - - if (domainUri == null) // no domain was found - { - if (current == null) - { - mode = UrlMode.Relative; // best we can do - } - - switch (mode) - { - case UrlMode.Absolute: - uri = new Uri(current!.GetLeftPart(UriPartial.Authority) + path); - break; - case UrlMode.Relative: - case UrlMode.Auto: - uri = new Uri(path, UriKind.Relative); - break; - default: - throw new ArgumentOutOfRangeException(nameof(mode)); - } - } - else // a domain was found - { - if (mode == UrlMode.Auto) - { - //this check is a little tricky, we can't just compare domains - if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == - current.GetLeftPart(UriPartial.Authority)) - { - mode = UrlMode.Relative; - } - else - { - mode = UrlMode.Absolute; - } - } - - switch (mode) - { - case UrlMode.Absolute: - uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); - break; - case UrlMode.Relative: - uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative); - break; - default: - throw new ArgumentOutOfRangeException(nameof(mode)); - } - } - - // UriFromUmbraco will handle vdir - // meaning it will add vdir into domain URLs too! - return _uriUtility.UriFromUmbraco(uri, _requestSettings); + var url = AssembleUrl(domainUri, path, current, mode).ToString(); + return UrlInfo.Url(url, culture); } - private string CombinePaths(string path1, string path2) - { - var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; - return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); - } - - #endregion + return null; } + + #endregion + + #region Utilities + + private Uri AssembleUrl(DomainAndUri? domainUri, string path, Uri current, UrlMode mode) + { + Uri uri; + + // ignore vdir at that point, UriFromUmbraco will do it + // no domain was found + if (domainUri == null) + { + if (current == null) + { + mode = UrlMode.Relative; // best we can do + } + + switch (mode) + { + case UrlMode.Absolute: + uri = new Uri(current!.GetLeftPart(UriPartial.Authority) + path); + break; + case UrlMode.Relative: + case UrlMode.Auto: + uri = new Uri(path, UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + } + + // a domain was found + else + { + if (mode == UrlMode.Auto) + { + // this check is a little tricky, we can't just compare domains + if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == + current.GetLeftPart(UriPartial.Authority)) + { + mode = UrlMode.Relative; + } + else + { + mode = UrlMode.Absolute; + } + } + + switch (mode) + { + case UrlMode.Absolute: + uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); + break; + case UrlMode.Relative: + uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + } + + // UriFromUmbraco will handle vdir + // meaning it will add vdir into domain URLs too! + return _uriUtility.UriFromUmbraco(uri, _requestSettings); + } + + private string CombinePaths(string path1, string path2) + { + var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2; + return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash); + } + + #endregion } diff --git a/src/Umbraco.Core/Routing/Domain.cs b/src/Umbraco.Core/Routing/Domain.cs index ecefb07e8b..291d7beed9 100644 --- a/src/Umbraco.Core/Routing/Domain.cs +++ b/src/Umbraco.Core/Routing/Domain.cs @@ -1,63 +1,62 @@ -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Represents a published snapshot domain. +/// +public class Domain { /// - /// Represents a published snapshot domain. + /// Initializes a new instance of the class. /// - public class Domain + /// The unique identifier of the domain. + /// The name of the domain. + /// The identifier of the content which supports the domain. + /// The culture of the domain. + /// A value indicating whether the domain is a wildcard domain. + public Domain(int id, string name, int contentId, string? culture, bool isWildcard) { - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the domain. - /// The name of the domain. - /// The identifier of the content which supports the domain. - /// The culture of the domain. - /// A value indicating whether the domain is a wildcard domain. - public Domain(int id, string name, int contentId, string? culture, bool isWildcard) - { - Id = id; - Name = name; - ContentId = contentId; - Culture = culture; - IsWildcard = isWildcard; - } - - /// - /// Initializes a new instance of the class. - /// - /// An origin domain. - protected Domain(Domain domain) - { - Id = domain.Id; - Name = domain.Name; - ContentId = domain.ContentId; - Culture = domain.Culture; - IsWildcard = domain.IsWildcard; - } - - /// - /// Gets the unique identifier of the domain. - /// - public int Id { get; } - - /// - /// Gets the name of the domain. - /// - public string Name { get; } - - /// - /// Gets the identifier of the content which supports the domain. - /// - public int ContentId { get; } - - /// - /// Gets the culture of the domain. - /// - public string? Culture { get; } - - /// - /// Gets a value indicating whether the domain is a wildcard domain. - /// - public bool IsWildcard { get; } + Id = id; + Name = name; + ContentId = contentId; + Culture = culture; + IsWildcard = isWildcard; } + + /// + /// Initializes a new instance of the class. + /// + /// An origin domain. + protected Domain(Domain domain) + { + Id = domain.Id; + Name = domain.Name; + ContentId = domain.ContentId; + Culture = domain.Culture; + IsWildcard = domain.IsWildcard; + } + + /// + /// Gets the unique identifier of the domain. + /// + public int Id { get; } + + /// + /// Gets the name of the domain. + /// + public string Name { get; } + + /// + /// Gets the identifier of the content which supports the domain. + /// + public int ContentId { get; } + + /// + /// Gets the culture of the domain. + /// + public string? Culture { get; } + + /// + /// Gets a value indicating whether the domain is a wildcard domain. + /// + public bool IsWildcard { get; } } diff --git a/src/Umbraco.Core/Routing/DomainAndUri.cs b/src/Umbraco.Core/Routing/DomainAndUri.cs index 751c4ead58..c5f9497d77 100644 --- a/src/Umbraco.Core/Routing/DomainAndUri.cs +++ b/src/Umbraco.Core/Routing/DomainAndUri.cs @@ -1,44 +1,47 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Represents a published snapshot domain with its normalized uri. +/// +/// +/// +/// In Umbraco it is valid to create domains with name such as example.com, https://www.example.com +/// , example.com/foo/. +/// +/// +/// The normalized uri of a domain begins with a scheme and ends with no slash, eg http://example.com/, +/// https://www.example.com/, http://example.com/foo/. +/// +/// +public class DomainAndUri : Domain { /// - /// Represents a published snapshot domain with its normalized uri. + /// Initializes a new instance of the class. /// - /// - /// In Umbraco it is valid to create domains with name such as example.com, https://www.example.com, example.com/foo/. - /// The normalized uri of a domain begins with a scheme and ends with no slash, eg http://example.com/, https://www.example.com/, http://example.com/foo/. - /// - public class DomainAndUri : Domain + /// The original domain. + /// The context current Uri. + public DomainAndUri(Domain domain, Uri currentUri) + : base(domain) { - /// - /// Initializes a new instance of the class. - /// - /// The original domain. - /// The context current Uri. - public DomainAndUri(Domain domain, Uri currentUri) - : base(domain) + try { - try - { - Uri = DomainUtilities.ParseUriFromDomainName(Name, currentUri); - } - catch (UriFormatException) - { - throw new ArgumentException($"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{Name.ToCSharpString()}\"." - + " Hostname should be a valid uri.", nameof(domain)); - } + Uri = DomainUtilities.ParseUriFromDomainName(Name, currentUri); } - - /// - /// Gets the normalized uri of the domain, within the current context. - /// - public Uri Uri { get; } - - public override string ToString() + catch (UriFormatException) { - return $"{{ \"{Name}\", \"{Uri}\" }}"; + throw new ArgumentException( + $"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{Name.ToCSharpString()}\"." + + " Hostname should be a valid uri.", + nameof(domain)); } } + + /// + /// Gets the normalized uri of the domain, within the current context. + /// + public Uri Uri { get; } + + public override string ToString() => $"{{ \"{Name}\", \"{Uri}\" }}"; } diff --git a/src/Umbraco.Core/Routing/DomainUtilities.cs b/src/Umbraco.Core/Routing/DomainUtilities.cs index 9e762a600e..f31244d2ac 100644 --- a/src/Umbraco.Core/Routing/DomainUtilities.cs +++ b/src/Umbraco.Core/Routing/DomainUtilities.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -32,10 +29,14 @@ namespace Umbraco.Cms.Core.Routing public static string? GetCultureFromDomains(int contentId, string contentPath, Uri? current, IUmbracoContext umbracoContext, ISiteDomainMapper siteDomainMapper) { if (umbracoContext == null) + { throw new InvalidOperationException("A current UmbracoContext is required."); + } if (current == null) + { current = umbracoContext.CleanedUmbracoUrl; + } // get the published route, else the preview route // if both are null then the content does not exist @@ -43,18 +44,28 @@ namespace Umbraco.Cms.Core.Routing umbracoContext.Content?.GetRouteById(true, contentId); if (route == null) + { return null; + } var pos = route.IndexOf('/'); - var domain = pos == 0 + DomainAndUri? domain = pos == 0 ? null : DomainForNode(umbracoContext.Domains, siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current); var rootContentId = domain?.ContentId ?? -1; - var wcDomain = FindWildcardDomainInPath(umbracoContext.Domains?.GetAll(true), contentPath, rootContentId); + Domain? wcDomain = FindWildcardDomainInPath(umbracoContext.Domains?.GetAll(true), contentPath, rootContentId); + + if (wcDomain != null) + { + return wcDomain.Culture; + } + + if (domain != null) + { + return domain.Culture; + } - if (wcDomain != null) return wcDomain.Culture; - if (domain != null) return domain.Culture; return umbracoContext.Domains?.DefaultCulture; } @@ -81,14 +92,18 @@ namespace Umbraco.Cms.Core.Routing { // be safe if (nodeId <= 0) + { return null; + } // get the domains on that node - var domains = domainCache?.GetAssigned(nodeId).ToArray(); + Domain[]? domains = domainCache?.GetAssigned(nodeId).ToArray(); // none? if (domains is null || domains.Length == 0) + { return null; + } // else filter // it could be that none apply (due to culture) @@ -110,17 +125,21 @@ namespace Umbraco.Cms.Core.Routing { // be safe if (nodeId <= 0) + { return null; + } // get the domains on that node - var domains = domainCache?.GetAssigned(nodeId).ToArray(); + Domain[]? domains = domainCache?.GetAssigned(nodeId).ToArray(); // none? if (domains is null || domains.Length == 0) + { return null; + } // get the domains and their uris - var domainAndUris = SelectDomains(domains, current).ToArray(); + DomainAndUri[] domainAndUris = SelectDomains(domains, current).ToArray(); // filter return siteDomainMapper.MapDomains(domainAndUris, current, excludeDefault, null, domainCache?.DefaultCulture).ToArray(); @@ -161,7 +180,9 @@ namespace Umbraco.Cms.Core.Routing // nothing = no magic, return null if (domainsAndUris is null || domainsAndUris.Count == 0) + { return null; + } // sanitize cultures culture = culture?.NullOrWhiteSpaceAsNull(); @@ -179,27 +200,31 @@ namespace Umbraco.Cms.Core.Routing // if a culture is specified, then try to get domains for that culture // (else cultureDomains will be null) // do NOT specify a default culture, else it would pick those domains - var cultureDomains = SelectByCulture(domainsAndUris, culture, defaultCulture: null); + IReadOnlyCollection? cultureDomains = SelectByCulture(domainsAndUris, culture, defaultCulture: null); IReadOnlyCollection considerForBaseDomains = domainsAndUris; if (cultureDomains != null) { if (cultureDomains.Count == 1) // only 1, return + { return cultureDomains.First(); + } // else restrict to those domains, for base lookup considerForBaseDomains = cultureDomains; } // look for domains that would be the base of the uri - var baseDomains = SelectByBase(considerForBaseDomains, uri, culture); + IReadOnlyCollection baseDomains = SelectByBase(considerForBaseDomains, uri, culture); if (baseDomains.Count > 0) // found, return + { return baseDomains.First(); + } // if nothing works, then try to run the filter to select a domain // either restricting on cultureDomains, or on all domains if (filter != null) { - var domainAndUri = filter(cultureDomains ?? domainsAndUris, uri, culture, defaultCulture); + DomainAndUri? domainAndUri = filter(cultureDomains ?? domainsAndUris, uri, culture, defaultCulture); return domainAndUri; } @@ -216,14 +241,16 @@ namespace Umbraco.Cms.Core.Routing { // look for domains that would be the base of the uri // ie current is www.example.com/foo/bar, look for domain www.example.com - var currentWithSlash = uri.EndPathWithSlash(); + Uri currentWithSlash = uri.EndPathWithSlash(); var baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithSlash) && MatchesCulture(d, culture)).ToList(); // if none matches, try again without the port // ie current is www.example.com:1234/foo/bar, look for domain www.example.com - var currentWithoutPort = currentWithSlash.WithoutPort(); + Uri currentWithoutPort = currentWithSlash.WithoutPort(); if (baseDomains.Count == 0) + { baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithoutPort)).ToList(); + } return baseDomains; } @@ -235,13 +262,19 @@ namespace Umbraco.Cms.Core.Routing if (culture != null) // try the supplied culture { var cultureDomains = domainsAndUris.Where(x => x.Culture.InvariantEquals(culture)).ToList(); - if (cultureDomains.Count > 0) return cultureDomains; + if (cultureDomains.Count > 0) + { + return cultureDomains; + } } if (defaultCulture != null) // try the defaultCulture culture { var cultureDomains = domainsAndUris.Where(x => x.Culture.InvariantEquals(defaultCulture)).ToList(); - if (cultureDomains.Count > 0) return cultureDomains; + if (cultureDomains.Count > 0) + { + return cultureDomains; + } } return null; @@ -256,13 +289,19 @@ namespace Umbraco.Cms.Core.Routing if (culture != null) // try the supplied culture { domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(culture)); - if (domainAndUri != null) return domainAndUri; + if (domainAndUri != null) + { + return domainAndUri; + } } if (defaultCulture != null) // try the defaultCulture culture { domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(defaultCulture)); - if (domainAndUri != null) return domainAndUri; + if (domainAndUri != null) + { + return domainAndUri; + } } return domainsAndUris.First(); // what else? diff --git a/src/Umbraco.Core/Routing/IContentFinder.cs b/src/Umbraco.Core/Routing/IContentFinder.cs index ab160715bb..3e4304fe70 100644 --- a/src/Umbraco.Core/Routing/IContentFinder.cs +++ b/src/Umbraco.Core/Routing/IContentFinder.cs @@ -1,18 +1,18 @@ -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Routing +/// +/// Provides a method to try to find and assign an Umbraco document to a PublishedRequest. +/// +public interface IContentFinder { /// - /// Provides a method to try to find and assign an Umbraco document to a PublishedRequest. + /// Tries to find and assign an Umbraco document to a PublishedRequest. /// - public interface IContentFinder - { - /// - /// Tries to find and assign an Umbraco document to a PublishedRequest. - /// - /// The PublishedRequest. - /// A value indicating whether an Umbraco document was found and assigned. - /// Optionally, can also assign the template or anything else on the document request, although that is not required. - Task TryFindContent(IPublishedRequestBuilder request); - } + /// The PublishedRequest. + /// A value indicating whether an Umbraco document was found and assigned. + /// + /// Optionally, can also assign the template or anything else on the document request, although that is not + /// required. + /// + Task TryFindContent(IPublishedRequestBuilder request); } diff --git a/src/Umbraco.Core/Routing/IContentLastChanceFinder.cs b/src/Umbraco.Core/Routing/IContentLastChanceFinder.cs index 19e5f80246..ad3959ae42 100644 --- a/src/Umbraco.Core/Routing/IContentLastChanceFinder.cs +++ b/src/Umbraco.Core/Routing/IContentLastChanceFinder.cs @@ -1,10 +1,10 @@ -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides a method to try to find and assign an Umbraco document to a PublishedRequest +/// when everything else has failed. +/// +/// Identical to but required in order to differentiate them in ioc. +public interface IContentLastChanceFinder : IContentFinder { - /// - /// Provides a method to try to find and assign an Umbraco document to a PublishedRequest - /// when everything else has failed. - /// - /// Identical to but required in order to differentiate them in ioc. - public interface IContentLastChanceFinder : IContentFinder - { } } diff --git a/src/Umbraco.Core/Routing/IMediaUrlProvider.cs b/src/Umbraco.Core/Routing/IMediaUrlProvider.cs index 4478f60334..9d944efff7 100644 --- a/src/Umbraco.Core/Routing/IMediaUrlProvider.cs +++ b/src/Umbraco.Core/Routing/IMediaUrlProvider.cs @@ -1,30 +1,32 @@ -using System; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides media URL. +/// +public interface IMediaUrlProvider { /// - /// Provides media URL. + /// Gets the URL of a media item. /// - public interface IMediaUrlProvider - { - /// - /// Gets the URL of a media item. - /// - /// The published content. - /// The property alias to resolve the URL from. - /// The URL mode. - /// The variation language. - /// The current absolute URL. - /// The URL for the media. - /// - /// The URL is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the URL for the specified culture or, - /// when no culture is specified, the current culture. - /// The URL provider can ignore the mode and always return an absolute URL, - /// e.g. a cdn URL provider will most likely always return an absolute URL. - /// If the provider is unable to provide a URL, it returns null. - /// - UrlInfo? GetMediaUrl(IPublishedContent content, string propertyAlias, UrlMode mode, string? culture, Uri current); - } + /// The published content. + /// The property alias to resolve the URL from. + /// The URL mode. + /// The variation language. + /// The current absolute URL. + /// The URL for the media. + /// + /// The URL is absolute or relative depending on mode and on current. + /// + /// If the media is multi-lingual, gets the URL for the specified culture or, + /// when no culture is specified, the current culture. + /// + /// + /// The URL provider can ignore the mode and always return an absolute URL, + /// e.g. a cdn URL provider will most likely always return an absolute URL. + /// + /// If the provider is unable to provide a URL, it returns null. + /// + UrlInfo? GetMediaUrl(IPublishedContent content, string propertyAlias, UrlMode mode, string? culture, Uri current); } diff --git a/src/Umbraco.Core/Routing/IPublishedRequest.cs b/src/Umbraco.Core/Routing/IPublishedRequest.cs index 9f68c618d2..645de414d7 100644 --- a/src/Umbraco.Core/Routing/IPublishedRequest.cs +++ b/src/Umbraco.Core/Routing/IPublishedRequest.cs @@ -1,99 +1,114 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// The result of Umbraco routing built with the +/// +public interface IPublishedRequest { /// - /// The result of Umbraco routing built with the + /// Gets the cleaned up inbound Uri used for routing. /// - public interface IPublishedRequest - { - /// - /// Gets the cleaned up inbound Uri used for routing. - /// - /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. - Uri Uri { get; } + /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. + Uri Uri { get; } - /// - /// Gets the URI decoded absolute path of the - /// - string AbsolutePathDecoded { get; } + /// + /// Gets the URI decoded absolute path of the + /// + string AbsolutePathDecoded { get; } - /// - /// Gets a value indicating the requested content. - /// - IPublishedContent? PublishedContent { get; } + /// + /// Gets a value indicating the requested content. + /// + IPublishedContent? PublishedContent { get; } - /// - /// Gets a value indicating whether the current published content has been obtained - /// from the initial published content following internal redirections exclusively. - /// - /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to - /// apply the internal redirect or not, when content is not the initial content. - bool IsInternalRedirect { get; } + /// + /// Gets a value indicating whether the current published content has been obtained + /// from the initial published content following internal redirections exclusively. + /// + /// + /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to + /// apply the internal redirect or not, when content is not the initial content. + /// + bool IsInternalRedirect { get; } - /// - /// Gets the template assigned to the request (if any) - /// - ITemplate? Template { get; } + /// + /// Gets the template assigned to the request (if any) + /// + ITemplate? Template { get; } - /// - /// Gets the content request's domain. - /// - /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, - /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg "http://example.com/". - DomainAndUri? Domain { get; } + /// + /// Gets the content request's domain. + /// + /// + /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, + /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg + /// "http://example.com/". + /// + DomainAndUri? Domain { get; } - /// - /// Gets the content request's culture. - /// - /// - /// This will get mapped to a CultureInfo eventually but CultureInfo are expensive to create so we want to leave that up to the - /// localization middleware to do. See https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs#L165. - /// - string? Culture { get; } + /// + /// Gets the content request's culture. + /// + /// + /// This will get mapped to a CultureInfo eventually but CultureInfo are expensive to create so we want to leave that + /// up to the + /// localization middleware to do. See + /// https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs#L165. + /// + string? Culture { get; } - /// - /// Gets the url to redirect to, when the content request triggers a redirect. - /// - string? RedirectUrl { get; } + /// + /// Gets the url to redirect to, when the content request triggers a redirect. + /// + string? RedirectUrl { get; } - /// - /// Gets the content request http response status code. - /// - /// Does not actually set the http response status code, only registers that the response - /// should use the specified code. The code will or will not be used, in due time. - int? ResponseStatusCode { get; } + /// + /// Gets the content request http response status code. + /// + /// + /// Does not actually set the http response status code, only registers that the response + /// should use the specified code. The code will or will not be used, in due time. + /// + int? ResponseStatusCode { get; } - /// - /// Gets a list of Extensions to append to the Response.Cache object. - /// - IReadOnlyList? CacheExtensions { get; } + /// + /// Gets a list of Extensions to append to the Response.Cache object. + /// + IReadOnlyList? CacheExtensions { get; } - /// - /// Gets a dictionary of Headers to append to the Response object. - /// - IReadOnlyDictionary? Headers { get; } + /// + /// Gets a dictionary of Headers to append to the Response object. + /// + IReadOnlyDictionary? Headers { get; } - /// - /// Gets a value indicating whether the no-cache value should be added to the Cache-Control header - /// - bool SetNoCacheHeader { get; } + /// + /// Gets a value indicating whether the no-cache value should be added to the Cache-Control header + /// + bool SetNoCacheHeader { get; } - /// - /// Gets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. - /// - /// - /// This is an uncommon API used for edge cases with complex routing and would be used - /// by developers to configure the request to disable collision checks in . - /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers since - /// collission checking only occurs in the back office which is launched by - /// for which events do not execute. - /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, https://issues.umbraco.org/issue/U4-10345 - /// but it's still unclear how this was used. - /// - bool IgnorePublishedContentCollisions { get; } - } + /// + /// Gets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. + /// + /// + /// + /// This is an uncommon API used for edge cases with complex routing and would be used + /// by developers to configure the request to disable collision checks in . + /// + /// + /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers + /// since + /// collission checking only occurs in the back office which is launched by + /// + /// for which events do not execute. + /// + /// + /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, + /// https://issues.umbraco.org/issue/U4-10345 + /// but it's still unclear how this was used. + /// + /// + bool IgnorePublishedContentCollisions { get; } } diff --git a/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs b/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs index e5a915d682..f6cdafee78 100644 --- a/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs +++ b/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs @@ -1,161 +1,175 @@ -using System; -using System.Collections.Generic; using System.Globalization; using System.Net; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Used by to route inbound requests to Umbraco content +/// +public interface IPublishedRequestBuilder { /// - /// Used by to route inbound requests to Umbraco content + /// Gets the cleaned up inbound Uri used for routing. /// - public interface IPublishedRequestBuilder - { - /// - /// Gets the cleaned up inbound Uri used for routing. - /// - /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. - Uri Uri { get; } + /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. + Uri Uri { get; } - /// - /// Gets the URI decoded absolute path of the - /// - string AbsolutePathDecoded { get; } + /// + /// Gets the URI decoded absolute path of the + /// + string AbsolutePathDecoded { get; } - /// - /// Gets the assigned (if any) - /// - DomainAndUri? Domain { get; } + /// + /// Gets the assigned (if any) + /// + DomainAndUri? Domain { get; } - /// - /// Gets the assigned (if any) - /// - string? Culture { get; } + /// + /// Gets the assigned (if any) + /// + string? Culture { get; } - /// - /// Gets a value indicating whether the current published content has been obtained - /// from the initial published content following internal redirections exclusively. - /// - /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to - /// apply the internal redirect or not, when content is not the initial content. - bool IsInternalRedirect { get; } + /// + /// Gets a value indicating whether the current published content has been obtained + /// from the initial published content following internal redirections exclusively. + /// + /// + /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to + /// apply the internal redirect or not, when content is not the initial content. + /// + bool IsInternalRedirect { get; } - /// - /// Gets the content request http response status code. - /// - int? ResponseStatusCode { get; } + /// + /// Gets the content request http response status code. + /// + int? ResponseStatusCode { get; } - /// - /// Gets the current assigned (if any) - /// - IPublishedContent? PublishedContent { get; } + /// + /// Gets the current assigned (if any) + /// + IPublishedContent? PublishedContent { get; } - /// - /// Gets the template assigned to the request (if any) - /// - ITemplate? Template { get; } + /// + /// Gets the template assigned to the request (if any) + /// + ITemplate? Template { get; } - /// - /// Builds the - /// - IPublishedRequest Build(); + /// + /// Builds the + /// + IPublishedRequest Build(); - /// - /// Sets the domain for the request which also sets the culture - /// - IPublishedRequestBuilder SetDomain(DomainAndUri domain); + /// + /// Sets the domain for the request which also sets the culture + /// + IPublishedRequestBuilder SetDomain(DomainAndUri domain); - /// - /// Sets the culture for the request - /// - IPublishedRequestBuilder SetCulture(string? culture); + /// + /// Sets the culture for the request + /// + IPublishedRequestBuilder SetCulture(string? culture); - /// - /// Sets the found for the request - /// - /// Setting the content clears the template and redirect - IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content); + /// + /// Sets the found for the request + /// + /// Setting the content clears the template and redirect + IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content); - /// - /// Sets the requested content, following an internal redirect. - /// - /// The requested content. - /// Since this sets the content, it will clear the template - IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content); + /// + /// Sets the requested content, following an internal redirect. + /// + /// The requested content. + /// Since this sets the content, it will clear the template + IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content); - /// - /// Tries to set the template to use to display the requested content. - /// - /// The alias of the template. - /// A value indicating whether a valid template with the specified alias was found. - /// - /// Successfully setting the template does refresh RenderingEngine. - /// If setting the template fails, then the previous template (if any) remains in place. - /// - bool TrySetTemplate(string alias); + /// + /// Tries to set the template to use to display the requested content. + /// + /// The alias of the template. + /// A value indicating whether a valid template with the specified alias was found. + /// + /// Successfully setting the template does refresh RenderingEngine. + /// If setting the template fails, then the previous template (if any) remains in place. + /// + bool TrySetTemplate(string alias); - /// - /// Sets the template to use to display the requested content. - /// - /// The template. - /// Setting the template does refresh RenderingEngine. - IPublishedRequestBuilder SetTemplate(ITemplate? template); + /// + /// Sets the template to use to display the requested content. + /// + /// The template. + /// Setting the template does refresh RenderingEngine. + IPublishedRequestBuilder SetTemplate(ITemplate? template); - /// - /// Indicates that the content request should trigger a permanent redirect (301). - /// - /// The url to redirect to. - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - IPublishedRequestBuilder SetRedirectPermanent(string url); + /// + /// Indicates that the content request should trigger a permanent redirect (301). + /// + /// The url to redirect to. + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + IPublishedRequestBuilder SetRedirectPermanent(string url); - /// - /// Indicates that the content request should trigger a redirect, with a specified status code. - /// - /// The url to redirect to. - /// The status code (300-308). - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect); + /// + /// Indicates that the content request should trigger a redirect, with a specified status code. + /// + /// The url to redirect to. + /// The status code (300-308). + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect); - /// - /// Sets the http response status code, along with an optional associated description. - /// - /// The http status code. - /// Does not actually set the http response status code and description, only registers that - /// the response should use the specified code and description. The code and description will or will - /// not be used, in due time. - IPublishedRequestBuilder SetResponseStatus(int code); + /// + /// Sets the http response status code, along with an optional associated description. + /// + /// The http status code. + /// + /// Does not actually set the http response status code and description, only registers that + /// the response should use the specified code and description. The code and description will or will + /// not be used, in due time. + /// + IPublishedRequestBuilder SetResponseStatus(int code); - /// - /// Sets the no-cache value to the Cache-Control header - /// - /// True to set the header, false to not set it - IPublishedRequestBuilder SetNoCacheHeader(bool setHeader); + /// + /// Sets the no-cache value to the Cache-Control header + /// + /// True to set the header, false to not set it + IPublishedRequestBuilder SetNoCacheHeader(bool setHeader); - /// - /// Sets a list of Extensions to append to the Response.Cache object. - /// - IPublishedRequestBuilder SetCacheExtensions(IEnumerable cacheExtensions); + /// + /// Sets a list of Extensions to append to the Response.Cache object. + /// + IPublishedRequestBuilder SetCacheExtensions(IEnumerable cacheExtensions); - /// - /// Sets a dictionary of Headers to append to the Response object. - /// - IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers); + /// + /// Sets a dictionary of Headers to append to the Response object. + /// + IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers); - /// - /// Can be called to configure the result to ignore URL collisions - /// - /// - /// This is an uncommon API used for edge cases with complex routing and would be used - /// by developers to configure the request to disable collision checks in . - /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers since - /// collission checking only occurs in the back office which is launched by - /// for which events do not execute. - /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, https://issues.umbraco.org/issue/U4-10345 - /// but it's still unclear how this was used. - /// - void IgnorePublishedContentCollisions(); - } + /// + /// Can be called to configure the result to ignore URL collisions + /// + /// + /// + /// This is an uncommon API used for edge cases with complex routing and would be used + /// by developers to configure the request to disable collision checks in . + /// + /// + /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers + /// since + /// collission checking only occurs in the back office which is launched by + /// + /// for which events do not execute. + /// + /// + /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, + /// https://issues.umbraco.org/issue/U4-10345 + /// but it's still unclear how this was used. + /// + /// + void IgnorePublishedContentCollisions(); } diff --git a/src/Umbraco.Core/Routing/IPublishedRouter.cs b/src/Umbraco.Core/Routing/IPublishedRouter.cs index a3c041768f..5434c46447 100644 --- a/src/Umbraco.Core/Routing/IPublishedRouter.cs +++ b/src/Umbraco.Core/Routing/IPublishedRouter.cs @@ -1,52 +1,49 @@ -using System; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Routes requests. +/// +public interface IPublishedRouter { /// - /// Routes requests. + /// Creates a published request. /// - public interface IPublishedRouter - { - /// - /// Creates a published request. - /// - /// The current request Uri. - /// A published request builder. - Task CreateRequestAsync(Uri uri); + /// The current request Uri. + /// A published request builder. + Task CreateRequestAsync(Uri uri); - /// - /// Sends a through the routing pipeline and builds a result. - /// - /// The request. - /// The options. - /// The built instance. - Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options); + /// + /// Sends a through the routing pipeline and builds a result. + /// + /// The request. + /// The options. + /// The built instance. + Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options); - /// - /// Updates the request to use the specified item, or NULL - /// - /// The request. - /// - /// - /// A new based on values from the original - /// and with the re-routed values based on the passed in - /// - /// - /// This method is used for 2 cases: - /// - When the rendering content needs to change due to Public Access rules. - /// - When there is nothing to render due to circumstances such as no template files. In this case, NULL is used as the parameter. - /// - /// - /// This method is invoked when the pipeline decides it cannot render - /// the request, for whatever reason, and wants to force it to be re-routed - /// and rendered as if no document were found (404). - /// This occurs if there is no template found and route hijacking was not matched. - /// In that case it's the same as if there was no content which means even if there was - /// content matched we want to run the request through the last chance finders. - /// - /// - Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent); - } + /// + /// Updates the request to use the specified item, or NULL + /// + /// The request. + /// + /// + /// A new based on values from the original + /// and with the re-routed values based on the passed in + /// + /// + /// This method is used for 2 cases: + /// - When the rendering content needs to change due to Public Access rules. + /// - When there is nothing to render due to circumstances such as no template files. In this case, NULL is used as the parameter. + /// + /// + /// This method is invoked when the pipeline decides it cannot render + /// the request, for whatever reason, and wants to force it to be re-routed + /// and rendered as if no document were found (404). + /// This occurs if there is no template found and route hijacking was not matched. + /// In that case it's the same as if there was no content which means even if there was + /// content matched we want to run the request through the last chance finders. + /// + /// + Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent); } diff --git a/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs b/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs index fd52bc7805..598ad1b535 100644 --- a/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs +++ b/src/Umbraco.Core/Routing/IPublishedUrlProvider.cs @@ -1,104 +1,109 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public interface IPublishedUrlProvider { - public interface IPublishedUrlProvider - { - /// - /// Gets or sets the provider url mode. - /// - UrlMode Mode { get; set; } + /// + /// Gets or sets the provider url mode. + /// + UrlMode Mode { get; set; } - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// The url mode. - /// A culture. - /// The current absolute url. - /// The url for the published content. - string GetUrl(Guid id, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); + /// + /// Gets the url of a published content. + /// + /// The published content identifier. + /// The url mode. + /// A culture. + /// The current absolute url. + /// The url for the published content. + string GetUrl(Guid id, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// The url mode. - /// A culture. - /// The current absolute url. - /// The url for the published content. - string GetUrl(int id, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); + /// + /// Gets the url of a published content. + /// + /// The published content identifier. + /// The url mode. + /// A culture. + /// The current absolute url. + /// The url for the published content. + string GetUrl(int id, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); - /// - /// Gets the url of a published content. - /// - /// The published content. - /// The url mode. - /// A culture. - /// The current absolute url. - /// The url for the published content. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the published content is multi-lingual, gets the url for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns "#". - /// - string GetUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); + /// + /// Gets the url of a published content. + /// + /// The published content. + /// The url mode. + /// A culture. + /// The current absolute url. + /// The url for the published content. + /// + /// The url is absolute or relative depending on mode and on current. + /// + /// If the published content is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// + /// If the provider is unable to provide a url, it returns "#". + /// + string GetUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null); - string GetUrlFromRoute(int id, string? route, string? culture); + string GetUrlFromRoute(int id, string? route, string? culture); - /// - /// Gets the other urls of a published content. - /// - /// The published content id. - /// The other urls for the published content. - /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// The results depend on the current url. - /// - IEnumerable GetOtherUrls(int id); + /// + /// Gets the other urls of a published content. + /// + /// The published content id. + /// The other urls for the published content. + /// + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// The results depend on the current url. + /// + IEnumerable GetOtherUrls(int id); - /// - /// Gets the other urls of a published content. - /// - /// The published content id. - /// The current absolute url. - /// The other urls for the published content. - /// - /// Other urls are those that GetUrl would not return in the current context, but would be valid - /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// - IEnumerable GetOtherUrls(int id, Uri current); + /// + /// Gets the other urls of a published content. + /// + /// The published content id. + /// The current absolute url. + /// The other urls for the published content. + /// + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + IEnumerable GetOtherUrls(int id, Uri current); - /// - /// Gets the url of a media item. - /// - /// - /// - /// - /// - /// - /// - string GetMediaUrl(Guid id, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); + /// + /// Gets the url of a media item. + /// + /// + /// + /// + /// + /// + /// + string GetMediaUrl(Guid id, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); - /// - /// Gets the url of a media item. - /// - /// The published content. - /// The property alias to resolve the url from. - /// The url mode. - /// The variation language. - /// The current absolute url. - /// The url for the media. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the url for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns . - /// - string GetMediaUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); - } + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// The url mode. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// + /// If the provider is unable to provide a url, it returns . + /// + string GetMediaUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null); } diff --git a/src/Umbraco.Core/Routing/ISiteDomainMapper.cs b/src/Umbraco.Core/Routing/ISiteDomainMapper.cs index e9ca34477c..93afe32d93 100644 --- a/src/Umbraco.Core/Routing/ISiteDomainMapper.cs +++ b/src/Umbraco.Core/Routing/ISiteDomainMapper.cs @@ -1,45 +1,47 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Routing +/// +/// Provides utilities to handle site domains. +/// +public interface ISiteDomainMapper { /// - /// Provides utilities to handle site domains. + /// Filters a list of DomainAndUri to pick one that best matches the current request. /// - public interface ISiteDomainMapper - { - /// - /// Filters a list of DomainAndUri to pick one that best matches the current request. - /// - /// The list of DomainAndUri to filter. - /// The Uri of the current request. - /// A culture. - /// The default culture. - /// The selected DomainAndUri. - /// - /// If the filter is invoked then is _not_ empty and - /// is _not_ null, and could not be - /// matched with anything in . - /// The may be null, but when non-null, it can be used - /// to help pick the best matches. - /// The filter _must_ return something else an exception will be thrown. - /// - DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, Uri current, string? culture, string? defaultCulture); + /// The list of DomainAndUri to filter. + /// The Uri of the current request. + /// A culture. + /// The default culture. + /// The selected DomainAndUri. + /// + /// + /// If the filter is invoked then is _not_ empty and + /// is _not_ null, and could not be + /// matched with anything in . + /// + /// + /// The may be null, but when non-null, it can be used + /// to help pick the best matches. + /// + /// The filter _must_ return something else an exception will be thrown. + /// + DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, Uri current, string? culture, string? defaultCulture); - /// - /// Filters a list of DomainAndUri to pick those that best matches the current request. - /// - /// The list of DomainAndUri to filter. - /// The Uri of the current request. - /// A value indicating whether to exclude the current/default domain. - /// A culture. - /// The default culture. - /// The selected DomainAndUri items. - /// - /// The filter must return something, even empty, else an exception will be thrown. - /// The may be null, but when non-null, it can be used - /// to help pick the best matches. - /// - IEnumerable MapDomains(IReadOnlyCollection domainAndUris, Uri current, bool excludeDefault, string? culture, string? defaultCulture); - } + /// + /// Filters a list of DomainAndUri to pick those that best matches the current request. + /// + /// The list of DomainAndUri to filter. + /// The Uri of the current request. + /// A value indicating whether to exclude the current/default domain. + /// A culture. + /// The default culture. + /// The selected DomainAndUri items. + /// + /// The filter must return something, even empty, else an exception will be thrown. + /// + /// The may be null, but when non-null, it can be used + /// to help pick the best matches. + /// + /// + IEnumerable MapDomains(IReadOnlyCollection domainAndUris, Uri current, bool excludeDefault, string? culture, string? defaultCulture); } diff --git a/src/Umbraco.Core/Routing/IUrlProvider.cs b/src/Umbraco.Core/Routing/IUrlProvider.cs index 0223b39c1d..38f28b3764 100644 --- a/src/Umbraco.Core/Routing/IUrlProvider.cs +++ b/src/Umbraco.Core/Routing/IUrlProvider.cs @@ -1,40 +1,41 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides URLs. +/// +public interface IUrlProvider { /// - /// Provides URLs. + /// Gets the URL of a published content. /// - public interface IUrlProvider - { - /// - /// Gets the URL of a published content. - /// - /// The published content. - /// The URL mode. - /// A culture. - /// The current absolute URL. - /// The URL for the published content. - /// - /// The URL is absolute or relative depending on mode and on current. - /// If the published content is multi-lingual, gets the URL for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a URL, it should return null. - /// - UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current); + /// The published content. + /// The URL mode. + /// A culture. + /// The current absolute URL. + /// The URL for the published content. + /// + /// The URL is absolute or relative depending on mode and on current. + /// + /// If the published content is multi-lingual, gets the URL for the specified culture or, + /// when no culture is specified, the current culture. + /// + /// If the provider is unable to provide a URL, it should return null. + /// + UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current); - /// - /// Gets the other URLs of a published content. - /// - /// The published content id. - /// The current absolute URL. - /// The other URLs for the published content. - /// - /// Other URLs are those that GetUrl would not return in the current context, but would be valid - /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). - /// - IEnumerable GetOtherUrls(int id, Uri current); - } + /// + /// Gets the other URLs of a published content. + /// + /// The published content id. + /// The current absolute URL. + /// The other URLs for the published content. + /// + /// + /// Other URLs are those that GetUrl would not return in the current context, but would be valid + /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + /// + IEnumerable GetOtherUrls(int id, Uri current); } diff --git a/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs b/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs index 264be41d60..85b864d717 100644 --- a/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs +++ b/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class MediaUrlProviderCollection : BuilderCollectionBase { - public class MediaUrlProviderCollection : BuilderCollectionBase + public MediaUrlProviderCollection(Func> items) + : base(items) { - public MediaUrlProviderCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Routing/MediaUrlProviderCollectionBuilder.cs b/src/Umbraco.Core/Routing/MediaUrlProviderCollectionBuilder.cs index d778540e31..ba0a9b9fc2 100644 --- a/src/Umbraco.Core/Routing/MediaUrlProviderCollectionBuilder.cs +++ b/src/Umbraco.Core/Routing/MediaUrlProviderCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class MediaUrlProviderCollectionBuilder : OrderedCollectionBuilderBase { - public class MediaUrlProviderCollectionBuilder : OrderedCollectionBuilderBase - { - protected override MediaUrlProviderCollectionBuilder This => this; - } + protected override MediaUrlProviderCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Routing/PublishedRequest.cs b/src/Umbraco.Core/Routing/PublishedRequest.cs index 50328cbfdd..e3fc3818ef 100644 --- a/src/Umbraco.Core/Routing/PublishedRequest.cs +++ b/src/Umbraco.Core/Routing/PublishedRequest.cs @@ -1,70 +1,79 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class PublishedRequest : IPublishedRequest { - - public class PublishedRequest : IPublishedRequest + /// + /// Initializes a new instance of the class. + /// + public PublishedRequest( + Uri uri, + string absolutePathDecoded, + IPublishedContent? publishedContent, + bool isInternalRedirect, + ITemplate? template, + DomainAndUri? domain, + string? culture, + string? redirectUrl, + int? responseStatusCode, + IReadOnlyList? cacheExtensions, + IReadOnlyDictionary? headers, + bool setNoCacheHeader, + bool ignorePublishedContentCollisions) { - /// - /// Initializes a new instance of the class. - /// - public PublishedRequest(Uri uri, string absolutePathDecoded, IPublishedContent? publishedContent, bool isInternalRedirect, ITemplate? template, DomainAndUri? domain, string? culture, string? redirectUrl, int? responseStatusCode, IReadOnlyList? cacheExtensions, IReadOnlyDictionary? headers, bool setNoCacheHeader, bool ignorePublishedContentCollisions) - { - Uri = uri ?? throw new ArgumentNullException(nameof(uri)); - AbsolutePathDecoded = absolutePathDecoded ?? throw new ArgumentNullException(nameof(absolutePathDecoded)); - PublishedContent = publishedContent; - IsInternalRedirect = isInternalRedirect; - Template = template; - Domain = domain; - Culture = culture; - RedirectUrl = redirectUrl; - ResponseStatusCode = responseStatusCode; - CacheExtensions = cacheExtensions; - Headers = headers; - SetNoCacheHeader = setNoCacheHeader; - IgnorePublishedContentCollisions = ignorePublishedContentCollisions; - } - - /// - public Uri Uri { get; } - - /// - public string AbsolutePathDecoded { get; } - - /// - public bool IgnorePublishedContentCollisions { get; } - - /// - public IPublishedContent? PublishedContent { get; } - - /// - public bool IsInternalRedirect { get; } - - /// - public ITemplate? Template { get; } - - /// - public DomainAndUri? Domain { get; } - - /// - public string? Culture { get; } - - /// - public string? RedirectUrl { get; } - - /// - public int? ResponseStatusCode { get; } - - /// - public IReadOnlyList? CacheExtensions { get; } - - /// - public IReadOnlyDictionary? Headers { get; } - - /// - public bool SetNoCacheHeader { get; } + Uri = uri ?? throw new ArgumentNullException(nameof(uri)); + AbsolutePathDecoded = absolutePathDecoded ?? throw new ArgumentNullException(nameof(absolutePathDecoded)); + PublishedContent = publishedContent; + IsInternalRedirect = isInternalRedirect; + Template = template; + Domain = domain; + Culture = culture; + RedirectUrl = redirectUrl; + ResponseStatusCode = responseStatusCode; + CacheExtensions = cacheExtensions; + Headers = headers; + SetNoCacheHeader = setNoCacheHeader; + IgnorePublishedContentCollisions = ignorePublishedContentCollisions; } + + /// + public Uri Uri { get; } + + /// + public string AbsolutePathDecoded { get; } + + /// + public bool IgnorePublishedContentCollisions { get; } + + /// + public IPublishedContent? PublishedContent { get; } + + /// + public bool IsInternalRedirect { get; } + + /// + public ITemplate? Template { get; } + + /// + public DomainAndUri? Domain { get; } + + /// + public string? Culture { get; } + + /// + public string? RedirectUrl { get; } + + /// + public int? ResponseStatusCode { get; } + + /// + public IReadOnlyList? CacheExtensions { get; } + + /// + public IReadOnlyDictionary? Headers { get; } + + /// + public bool SetNoCacheHeader { get; } } diff --git a/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs b/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs index 128c81f605..180033dd33 100644 --- a/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs +++ b/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs @@ -1,204 +1,200 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class PublishedRequestBuilder : IPublishedRequestBuilder { - public class PublishedRequestBuilder : IPublishedRequestBuilder + private readonly IFileService _fileService; + private bool _cacheability; + private IReadOnlyList? _cacheExtensions; + private IReadOnlyDictionary? _headers; + private bool _ignorePublishedContentCollisions; + private IPublishedContent? _publishedContent; + private string? _redirectUrl; + private HttpStatusCode? _responseStatus; + + /// + /// Initializes a new instance of the class. + /// + public PublishedRequestBuilder(Uri uri, IFileService fileService) { - private readonly IFileService _fileService; - private IReadOnlyDictionary? _headers; - private bool _cacheability; - private IReadOnlyList? _cacheExtensions; - private string? _redirectUrl; - private HttpStatusCode? _responseStatus; - private IPublishedContent? _publishedContent; - private bool _ignorePublishedContentCollisions; + Uri = uri; + AbsolutePathDecoded = uri.GetAbsolutePathDecoded(); + _fileService = fileService; + } - /// - /// Initializes a new instance of the class. - /// - public PublishedRequestBuilder(Uri uri, IFileService fileService) + /// + public Uri Uri { get; } + + /// + public string AbsolutePathDecoded { get; } + + /// + public DomainAndUri? Domain { get; private set; } + + /// + public string? Culture { get; private set; } + + /// + public ITemplate? Template { get; private set; } + + /// + public bool IsInternalRedirect { get; private set; } + + /// + public int? ResponseStatusCode => _responseStatus.HasValue ? (int?)_responseStatus : null; + + /// + public IPublishedContent? PublishedContent + { + get => _publishedContent; + private set { - Uri = uri; - AbsolutePathDecoded = uri.GetAbsolutePathDecoded(); - _fileService = fileService; - } - - /// - public Uri Uri { get; } - - /// - public string AbsolutePathDecoded { get; } - - /// - public DomainAndUri? Domain { get; private set; } - - /// - public string? Culture { get; private set; } - - /// - public ITemplate? Template { get; private set; } - - /// - public bool IsInternalRedirect { get; private set; } - - /// - public int? ResponseStatusCode => _responseStatus.HasValue ? (int?)_responseStatus : null; - - /// - public IPublishedContent? PublishedContent - { - get => _publishedContent; - private set - { - _publishedContent = value; - IsInternalRedirect = false; - Template = null; - } - } - - /// - public IPublishedRequest Build() => new PublishedRequest( - Uri, - AbsolutePathDecoded, - PublishedContent, - IsInternalRedirect, - Template, - Domain, - Culture, - _redirectUrl, - _responseStatus.HasValue ? (int?)_responseStatus : null, - _cacheExtensions, - _headers, - _cacheability, - _ignorePublishedContentCollisions); - - /// - public IPublishedRequestBuilder SetNoCacheHeader(bool cacheability) - { - _cacheability = cacheability; - return this; - } - - /// - public IPublishedRequestBuilder SetCacheExtensions(IEnumerable cacheExtensions) - { - _cacheExtensions = cacheExtensions.ToList(); - return this; - } - - /// - public IPublishedRequestBuilder SetCulture(string? culture) - { - Culture = culture; - return this; - } - - /// - public IPublishedRequestBuilder SetDomain(DomainAndUri domain) - { - Domain = domain; - SetCulture(domain.Culture); - return this; - } - - /// - public IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers) - { - _headers = headers; - return this; - } - - /// - public IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content) - { - // unless a template has been set already by the finder, - // template should be null at that point. - - // redirecting to self - if (PublishedContent != null && content.Id == PublishedContent.Id) - { - // no need to set PublishedContent, we're done - IsInternalRedirect = true; - return this; - } - - // else - - // set published content - this resets the template, and sets IsInternalRedirect to false - PublishedContent = content; - IsInternalRedirect = true; - - return this; - } - - /// - public IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content) - { - PublishedContent = content; + _publishedContent = value; IsInternalRedirect = false; + Template = null; + } + } + + /// + public IPublishedRequest Build() => new PublishedRequest( + Uri, + AbsolutePathDecoded, + PublishedContent, + IsInternalRedirect, + Template, + Domain, + Culture, + _redirectUrl, + _responseStatus.HasValue ? (int?)_responseStatus : null, + _cacheExtensions, + _headers, + _cacheability, + _ignorePublishedContentCollisions); + + /// + public IPublishedRequestBuilder SetNoCacheHeader(bool cacheability) + { + _cacheability = cacheability; + return this; + } + + /// + public IPublishedRequestBuilder SetCacheExtensions(IEnumerable cacheExtensions) + { + _cacheExtensions = cacheExtensions.ToList(); + return this; + } + + /// + public IPublishedRequestBuilder SetCulture(string? culture) + { + Culture = culture; + return this; + } + + /// + public IPublishedRequestBuilder SetDomain(DomainAndUri domain) + { + Domain = domain; + SetCulture(domain.Culture); + return this; + } + + /// + public IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers) + { + _headers = headers; + return this; + } + + /// + public IPublishedRequestBuilder SetInternalRedirect(IPublishedContent content) + { + // unless a template has been set already by the finder, + // template should be null at that point. + + // redirecting to self + if (PublishedContent != null && content.Id == PublishedContent.Id) + { + // no need to set PublishedContent, we're done + IsInternalRedirect = true; return this; } - /// - public IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect) + // else + + // set published content - this resets the template, and sets IsInternalRedirect to false + PublishedContent = content; + IsInternalRedirect = true; + + return this; + } + + /// + public IPublishedRequestBuilder SetPublishedContent(IPublishedContent? content) + { + PublishedContent = content; + IsInternalRedirect = false; + return this; + } + + /// + public IPublishedRequestBuilder SetRedirect(string url, int status = (int)HttpStatusCode.Redirect) + { + _redirectUrl = url; + _responseStatus = (HttpStatusCode)status; + return this; + } + + /// + public IPublishedRequestBuilder SetRedirectPermanent(string url) + { + _redirectUrl = url; + _responseStatus = HttpStatusCode.Moved; + return this; + } + + /// + public IPublishedRequestBuilder SetResponseStatus(int code) + { + _responseStatus = (HttpStatusCode)code; + return this; + } + + /// + public IPublishedRequestBuilder SetTemplate(ITemplate? template) + { + Template = template; + return this; + } + + /// + public bool TrySetTemplate(string alias) + { + if (string.IsNullOrWhiteSpace(alias)) { - _redirectUrl = url; - _responseStatus = (HttpStatusCode)status; - return this; - } - - /// - public IPublishedRequestBuilder SetRedirectPermanent(string url) - { - _redirectUrl = url; - _responseStatus = HttpStatusCode.Moved; - return this; - } - - /// - public IPublishedRequestBuilder SetResponseStatus(int code) - { - _responseStatus = (HttpStatusCode)code; - return this; - } - - /// - public IPublishedRequestBuilder SetTemplate(ITemplate? template) - { - Template = template; - return this; - } - - /// - public bool TrySetTemplate(string alias) - { - if (string.IsNullOrWhiteSpace(alias)) - { - Template = null; - return true; - } - - // NOTE - can we still get it with whitespaces in it due to old legacy bugs? - alias = alias.Replace(" ", string.Empty); - - ITemplate? model = _fileService.GetTemplate(alias); - if (model == null) - { - return false; - } - - Template = model; + Template = null; return true; } - /// - public void IgnorePublishedContentCollisions() => _ignorePublishedContentCollisions = true; + // NOTE - can we still get it with whitespaces in it due to old legacy bugs? + alias = alias.Replace(" ", string.Empty); + + ITemplate? model = _fileService.GetTemplate(alias); + if (model == null) + { + return false; + } + + Template = model; + return true; } + + /// + public void IgnorePublishedContentCollisions() => _ignorePublishedContentCollisions = true; } diff --git a/src/Umbraco.Core/Routing/PublishedRequestExtensions.cs b/src/Umbraco.Core/Routing/PublishedRequestExtensions.cs index 855bd53bde..6b9720e4ac 100644 --- a/src/Umbraco.Core/Routing/PublishedRequestExtensions.cs +++ b/src/Umbraco.Core/Routing/PublishedRequestExtensions.cs @@ -1,97 +1,102 @@ using System.Net; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public static class PublishedRequestExtensions { - - public static class PublishedRequestExtensions + /// + /// Gets the + /// + public static UmbracoRouteResult GetRouteResult(this IPublishedRequest publishedRequest) { - /// - /// Gets the - /// - public static UmbracoRouteResult GetRouteResult(this IPublishedRequest publishedRequest) + if (publishedRequest.IsRedirect()) { - if (publishedRequest.IsRedirect()) - { - return UmbracoRouteResult.Redirect; - } - - if (!publishedRequest.HasPublishedContent()) - { - return UmbracoRouteResult.NotFound; - } - - return UmbracoRouteResult.Success; + return UmbracoRouteResult.Redirect; } - /// - /// Gets a value indicating whether the request was successfully routed - /// - public static bool Success(this IPublishedRequest publishedRequest) - => !publishedRequest.IsRedirect() && publishedRequest.HasPublishedContent(); - - /// - /// Sets the response status to be 404 not found - /// - public static IPublishedRequestBuilder SetIs404(this IPublishedRequestBuilder publishedRequest) + if (!publishedRequest.HasPublishedContent()) { - publishedRequest.SetResponseStatus((int)HttpStatusCode.NotFound); - return publishedRequest; + return UmbracoRouteResult.NotFound; } - /// - /// Gets a value indicating whether the content request has a content. - /// - public static bool HasPublishedContent(this IPublishedRequestBuilder publishedRequest) => publishedRequest.PublishedContent != null; - - /// - /// Gets a value indicating whether the content request has a content. - /// - public static bool HasPublishedContent(this IPublishedRequest publishedRequest) => publishedRequest.PublishedContent != null; - - /// - /// Gets a value indicating whether the content request has a template. - /// - public static bool HasTemplate(this IPublishedRequestBuilder publishedRequest) => publishedRequest.Template != null; - - /// - /// Gets a value indicating whether the content request has a template. - /// - public static bool HasTemplate(this IPublishedRequest publishedRequest) => publishedRequest.Template != null; - - /// - /// Gets the alias of the template to use to display the requested content. - /// - public static string? GetTemplateAlias(this IPublishedRequest publishedRequest) => publishedRequest.Template?.Alias; - - /// - /// Gets a value indicating whether the requested content could not be found. - /// - public static bool Is404(this IPublishedRequest publishedRequest) => publishedRequest.ResponseStatusCode == (int)HttpStatusCode.NotFound; - - /// - /// Gets a value indicating whether the content request triggers a redirect (permanent or not). - /// - public static bool IsRedirect(this IPublishedRequestBuilder publishedRequest) => publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Redirect || publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; - - /// - /// Gets indicating whether the content request triggers a redirect (permanent or not). - /// - public static bool IsRedirect(this IPublishedRequest publishedRequest) => publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Redirect || publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; - - /// - /// Gets a value indicating whether the redirect is permanent. - /// - public static bool IsRedirectPermanent(this IPublishedRequest publishedRequest) => publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; - - /// - /// Gets a value indicating whether the content request has a domain. - /// - public static bool HasDomain(this IPublishedRequestBuilder publishedRequest) => publishedRequest.Domain != null; - - /// - /// Gets a value indicating whether the content request has a domain. - /// - public static bool HasDomain(this IPublishedRequest publishedRequest) => publishedRequest.Domain != null; - + return UmbracoRouteResult.Success; } + + /// + /// Gets a value indicating whether the request was successfully routed + /// + public static bool Success(this IPublishedRequest publishedRequest) + => !publishedRequest.IsRedirect() && publishedRequest.HasPublishedContent(); + + /// + /// Sets the response status to be 404 not found + /// + public static IPublishedRequestBuilder SetIs404(this IPublishedRequestBuilder publishedRequest) + { + publishedRequest.SetResponseStatus((int)HttpStatusCode.NotFound); + return publishedRequest; + } + + /// + /// Gets a value indicating whether the content request has a content. + /// + public static bool HasPublishedContent(this IPublishedRequestBuilder publishedRequest) => + publishedRequest.PublishedContent != null; + + /// + /// Gets a value indicating whether the content request has a content. + /// + public static bool HasPublishedContent(this IPublishedRequest publishedRequest) => + publishedRequest.PublishedContent != null; + + /// + /// Gets a value indicating whether the content request has a template. + /// + public static bool HasTemplate(this IPublishedRequestBuilder publishedRequest) => publishedRequest.Template != null; + + /// + /// Gets a value indicating whether the content request has a template. + /// + public static bool HasTemplate(this IPublishedRequest publishedRequest) => publishedRequest.Template != null; + + /// + /// Gets the alias of the template to use to display the requested content. + /// + public static string? GetTemplateAlias(this IPublishedRequest publishedRequest) => publishedRequest.Template?.Alias; + + /// + /// Gets a value indicating whether the requested content could not be found. + /// + public static bool Is404(this IPublishedRequest publishedRequest) => + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.NotFound; + + /// + /// Gets a value indicating whether the content request triggers a redirect (permanent or not). + /// + public static bool IsRedirect(this IPublishedRequestBuilder publishedRequest) => + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Redirect || + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; + + /// + /// Gets indicating whether the content request triggers a redirect (permanent or not). + /// + public static bool IsRedirect(this IPublishedRequest publishedRequest) => + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Redirect || + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; + + /// + /// Gets a value indicating whether the redirect is permanent. + /// + public static bool IsRedirectPermanent(this IPublishedRequest publishedRequest) => + publishedRequest.ResponseStatusCode == (int)HttpStatusCode.Moved; + + /// + /// Gets a value indicating whether the content request has a domain. + /// + public static bool HasDomain(this IPublishedRequestBuilder publishedRequest) => publishedRequest.Domain != null; + + /// + /// Gets a value indicating whether the content request has a domain. + /// + public static bool HasDomain(this IPublishedRequest publishedRequest) => publishedRequest.Domain != null; } diff --git a/src/Umbraco.Core/Routing/PublishedRequestOld.cs b/src/Umbraco.Core/Routing/PublishedRequestOld.cs index 44a75aaccd..c7167971df 100644 --- a/src/Umbraco.Core/Routing/PublishedRequestOld.cs +++ b/src/Umbraco.Core/Routing/PublishedRequestOld.cs @@ -1,392 +1,412 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading; +using System.Globalization; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +// TODO: Kill this, but we need to port all of it's functionality +public class PublishedRequestOld // : IPublishedRequest { - // TODO: Kill this, but we need to port all of it's functionality - public class PublishedRequestOld // : IPublishedRequest + private readonly IPublishedRouter _publishedRouter; + private readonly WebRoutingSettings _webRoutingSettings; + private CultureInfo? _culture; + private DomainAndUri? _domain; + private bool _is404; + private IPublishedContent? _publishedContent; + + private bool _readonly; // after prepared + + /// + /// Initializes a new instance of the class. + /// + public PublishedRequestOld(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IOptions webRoutingSettings, Uri? uri = null) { - private readonly IPublishedRouter _publishedRouter; - private readonly WebRoutingSettings _webRoutingSettings; - - private bool _readonly; // after prepared - private bool _is404; - private DomainAndUri? _domain; - private CultureInfo? _culture; - private IPublishedContent? _publishedContent; - private IPublishedContent? _initialPublishedContent; // found by finders before 404, redirects, etc - - /// - /// Initializes a new instance of the class. - /// - public PublishedRequestOld(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IOptions webRoutingSettings, Uri? uri = null) - { - UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); - _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); - _webRoutingSettings = webRoutingSettings.Value; - Uri = uri ?? umbracoContext.CleanedUmbracoUrl; - } - - /// - /// Gets the UmbracoContext. - /// - public IUmbracoContext UmbracoContext { get; } - - /// - /// Gets or sets the cleaned up Uri used for routing. - /// - /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. - public Uri Uri { get; } - - // utility for ensuring it is ok to set some properties - public void EnsureWriteable() - { - if (_readonly) - { - throw new InvalidOperationException("Cannot modify a PublishedRequest once it is read-only."); - } - } - - public bool CacheabilityNoCache { get; set; } - - ///// - ///// Prepares the request. - ///// - //public void Prepare() - //{ - // _publishedRouter.PrepareRequest(this); - //} - - /// - /// Gets or sets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. - /// - public bool IgnorePublishedContentCollisions { get; set; } - - //#region Events - - ///// - ///// Triggers before the published content request is prepared. - ///// - ///// When the event triggers, no preparation has been done. It is still possible to - ///// modify the request's Uri property, for example to restore its original, public-facing value - ///// that might have been modified by an in-between equipment such as a load-balancer. - //public static event EventHandler Preparing; - - ///// - ///// Triggers once the published content request has been prepared, but before it is processed. - ///// - ///// When the event triggers, preparation is done ie domain, culture, document, template, - ///// rendering engine, etc. have been setup. It is then possible to change anything, before - ///// the request is actually processed and rendered by Umbraco. - //public static event EventHandler Prepared; - - ///// - ///// Triggers the Preparing event. - ///// - //public void OnPreparing() - //{ - // Preparing?.Invoke(this, EventArgs.Empty); - //} - - ///// - ///// Triggers the Prepared event. - ///// - //public void OnPrepared() - //{ - // Prepared?.Invoke(this, EventArgs.Empty); - - // if (HasPublishedContent == false) - // Is404 = true; // safety - - // _readonly = true; - //} - - //#endregion - - #region PublishedContent - - ///// - ///// Gets or sets the requested content. - ///// - ///// Setting the requested content clears Template. - //public IPublishedContent PublishedContent - //{ - // get { return _publishedContent; } - // set - // { - // EnsureWriteable(); - // _publishedContent = value; - // IsInternalRedirectPublishedContent = false; - // TemplateModel = null; - // } - //} - - /// - /// Sets the requested content, following an internal redirect. - /// - /// The requested content. - /// Depending on UmbracoSettings.InternalRedirectPreservesTemplate, will - /// preserve or reset the template, if any. - public void SetInternalRedirectPublishedContent(IPublishedContent content) - { - //if (content == null) - // throw new ArgumentNullException(nameof(content)); - //EnsureWriteable(); - - //// unless a template has been set already by the finder, - //// template should be null at that point. - - //// IsInternalRedirect if IsInitial, or already IsInternalRedirect - //var isInternalRedirect = IsInitialPublishedContent || IsInternalRedirectPublishedContent; - - //// redirecting to self - //if (content.Id == PublishedContent.Id) // neither can be null - //{ - // // no need to set PublishedContent, we're done - // IsInternalRedirectPublishedContent = isInternalRedirect; - // return; - //} - - //// else - - //// save - //var template = Template; - - //// set published content - this resets the template, and sets IsInternalRedirect to false - //PublishedContent = content; - //IsInternalRedirectPublishedContent = isInternalRedirect; - - //// must restore the template if it's an internal redirect & the config option is set - //if (isInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) - //{ - // // restore - // TemplateModel = template; - //} - } - - /// - /// Gets the initial requested content. - /// - /// The initial requested content is the content that was found by the finders, - /// before anything such as 404, redirect... took place. - public IPublishedContent? InitialPublishedContent => _initialPublishedContent; - - /// - /// Gets value indicating whether the current published content is the initial one. - /// - public bool IsInitialPublishedContent => _initialPublishedContent != null && _initialPublishedContent == _publishedContent; - - /// - /// Indicates that the current PublishedContent is the initial one. - /// - public void SetIsInitialPublishedContent() - { - EnsureWriteable(); - - // note: it can very well be null if the initial content was not found - _initialPublishedContent = _publishedContent; - IsInternalRedirectPublishedContent = false; - } - - /// - /// Gets or sets a value indicating whether the current published content has been obtained - /// from the initial published content following internal redirections exclusively. - /// - /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to - /// apply the internal redirect or not, when content is not the initial content. - public bool IsInternalRedirectPublishedContent { get; private set; } - - - #endregion - - /// - /// Gets or sets the template model to use to display the requested content. - /// - public ITemplate? Template { get; } - - /// - /// Gets the alias of the template to use to display the requested content. - /// - public string? TemplateAlias => Template?.Alias; - - - /// - /// Gets or sets the content request's domain. - /// - /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, - /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg "http://example.com/". - public DomainAndUri? Domain - { - get { return _domain; } - set - { - EnsureWriteable(); - _domain = value; - } - } - - /// - /// Gets a value indicating whether the content request has a domain. - /// - public bool HasDomain => Domain != null; - - /// - /// Gets or sets the content request's culture. - /// - public CultureInfo Culture - { - get { return _culture ?? Thread.CurrentThread.CurrentCulture; } - set - { - EnsureWriteable(); - _culture = value; - } - } - - // note: do we want to have an ordered list of alternate cultures, - // to allow for fallbacks when doing dictionary lookup and such? - - - #region Status - - /// - /// Gets or sets a value indicating whether the requested content could not be found. - /// - /// This is set in the PublishedContentRequestBuilder and can also be used in - /// custom content finders or Prepared event handlers, where we want to allow developers - /// to indicate a request is 404 but not to cancel it. - public bool Is404 - { - get { return _is404; } - set - { - EnsureWriteable(); - _is404 = value; - } - } - - /// - /// Gets a value indicating whether the content request triggers a redirect (permanent or not). - /// - public bool IsRedirect => string.IsNullOrWhiteSpace(RedirectUrl) == false; - - /// - /// Gets or sets a value indicating whether the redirect is permanent. - /// - public bool IsRedirectPermanent { get; private set; } - - /// - /// Gets or sets the URL to redirect to, when the content request triggers a redirect. - /// - public string? RedirectUrl { get; private set; } - - /// - /// Indicates that the content request should trigger a redirect (302). - /// - /// The URL to redirect to. - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - public void SetRedirect(string url) - { - EnsureWriteable(); - RedirectUrl = url; - IsRedirectPermanent = false; - } - - /// - /// Indicates that the content request should trigger a permanent redirect (301). - /// - /// The URL to redirect to. - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - public void SetRedirectPermanent(string url) - { - EnsureWriteable(); - RedirectUrl = url; - IsRedirectPermanent = true; - } - - /// - /// Indicates that the content request should trigger a redirect, with a specified status code. - /// - /// The URL to redirect to. - /// The status code (300-308). - /// Does not actually perform a redirect, only registers that the response should - /// redirect. Redirect will or will not take place in due time. - public void SetRedirect(string url, int status) - { - EnsureWriteable(); - - if (status < 300 || status > 308) - throw new ArgumentOutOfRangeException(nameof(status), "Valid redirection status codes 300-308."); - - RedirectUrl = url; - IsRedirectPermanent = (status == 301 || status == 308); - if (status != 301 && status != 302) // default redirect statuses - ResponseStatusCode = status; - } - - /// - /// Gets or sets the content request http response status code. - /// - /// Does not actually set the http response status code, only registers that the response - /// should use the specified code. The code will or will not be used, in due time. - public int ResponseStatusCode { get; private set; } - - /// - /// Gets or sets the content request http response status description. - /// - /// Does not actually set the http response status description, only registers that the response - /// should use the specified description. The description will or will not be used, in due time. - public string? ResponseStatusDescription { get; private set; } - - /// - /// Sets the http response status code, along with an optional associated description. - /// - /// The http status code. - /// The description. - /// Does not actually set the http response status code and description, only registers that - /// the response should use the specified code and description. The code and description will or will - /// not be used, in due time. - public void SetResponseStatus(int code, string? description = null) - { - EnsureWriteable(); - - // .Status is deprecated - // .SubStatusCode is IIS 7+ internal, ignore - ResponseStatusCode = code; - ResponseStatusDescription = description; - } - - #endregion - - #region Response Cache - - /// - /// Gets or sets the System.Web.HttpCacheability - /// - // Note: we used to set a default value here but that would then be the default - // for ALL requests, we shouldn't overwrite it though if people are using [OutputCache] for example - // see: https://our.umbraco.com/forum/using-umbraco-and-getting-started/79715-output-cache-in-umbraco-752 - //public HttpCacheability Cacheability { get; set; } - - /// - /// Gets or sets a list of Extensions to append to the Response.Cache object. - /// - public List CacheExtensions { get; set; } = new List(); - - /// - /// Gets or sets a dictionary of Headers to append to the Response object. - /// - public Dictionary Headers { get; set; } = new Dictionary(); - - #endregion + UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); + _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); + _webRoutingSettings = webRoutingSettings.Value; + Uri = uri ?? umbracoContext.CleanedUmbracoUrl; } + + /// + /// Gets the UmbracoContext. + /// + public IUmbracoContext UmbracoContext { get; } + + /// + /// Gets or sets the cleaned up Uri used for routing. + /// + /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. + public Uri Uri { get; } + + public bool CacheabilityNoCache { get; set; } + + ///// + ///// Prepares the request. + ///// + // public void Prepare() + // { + // _publishedRouter.PrepareRequest(this); + // } + + /// + /// Gets or sets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. + /// + public bool IgnorePublishedContentCollisions { get; set; } + + /// + /// Gets or sets the template model to use to display the requested content. + /// + public ITemplate? Template { get; } + + /// + /// Gets the alias of the template to use to display the requested content. + /// + public string? TemplateAlias => Template?.Alias; + + /// + /// Gets or sets the content request's domain. + /// + /// + /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, + /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg + /// "http://example.com/". + /// + public DomainAndUri? Domain + { + get => _domain; + set + { + EnsureWriteable(); + _domain = value; + } + } + + /// + /// Gets a value indicating whether the content request has a domain. + /// + public bool HasDomain => Domain != null; + + /// + /// Gets or sets the content request's culture. + /// + public CultureInfo Culture + { + get => _culture ?? Thread.CurrentThread.CurrentCulture; + set + { + EnsureWriteable(); + _culture = value; + } + } + + // utility for ensuring it is ok to set some properties + public void EnsureWriteable() + { + if (_readonly) + { + throw new InvalidOperationException("Cannot modify a PublishedRequest once it is read-only."); + } + } + + // #region Events + + ///// + ///// Triggers before the published content request is prepared. + ///// + ///// When the event triggers, no preparation has been done. It is still possible to + ///// modify the request's Uri property, for example to restore its original, public-facing value + ///// that might have been modified by an in-between equipment such as a load-balancer. + // public static event EventHandler Preparing; + + ///// + ///// Triggers once the published content request has been prepared, but before it is processed. + ///// + ///// When the event triggers, preparation is done ie domain, culture, document, template, + ///// rendering engine, etc. have been setup. It is then possible to change anything, before + ///// the request is actually processed and rendered by Umbraco. + // public static event EventHandler Prepared; + + ///// + ///// Triggers the Preparing event. + ///// + // public void OnPreparing() + // { + // Preparing?.Invoke(this, EventArgs.Empty); + // } + + ///// + ///// Triggers the Prepared event. + ///// + // public void OnPrepared() + // { + // Prepared?.Invoke(this, EventArgs.Empty); + + // if (HasPublishedContent == false) + // Is404 = true; // safety + + // _readonly = true; + // } + + // #endregion + #region PublishedContent + + ///// + ///// Gets or sets the requested content. + ///// + ///// Setting the requested content clears Template. + // public IPublishedContent PublishedContent + // { + // get { return _publishedContent; } + // set + // { + // EnsureWriteable(); + // _publishedContent = value; + // IsInternalRedirectPublishedContent = false; + // TemplateModel = null; + // } + // } + + /// + /// Sets the requested content, following an internal redirect. + /// + /// The requested content. + /// + /// Depending on UmbracoSettings.InternalRedirectPreservesTemplate, will + /// preserve or reset the template, if any. + /// + public void SetInternalRedirectPublishedContent(IPublishedContent content) + { + // if (content == null) + // throw new ArgumentNullException(nameof(content)); + // EnsureWriteable(); + + //// unless a template has been set already by the finder, + //// template should be null at that point. + + //// IsInternalRedirect if IsInitial, or already IsInternalRedirect + // var isInternalRedirect = IsInitialPublishedContent || IsInternalRedirectPublishedContent; + + //// redirecting to self + // if (content.Id == PublishedContent.Id) // neither can be null + // { + // // no need to set PublishedContent, we're done + // IsInternalRedirectPublishedContent = isInternalRedirect; + // return; + // } + + //// else + + //// save + // var template = Template; + + //// set published content - this resets the template, and sets IsInternalRedirect to false + // PublishedContent = content; + // IsInternalRedirectPublishedContent = isInternalRedirect; + + //// must restore the template if it's an internal redirect & the config option is set + // if (isInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) + // { + // // restore + // TemplateModel = template; + // } + } + + /// + /// Gets the initial requested content. + /// + /// + /// The initial requested content is the content that was found by the finders, + /// before anything such as 404, redirect... took place. + /// + public IPublishedContent? InitialPublishedContent { get; private set; } + + /// + /// Gets value indicating whether the current published content is the initial one. + /// + public bool IsInitialPublishedContent => + InitialPublishedContent != null && InitialPublishedContent == _publishedContent; + + /// + /// Indicates that the current PublishedContent is the initial one. + /// + public void SetIsInitialPublishedContent() + { + EnsureWriteable(); + + // note: it can very well be null if the initial content was not found + InitialPublishedContent = _publishedContent; + IsInternalRedirectPublishedContent = false; + } + + /// + /// Gets or sets a value indicating whether the current published content has been obtained + /// from the initial published content following internal redirections exclusively. + /// + /// + /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to + /// apply the internal redirect or not, when content is not the initial content. + /// + public bool IsInternalRedirectPublishedContent { get; private set; } + + #endregion + + // note: do we want to have an ordered list of alternate cultures, + // to allow for fallbacks when doing dictionary lookup and such? + #region Status + + /// + /// Gets or sets a value indicating whether the requested content could not be found. + /// + /// + /// This is set in the PublishedContentRequestBuilder and can also be used in + /// custom content finders or Prepared event handlers, where we want to allow developers + /// to indicate a request is 404 but not to cancel it. + /// + public bool Is404 + { + get => _is404; + set + { + EnsureWriteable(); + _is404 = value; + } + } + + /// + /// Gets a value indicating whether the content request triggers a redirect (permanent or not). + /// + public bool IsRedirect => string.IsNullOrWhiteSpace(RedirectUrl) == false; + + /// + /// Gets or sets a value indicating whether the redirect is permanent. + /// + public bool IsRedirectPermanent { get; private set; } + + /// + /// Gets or sets the URL to redirect to, when the content request triggers a redirect. + /// + public string? RedirectUrl { get; private set; } + + /// + /// Indicates that the content request should trigger a redirect (302). + /// + /// The URL to redirect to. + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + public void SetRedirect(string url) + { + EnsureWriteable(); + RedirectUrl = url; + IsRedirectPermanent = false; + } + + /// + /// Indicates that the content request should trigger a permanent redirect (301). + /// + /// The URL to redirect to. + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + public void SetRedirectPermanent(string url) + { + EnsureWriteable(); + RedirectUrl = url; + IsRedirectPermanent = true; + } + + /// + /// Indicates that the content request should trigger a redirect, with a specified status code. + /// + /// The URL to redirect to. + /// The status code (300-308). + /// + /// Does not actually perform a redirect, only registers that the response should + /// redirect. Redirect will or will not take place in due time. + /// + public void SetRedirect(string url, int status) + { + EnsureWriteable(); + + if (status < 300 || status > 308) + { + throw new ArgumentOutOfRangeException(nameof(status), "Valid redirection status codes 300-308."); + } + + RedirectUrl = url; + IsRedirectPermanent = status == 301 || status == 308; + + // default redirect statuses + if (status != 301 && status != 302) + { + ResponseStatusCode = status; + } + } + + /// + /// Gets or sets the content request http response status code. + /// + /// + /// Does not actually set the http response status code, only registers that the response + /// should use the specified code. The code will or will not be used, in due time. + /// + public int ResponseStatusCode { get; private set; } + + /// + /// Gets or sets the content request http response status description. + /// + /// + /// Does not actually set the http response status description, only registers that the response + /// should use the specified description. The description will or will not be used, in due time. + /// + public string? ResponseStatusDescription { get; private set; } + + /// + /// Sets the http response status code, along with an optional associated description. + /// + /// The http status code. + /// The description. + /// + /// Does not actually set the http response status code and description, only registers that + /// the response should use the specified code and description. The code and description will or will + /// not be used, in due time. + /// + public void SetResponseStatus(int code, string? description = null) + { + EnsureWriteable(); + + // .Status is deprecated + // .SubStatusCode is IIS 7+ internal, ignore + ResponseStatusCode = code; + ResponseStatusDescription = description; + } + + #endregion + + #region Response Cache + + // /// + // /// Gets or sets the System.Web.HttpCacheability + // /// + // Note: we used to set a default value here but that would then be the default + // for ALL requests, we shouldn't overwrite it though if people are using [OutputCache] for example + // see: https://our.umbraco.com/forum/using-umbraco-and-getting-started/79715-output-cache-in-umbraco-752 + // public HttpCacheability Cacheability { get; set; } + + /// + /// Gets or sets a list of Extensions to append to the Response.Cache object. + /// + public List CacheExtensions { get; set; } = new(); + + /// + /// Gets or sets a dictionary of Headers to append to the Response object. + /// + public Dictionary Headers { get; set; } = new(); + + #endregion } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 119f9980b4..5f195f78b5 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -1,8 +1,4 @@ -using System; using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -15,437 +11,460 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Provides the default implementation. +/// +public class PublishedRouter : IPublishedRouter { + private readonly ContentFinderCollection _contentFinders; + private readonly IContentLastChanceFinder _contentLastChanceFinder; + private readonly IContentTypeService _contentTypeService; + private readonly IEventAggregator _eventAggregator; + private readonly IFileService _fileService; + private readonly ILogger _logger; + private readonly IProfilingLogger _profilingLogger; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IPublishedValueFallback _publishedValueFallback; + private readonly IRequestAccessor _requestAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IVariationContextAccessor _variationContextAccessor; + private WebRoutingSettings _webRoutingSettings; /// - /// Provides the default implementation. + /// Initializes a new instance of the class. /// - public class PublishedRouter : IPublishedRouter + public PublishedRouter( + IOptionsMonitor webRoutingSettings, + ContentFinderCollection contentFinders, + IContentLastChanceFinder contentLastChanceFinder, + IVariationContextAccessor variationContextAccessor, + IProfilingLogger proflog, + ILogger logger, + IPublishedUrlProvider publishedUrlProvider, + IRequestAccessor requestAccessor, + IPublishedValueFallback publishedValueFallback, + IFileService fileService, + IContentTypeService contentTypeService, + IUmbracoContextAccessor umbracoContextAccessor, + IEventAggregator eventAggregator) { - private WebRoutingSettings _webRoutingSettings; - private readonly ContentFinderCollection _contentFinders; - private readonly IContentLastChanceFinder _contentLastChanceFinder; - private readonly IProfilingLogger _profilingLogger; - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly ILogger _logger; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IRequestAccessor _requestAccessor; - private readonly IPublishedValueFallback _publishedValueFallback; - private readonly IFileService _fileService; - private readonly IContentTypeService _contentTypeService; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IEventAggregator _eventAggregator; + _webRoutingSettings = webRoutingSettings.CurrentValue ?? + throw new ArgumentNullException(nameof(webRoutingSettings)); + _contentFinders = contentFinders ?? throw new ArgumentNullException(nameof(contentFinders)); + _contentLastChanceFinder = + contentLastChanceFinder ?? throw new ArgumentNullException(nameof(contentLastChanceFinder)); + _profilingLogger = proflog ?? throw new ArgumentNullException(nameof(proflog)); + _variationContextAccessor = variationContextAccessor ?? + throw new ArgumentNullException(nameof(variationContextAccessor)); + _logger = logger; + _publishedUrlProvider = publishedUrlProvider; + _requestAccessor = requestAccessor; + _publishedValueFallback = publishedValueFallback; + _fileService = fileService; + _contentTypeService = contentTypeService; + _umbracoContextAccessor = umbracoContextAccessor; + _eventAggregator = eventAggregator; + webRoutingSettings.OnChange(x => _webRoutingSettings = x); + } - /// - /// Initializes a new instance of the class. - /// - public PublishedRouter( - IOptionsMonitor webRoutingSettings, - ContentFinderCollection contentFinders, - IContentLastChanceFinder contentLastChanceFinder, - IVariationContextAccessor variationContextAccessor, - IProfilingLogger proflog, - ILogger logger, - IPublishedUrlProvider publishedUrlProvider, - IRequestAccessor requestAccessor, - IPublishedValueFallback publishedValueFallback, - IFileService fileService, - IContentTypeService contentTypeService, - IUmbracoContextAccessor umbracoContextAccessor, - IEventAggregator eventAggregator) + /// + public async Task CreateRequestAsync(Uri uri) + { + // trigger the Creating event - at that point the URL can be changed + // this is based on this old task here: https://issues.umbraco.org/issue/U4-7914 which was fulfiled by + // this PR https://github.com/umbraco/Umbraco-CMS/pull/1137 + // It's to do with proxies, quote: + + /* + "Thinking about another solution. + We already have an event, PublishedContentRequest.Prepared, which triggers once the request has been prepared and domain, content, template have been figured out -- but before it renders -- so ppl can change things before rendering. + Wondering whether we could have a event, PublishedContentRequest.Preparing, which would trigger before the request is prepared, and would let ppl change the value of the request's URI (which by default derives from the HttpContext request). + That way, if an in-between equipement changes the URI, you could replace it with the original, public-facing URI before we process the request, meaning you could register your HTTPS domain and it would work. And you would have to supply code for each equipment. Less magic in Core." + */ + + // but now we'll just have one event for creating so if people wish to change the URL here they can but nothing else + var creatingRequest = new CreatingRequestNotification(uri); + await _eventAggregator.PublishAsync(creatingRequest); + + var publishedRequestBuilder = new PublishedRequestBuilder(creatingRequest.Url, _fileService); + return publishedRequestBuilder; + } + + /// + public async Task RouteRequestAsync( + IPublishedRequestBuilder builder, + RouteRequestOptions options) + { + // outbound routing performs different/simpler logic + if (options.RouteDirection == RouteDirection.Outbound) { - _webRoutingSettings = webRoutingSettings.CurrentValue ?? throw new ArgumentNullException(nameof(webRoutingSettings)); - _contentFinders = contentFinders ?? throw new ArgumentNullException(nameof(contentFinders)); - _contentLastChanceFinder = contentLastChanceFinder ?? throw new ArgumentNullException(nameof(contentLastChanceFinder)); - _profilingLogger = proflog ?? throw new ArgumentNullException(nameof(proflog)); - _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - _logger = logger; - _publishedUrlProvider = publishedUrlProvider; - _requestAccessor = requestAccessor; - _publishedValueFallback = publishedValueFallback; - _fileService = fileService; - _contentTypeService = contentTypeService; - _umbracoContextAccessor = umbracoContextAccessor; - _eventAggregator = eventAggregator; - webRoutingSettings.OnChange(x => _webRoutingSettings = x); + return await TryRouteRequest(builder); } - /// - public async Task CreateRequestAsync(Uri uri) + // find domain + if (builder.Domain == null) { - // trigger the Creating event - at that point the URL can be changed - // this is based on this old task here: https://issues.umbraco.org/issue/U4-7914 which was fulfiled by - // this PR https://github.com/umbraco/Umbraco-CMS/pull/1137 - // It's to do with proxies, quote: - - /* - "Thinking about another solution. - We already have an event, PublishedContentRequest.Prepared, which triggers once the request has been prepared and domain, content, template have been figured out -- but before it renders -- so ppl can change things before rendering. - Wondering whether we could have a event, PublishedContentRequest.Preparing, which would trigger before the request is prepared, and would let ppl change the value of the request's URI (which by default derives from the HttpContext request). - That way, if an in-between equipement changes the URI, you could replace it with the original, public-facing URI before we process the request, meaning you could register your HTTPS domain and it would work. And you would have to supply code for each equipment. Less magic in Core." - */ - - // but now we'll just have one event for creating so if people wish to change the URL here they can but nothing else - var creatingRequest = new CreatingRequestNotification(uri); - await _eventAggregator.PublishAsync(creatingRequest); - - var publishedRequestBuilder = new PublishedRequestBuilder(creatingRequest.Url, _fileService); - return publishedRequestBuilder; + FindDomain(builder); } - private async Task TryRouteRequest(IPublishedRequestBuilder request) + await RouteRequestInternalAsync(builder); + + // complete the PCR and assign the remaining values + return BuildRequest(builder); + } + + /// + public async Task UpdateRequestAsync( + IPublishedRequest request, + IPublishedContent? publishedContent) + { + // store the original (if any) + IPublishedContent? content = request.PublishedContent; + + IPublishedRequestBuilder builder = new PublishedRequestBuilder(request.Uri, _fileService); + + // set to the new content (or null if specified) + builder.SetPublishedContent(publishedContent); + + // re-route + await RouteRequestInternalAsync(builder); + + // return if we are redirect + if (builder.IsRedirect()) { - FindDomain(request); - - if (request.IsRedirect()) - { - return request.Build(); - } - - if (request.HasPublishedContent()) - { - return request.Build(); - } - - await FindPublishedContent(request); - - return request.Build(); - } - - private void SetVariationContext(string? culture) - { - VariationContext? variationContext = _variationContextAccessor.VariationContext; - if (variationContext != null && variationContext.Culture == culture) - { - return; - } - - _variationContextAccessor.VariationContext = new VariationContext(culture); - } - - /// - public async Task RouteRequestAsync(IPublishedRequestBuilder builder, RouteRequestOptions options) - { - // outbound routing performs different/simpler logic - if (options.RouteDirection == RouteDirection.Outbound) - { - return await TryRouteRequest(builder); - } - - // find domain - if (builder.Domain == null) - { - FindDomain(builder); - } - - await RouteRequestInternalAsync(builder); - - // complete the PCR and assign the remaining values return BuildRequest(builder); } - private async Task RouteRequestInternalAsync(IPublishedRequestBuilder builder) + // this will occur if publishedContent is null and the last chance finders also don't assign content + if (!builder.HasPublishedContent()) { - // if request builder was already flagged to redirect then return - // whoever called us is in charge of actually redirecting - if (builder.IsRedirect()) - { - return; - } - - // set the culture - SetVariationContext(builder.Culture); - - var foundContentByFinders = false; - - // Find the published content if it's not assigned. - // This could be manually assigned with a custom route handler, etc... - // which in turn could call this method - // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. - if (!builder.HasPublishedContent()) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", builder.Uri.AbsolutePath); - } - - // run the document finders - foundContentByFinders = await FindPublishedContent(builder); - } - - // if we are not a redirect - if (!builder.IsRedirect()) - { - // handle not-found, redirects, access... - await HandlePublishedContent(builder); - - // find a template - FindTemplate(builder, foundContentByFinders); - - // handle umbracoRedirect - FollowExternalRedirect(builder); - - // handle wildcard domains - HandleWildcardDomains(builder); - - // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain - SetVariationContext(builder.Culture); - } - - // trigger the routing request (used to be called Prepared) event - at that point it is still possible to change about anything - // even though the request might be flagged for redirection - we'll redirect _after_ the event - var routingRequest = new RoutingRequestNotification(builder); - await _eventAggregator.PublishAsync(routingRequest); - - // we don't take care of anything so if the content has changed, it's up to the user - // to find out the appropriate template + // means the engine could not find a proper document to handle 404 + // restore the saved content so we know it exists + builder.SetPublishedContent(content); } - /// - /// This method finalizes/builds the PCR with the values assigned. - /// - /// - /// Returns false if the request was not successfully configured - /// - /// - /// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning their own values - /// but need to finalize it themselves. - /// - internal IPublishedRequest BuildRequest(IPublishedRequestBuilder builder) + if (!builder.HasDomain()) { - IPublishedRequest result = builder.Build(); + FindDomain(builder); + } - if (!builder.HasPublishedContent()) - { - return result; - } + return BuildRequest(builder); + } - // set the culture -- again, 'cos it might have changed in the event handler - SetVariationContext(result.Culture); + /// + /// This method finalizes/builds the PCR with the values assigned. + /// + /// + /// Returns false if the request was not successfully configured + /// + /// + /// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning + /// their own values + /// but need to finalize it themselves. + /// + internal IPublishedRequest BuildRequest(IPublishedRequestBuilder builder) + { + IPublishedRequest result = builder.Build(); + if (!builder.HasPublishedContent()) + { return result; } - /// - public async Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent) + // set the culture -- again, 'cos it might have changed in the event handler + SetVariationContext(result.Culture); + + return result; + } + + private async Task TryRouteRequest(IPublishedRequestBuilder request) + { + FindDomain(request); + + if (request.IsRedirect()) { - // store the original (if any) - IPublishedContent? content = request.PublishedContent; - - IPublishedRequestBuilder builder = new PublishedRequestBuilder(request.Uri, _fileService); - - // set to the new content (or null if specified) - builder.SetPublishedContent(publishedContent); - - // re-route - await RouteRequestInternalAsync(builder); - - // return if we are redirect - if (builder.IsRedirect()) - { - return BuildRequest(builder); - } - - // this will occur if publishedContent is null and the last chance finders also don't assign content - if (!builder.HasPublishedContent()) - { - // means the engine could not find a proper document to handle 404 - // restore the saved content so we know it exists - builder.SetPublishedContent(content); - } - - if (!builder.HasDomain()) - { - FindDomain(builder); - } - - return BuildRequest(builder); + return request.Build(); } - /// - /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. - /// - /// A value indicating whether a domain was found. - internal bool FindDomain(IPublishedRequestBuilder request) + if (request.HasPublishedContent()) { - const string tracePrefix = "FindDomain: "; - - // note - we are not handling schemes nor ports here. - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); - } - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - IDomainCache? domainsCache = umbracoContext.PublishedSnapshot.Domains; - var domains = domainsCache?.GetAll(includeWildcards: false).ToList(); - - // determines whether a domain corresponds to a published document, since some - // domains may exist but on a document that has been unpublished - as a whole - or - // that is not published for the domain's culture - in which case the domain does - // not apply - bool IsPublishedContentDomain(Domain domain) - { - // just get it from content cache - optimize there, not here - IPublishedContent? domainDocument = umbracoContext.PublishedSnapshot.Content?.GetById(domain.ContentId); - - // not published - at all - if (domainDocument == null) - { - return false; - } - - // invariant - always published - if (!domainDocument.ContentType.VariesByCulture()) - { - return true; - } - - // variant, ensure that the culture corresponding to the domain's language is published - return domain.Culture is not null && domainDocument.Cultures.ContainsKey(domain.Culture); - } - - domains = domains?.Where(IsPublishedContentDomain).ToList(); - - var defaultCulture = domainsCache?.DefaultCulture; - - // try to find a domain matching the current request - DomainAndUri? domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); - - // handle domain - always has a contentId and a culture - if (domainAndUri != null) - { - // matching an existing domain - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Matches domain={Domain}, rootId={RootContentId}, culture={Culture}", tracePrefix, domainAndUri.Name, domainAndUri.ContentId, domainAndUri.Culture); - } - request.SetDomain(domainAndUri); - - // canonical? not implemented at the moment - // if (...) - // { - // _pcr.RedirectUrl = "..."; - // return true; - // } - } - else - { - // not matching any existing domain - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Matches no domain", tracePrefix); - } - - request.SetCulture(defaultCulture ?? CultureInfo.CurrentUICulture.Name); - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture); - } - - return request.Domain != null; + return request.Build(); } - /// - /// Looks for wildcard domains in the path and updates Culture accordingly. - /// - internal void HandleWildcardDomains(IPublishedRequestBuilder request) + await FindPublishedContent(request); + + return request.Build(); + } + + private void SetVariationContext(string? culture) + { + VariationContext? variationContext = _variationContextAccessor.VariationContext; + if (variationContext != null && variationContext.Culture == culture) { - const string tracePrefix = "HandleWildcardDomains: "; - - if (request.PublishedContent == null) - { - return; - } - - var nodePath = request.PublishedContent.Path; - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); - } - var rootNodeId = request.Domain != null ? request.Domain.ContentId : (int?)null; - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - Domain? domain = DomainUtilities.FindWildcardDomainInPath(umbracoContext.PublishedSnapshot.Domains?.GetAll(true), nodePath, rootNodeId); - - // always has a contentId and a culture - if (domain != null) - { - request.SetCulture(domain.Culture); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", tracePrefix, domain.ContentId, request.Culture); - } - } - else - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("{TracePrefix}No match.", tracePrefix); - } - } + return; } - internal bool FindTemplateRenderingEngineInDirectory(DirectoryInfo directory, string alias, string[] extensions) + _variationContextAccessor.VariationContext = new VariationContext(culture); + } + + private async Task RouteRequestInternalAsync(IPublishedRequestBuilder builder) + { + // if request builder was already flagged to redirect then return + // whoever called us is in charge of actually redirecting + if (builder.IsRedirect()) { - if (directory == null || directory.Exists == false) + return; + } + + // set the culture + SetVariationContext(builder.Culture); + + var foundContentByFinders = false; + + // Find the published content if it's not assigned. + // This could be manually assigned with a custom route handler, etc... + // which in turn could call this method + // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. + if (!builder.HasPublishedContent()) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", builder.Uri.AbsolutePath); + } + + // run the document finders + foundContentByFinders = await FindPublishedContent(builder); + } + + // if we are not a redirect + if (!builder.IsRedirect()) + { + // handle not-found, redirects, access... + await HandlePublishedContent(builder); + + // find a template + FindTemplate(builder, foundContentByFinders); + + // handle umbracoRedirect + FollowExternalRedirect(builder); + + // handle wildcard domains + HandleWildcardDomains(builder); + + // set the culture -- again, 'cos it might have changed due to a finder or wildcard domain + SetVariationContext(builder.Culture); + } + + // trigger the routing request (used to be called Prepared) event - at that point it is still possible to change about anything + // even though the request might be flagged for redirection - we'll redirect _after_ the event + var routingRequest = new RoutingRequestNotification(builder); + await _eventAggregator.PublishAsync(routingRequest); + + // we don't take care of anything so if the content has changed, it's up to the user + // to find out the appropriate template + } + + /// + /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. + /// + /// A value indicating whether a domain was found. + internal bool FindDomain(IPublishedRequestBuilder request) + { + const string tracePrefix = "FindDomain: "; + + // note - we are not handling schemes nor ports here. + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); + } + + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IDomainCache? domainsCache = umbracoContext.PublishedSnapshot.Domains; + var domains = domainsCache?.GetAll(false).ToList(); + + // determines whether a domain corresponds to a published document, since some + // domains may exist but on a document that has been unpublished - as a whole - or + // that is not published for the domain's culture - in which case the domain does + // not apply + bool IsPublishedContentDomain(Domain domain) + { + // just get it from content cache - optimize there, not here + IPublishedContent? domainDocument = umbracoContext.PublishedSnapshot.Content?.GetById(domain.ContentId); + + // not published - at all + if (domainDocument == null) { return false; } - var pos = alias.IndexOf('/'); - if (pos > 0) + // invariant - always published + if (!domainDocument.ContentType.VariesByCulture()) { - // recurse - DirectoryInfo? subdir = directory.GetDirectories(alias.Substring(0, pos)).FirstOrDefault(); - alias = alias.Substring(pos + 1); - return subdir != null && FindTemplateRenderingEngineInDirectory(subdir, alias, extensions); + return true; } - // look here - return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); + // variant, ensure that the culture corresponding to the domain's language is published + return domain.Culture is not null && domainDocument.Cultures.ContainsKey(domain.Culture); } - /// - /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. - /// - /// There is no finder collection. - internal async Task FindPublishedContent(IPublishedRequestBuilder request) - { - const string tracePrefix = "FindPublishedContent: "; + domains = domains?.Where(IsPublishedContentDomain).ToList(); - // look for the document - // the first successful finder, if any, will set this.PublishedContent, and may also set this.Template - // some finders may implement caching - DisposableTimer? profilingScope = null; - try + var defaultCulture = domainsCache?.DefaultCulture; + + // try to find a domain matching the current request + DomainAndUri? domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); + + // handle domain - always has a contentId and a culture + if (domainAndUri != null) + { + // matching an existing domain + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - profilingScope = _profilingLogger.DebugDuration( + _logger.LogDebug( + "{TracePrefix}Matches domain={Domain}, rootId={RootContentId}, culture={Culture}", + tracePrefix, + domainAndUri.Name, + domainAndUri.ContentId, + domainAndUri.Culture); + } + + request.SetDomain(domainAndUri); + + // canonical? not implemented at the moment + // if (...) + // { + // _pcr.RedirectUrl = "..."; + // return true; + // } + } + else + { + // not matching any existing domain + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}Matches no domain", tracePrefix); + } + + request.SetCulture(defaultCulture ?? CultureInfo.CurrentUICulture.Name); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture); + } + + return request.Domain != null; + } + + /// + /// Looks for wildcard domains in the path and updates Culture accordingly. + /// + internal void HandleWildcardDomains(IPublishedRequestBuilder request) + { + const string tracePrefix = "HandleWildcardDomains: "; + + if (request.PublishedContent == null) + { + return; + } + + var nodePath = request.PublishedContent.Path; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); + } + + var rootNodeId = request.Domain != null ? request.Domain.ContentId : (int?)null; + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + Domain? domain = + DomainUtilities.FindWildcardDomainInPath(umbracoContext.PublishedSnapshot.Domains?.GetAll(true), nodePath, rootNodeId); + + // always has a contentId and a culture + if (domain != null) + { + request.SetCulture(domain.Culture); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + "{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", + tracePrefix, + domain.ContentId, + request.Culture); + } + } + else + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("{TracePrefix}No match.", tracePrefix); + } + } + } + + internal bool FindTemplateRenderingEngineInDirectory(DirectoryInfo? directory, string alias, string[] extensions) + { + if (directory == null || directory.Exists == false) + { + return false; + } + + var pos = alias.IndexOf('/'); + if (pos > 0) + { + // recurse + DirectoryInfo? subdir = directory.GetDirectories(alias.Substring(0, pos)).FirstOrDefault(); + alias = alias.Substring(pos + 1); + return subdir != null && FindTemplateRenderingEngineInDirectory(subdir, alias, extensions); + } + + // look here + return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); + } + + /// + /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. + /// + /// There is no finder collection. + internal async Task FindPublishedContent(IPublishedRequestBuilder request) + { + const string tracePrefix = "FindPublishedContent: "; + + // look for the document + // the first successful finder, if any, will set this.PublishedContent, and may also set this.Template + // some finders may implement caching + DisposableTimer? profilingScope = null; + try + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + profilingScope = _profilingLogger.DebugDuration( $"{tracePrefix}Begin finders", $"{tracePrefix}End finders"); + } + + // iterate but return on first one that finds it + var found = false; + foreach (IContentFinder contentFinder in _contentFinders) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Finder {ContentFinderType}", contentFinder.GetType().FullName); } - // iterate but return on first one that finds it - var found = false; - foreach (var contentFinder in _contentFinders) + found = await contentFinder.TryFindContent(request); + if (found) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("Finder {ContentFinderType}", contentFinder.GetType().FullName); - } - found = await contentFinder.TryFindContent(request); - if (found) - { - break; - } + break; } + } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug( + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( "Found? {Found}, Content: {PublishedContentId}, Template: {TemplateAlias}, Domain: {Domain}, Culture: {Culture}, StatusCode: {StatusCode}", found, request.HasPublishedContent() ? request.PublishedContent?.Id : "NULL", @@ -453,393 +472,431 @@ namespace Umbraco.Cms.Core.Routing request.HasDomain() ? request.Domain?.ToString() : "NULL", request.Culture ?? "NULL", request.ResponseStatusCode); - } + } - return found; - } - finally - { - profilingScope?.Dispose(); - } + return found; } - - /// - /// Handles the published content (if any). - /// - /// The request builder. - /// - /// Handles "not found", internal redirects ... - /// things that must be handled in one place because they can create loops - /// - private async Task HandlePublishedContent(IPublishedRequestBuilder request) + finally { - // because these might loop, we have to have some sort of infinite loop detection - int i = 0, j = 0; - const int maxLoop = 8; - do + profilingScope?.Dispose(); + } + } + + /// + /// Handles the published content (if any). + /// + /// The request builder. + /// + /// Handles "not found", internal redirects ... + /// things that must be handled in one place because they can create loops + /// + private async Task HandlePublishedContent(IPublishedRequestBuilder request) + { + // because these might loop, we have to have some sort of infinite loop detection + int i = 0, j = 0; + const int maxLoop = 8; + do + { + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + _logger.LogDebug("HandlePublishedContent: Loop {LoopCounter}", i); + } + + // handle not found + if (request.PublishedContent == null) + { + request.SetIs404(); + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("HandlePublishedContent: Loop {LoopCounter}", i); + _logger.LogDebug("HandlePublishedContent: No document, try last chance lookup"); } - // handle not found - if (request.PublishedContent == null) + // if it fails then give up, there isn't much more that we can do + if (await _contentLastChanceFinder.TryFindContent(request) == false) { - request.SetIs404(); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("HandlePublishedContent: No document, try last chance lookup"); + _logger.LogDebug("HandlePublishedContent: Failed to find a document, give up"); } - // if it fails then give up, there isn't much more that we can do - if (await _contentLastChanceFinder.TryFindContent(request) == false) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("HandlePublishedContent: Failed to find a document, give up"); - } - break; - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("HandlePublishedContent: Found a document"); - } - } - - // follow internal redirects as long as it's not running out of control ie infinite loop of some sort - j = 0; - while (FollowInternalRedirects(request) && j++ < maxLoop) - { } - - // we're running out of control - if (j == maxLoop) - { break; } - // loop while we don't have page, ie the redirect or access - // got us to nowhere and now we need to run the notFoundLookup again - // as long as it's not running out of control ie infinite loop of some sort - } while (request.PublishedContent == null && i++ < maxLoop); - - if (i == maxLoop || j == maxLoop) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("HandlePublishedContent: Looks like we are running into an infinite loop, abort"); + _logger.LogDebug("HandlePublishedContent: Found a document"); } - request.SetPublishedContent(null); } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + + // follow internal redirects as long as it's not running out of control ie infinite loop of some sort + j = 0; + while (FollowInternalRedirects(request) && j++ < maxLoop) { - _logger.LogDebug("HandlePublishedContent: End"); + } + + // we're running out of control + if (j == maxLoop) + { + break; + } + + // loop while we don't have page, ie the redirect or access + // got us to nowhere and now we need to run the notFoundLookup again + // as long as it's not running out of control ie infinite loop of some sort + } + while (request.PublishedContent == null && i++ < maxLoop); + + if (i == maxLoop || j == maxLoop) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("HandlePublishedContent: Looks like we are running into an infinite loop, abort"); + } + + request.SetPublishedContent(null); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("HandlePublishedContent: End"); + } + } + + /// + /// Follows internal redirections through the umbracoInternalRedirectId document property. + /// + /// The request builder. + /// A value indicating whether redirection took place and led to a new published document. + /// + /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. + /// As per legacy, if the redirect does not work, we just ignore it. + /// + private bool FollowInternalRedirects(IPublishedRequestBuilder request) + { + if (request.PublishedContent == null) + { + throw new InvalidOperationException("There is no PublishedContent."); + } + + // don't try to find a redirect if the property doesn't exist + if (request.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) + { + return false; + } + + var redirect = false; + var valid = false; + IPublishedContent? internalRedirectNode = null; + var internalRedirectId = request.PublishedContent.Value( + _publishedValueFallback, + Constants.Conventions.Content.InternalRedirectId, + defaultValue: -1); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + if (internalRedirectId > 0) + { + // try and get the redirect node from a legacy integer ID + valid = true; + internalRedirectNode = umbracoContext.Content?.GetById(internalRedirectId); + } + else + { + GuidUdi? udiInternalRedirectId = request.PublishedContent.Value( + _publishedValueFallback, + Constants.Conventions.Content.InternalRedirectId); + if (udiInternalRedirectId is not null) + { + // try and get the redirect node from a UDI Guid + valid = true; + internalRedirectNode = umbracoContext.Content?.GetById(udiInternalRedirectId.Guid); } } - /// - /// Follows internal redirections through the umbracoInternalRedirectId document property. - /// - /// The request builder. - /// A value indicating whether redirection took place and led to a new published document. - /// - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - /// As per legacy, if the redirect does not work, we just ignore it. - /// - private bool FollowInternalRedirects(IPublishedRequestBuilder request) + if (valid == false) { - if (request.PublishedContent == null) + // bad redirect - log and display the current page (legacy behavior) + if (_logger.IsEnabled(LogLevel.Debug)) { - throw new InvalidOperationException("There is no PublishedContent."); - } - - // don't try to find a redirect if the property doesn't exist - if (request.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) - { - return false; - } - - var redirect = false; - var valid = false; - IPublishedContent? internalRedirectNode = null; - var internalRedirectId = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.InternalRedirectId, defaultValue: -1); - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - - if (internalRedirectId > 0) - { - // try and get the redirect node from a legacy integer ID - valid = true; - internalRedirectNode = umbracoContext.Content?.GetById(internalRedirectId); - } - else - { - GuidUdi? udiInternalRedirectId = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.InternalRedirectId); - if (udiInternalRedirectId is not null) - { - // try and get the redirect node from a UDI Guid - valid = true; - internalRedirectNode = umbracoContext.Content?.GetById(udiInternalRedirectId.Guid); - } - } - - if (valid == false) - { - // bad redirect - log and display the current page (legacy behavior) - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug( + _logger.LogDebug( "FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: value is not an int nor a GuidUdi.", request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId)?.GetSourceValue()); - } } - - if (internalRedirectNode == null) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug( - "FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", - request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId)?.GetSourceValue()); - } - } - else if (internalRedirectId == request.PublishedContent.Id) - { - // redirect to self - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FollowInternalRedirects: Redirecting to self, ignore"); - } - } - else - { - // save since it will be cleared - ITemplate? template = request.Template; - - request.SetInternalRedirect(internalRedirectNode); // don't use .PublishedContent here - - // must restore the template if it's an internal redirect & the config option is set - if (request.IsInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) - { - // restore - request.SetTemplate(template); - } - - redirect = true; - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); - } - } - - return redirect; } - /// - /// Finds a template for the current node, if any. - /// - /// The request builder. - /// If the content was found by the finders, before anything such as 404, redirect... took place. - private void FindTemplate(IPublishedRequestBuilder request, bool contentFoundByFinders) + if (internalRedirectNode == null) { - // TODO: We've removed the event, might need to re-add? - // NOTE: at the moment there is only 1 way to find a template, and then ppl must - // use the Prepared event to change the template if they wish. Should we also - // implement an ITemplateFinder logic? - if (request.PublishedContent == null) + if (_logger.IsEnabled(LogLevel.Debug)) { - request.SetTemplate(null); + _logger.LogDebug( + "FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", + request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId)?.GetSourceValue()); + } + } + else if (internalRedirectId == request.PublishedContent.Id) + { + // redirect to self + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FollowInternalRedirects: Redirecting to self, ignore"); + } + } + else + { + // save since it will be cleared + ITemplate? template = request.Template; + + request.SetInternalRedirect(internalRedirectNode); // don't use .PublishedContent here + + // must restore the template if it's an internal redirect & the config option is set + if (request.IsInternalRedirect && _webRoutingSettings.InternalRedirectPreservesTemplate) + { + // restore + request.SetTemplate(template); + } + + redirect = true; + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); + } + } + + return redirect; + } + + /// + /// Finds a template for the current node, if any. + /// + /// The request builder. + /// + /// If the content was found by the finders, before anything such as 404, redirect... + /// took place. + /// + private void FindTemplate(IPublishedRequestBuilder request, bool contentFoundByFinders) + { + // TODO: We've removed the event, might need to re-add? + // NOTE: at the moment there is only 1 way to find a template, and then ppl must + // use the Prepared event to change the template if they wish. Should we also + // implement an ITemplateFinder logic? + if (request.PublishedContent == null) + { + request.SetTemplate(null); + return; + } + + // read the alternate template alias, from querystring, form, cookie or server vars, + // only if the published content is the initial once, else the alternate template + // does not apply + // + optionally, apply the alternate template on internal redirects + var useAltTemplate = contentFoundByFinders + || (_webRoutingSettings.InternalRedirectPreservesTemplate && request.IsInternalRedirect); + + var altTemplate = useAltTemplate + ? _requestAccessor.GetRequestValue(Constants.Conventions.Url.AltTemplate) + : null; + + if (string.IsNullOrWhiteSpace(altTemplate)) + { + // we don't have an alternate template specified. use the current one if there's one already, + // which can happen if a content lookup also set the template (LookupByNiceUrlAndTemplate...), + // else lookup the template id on the document then lookup the template with that id. + if (request.HasTemplate()) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FindTemplate: Has a template already, and no alternate template."); + } + return; } - // read the alternate template alias, from querystring, form, cookie or server vars, - // only if the published content is the initial once, else the alternate template - // does not apply - // + optionally, apply the alternate template on internal redirects - var useAltTemplate = contentFoundByFinders - || (_webRoutingSettings.InternalRedirectPreservesTemplate && request.IsInternalRedirect); - - var altTemplate = useAltTemplate - ? _requestAccessor.GetRequestValue(Constants.Conventions.Url.AltTemplate) - : null; - - if (string.IsNullOrWhiteSpace(altTemplate)) + // TODO: We need to limit altTemplate to only allow templates that are assigned to the current document type! + // if the template isn't assigned to the document type we should log a warning and return 404 + var templateId = request.PublishedContent.TemplateId; + ITemplate? template = GetTemplate(templateId); + request.SetTemplate(template); + if (template != null) { - // we don't have an alternate template specified. use the current one if there's one already, - // which can happen if a content lookup also set the template (LookupByNiceUrlAndTemplate...), - // else lookup the template id on the document then lookup the template with that id. - if (request.HasTemplate()) + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Has a template already, and no alternate template."); - } - return; - } - - // TODO: We need to limit altTemplate to only allow templates that are assigned to the current document type! - // if the template isn't assigned to the document type we should log a warning and return 404 - var templateId = request.PublishedContent.TemplateId; - ITemplate? template = GetTemplate(templateId); - request.SetTemplate(template); - if (template != null) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); - } - } - else - { - _logger.LogWarning("FindTemplate: Could not find template with id {TemplateId}", templateId); + _logger.LogDebug( + "FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", + template.Id, + template.Alias); } } else { - // we have an alternate template specified. lookup the template with that alias - // this means the we override any template that a content lookup might have set - // so /path/to/page/template1?altTemplate=template2 will use template2 + _logger.LogWarning("FindTemplate: Could not find template with id {TemplateId}", templateId); + } + } + else + { + // we have an alternate template specified. lookup the template with that alias + // this means the we override any template that a content lookup might have set + // so /path/to/page/template1?altTemplate=template2 will use template2 - // ignore if the alias does not match - just trace - if (request.HasTemplate()) + // ignore if the alias does not match - just trace + if (request.HasTemplate()) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Has a template already, but also an alternative template."); - } - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Look for alternative template alias={AltTemplate}", altTemplate); + _logger.LogDebug("FindTemplate: Has a template already, but also an alternative template."); } + } - // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings - if (request.PublishedContent.IsAllowedTemplate( + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("FindTemplate: Look for alternative template alias={AltTemplate}", altTemplate); + } + + // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings + if (request.PublishedContent.IsAllowedTemplate( _fileService, _contentTypeService, _webRoutingSettings.DisableAlternativeTemplates, _webRoutingSettings.ValidateAlternativeTemplates, altTemplate)) - { - // allowed, use - ITemplate? template = _fileService.GetTemplate(altTemplate); + { + // allowed, use + ITemplate? template = _fileService.GetTemplate(altTemplate); - if (template != null) + if (template != null) + { + request.SetTemplate(template); + if (_logger.IsEnabled(LogLevel.Debug)) { - request.SetTemplate(template); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: Got alternative template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); - } - } - else - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: The alternative template with alias={AltTemplate} does not exist, ignoring.", altTemplate); - } + _logger.LogDebug( + "FindTemplate: Got alternative template id={TemplateId} alias={TemplateAlias}", + template.Id, + template.Alias); } } else { - _logger.LogWarning("FindTemplate: Alternative template {TemplateAlias} is not allowed on node {NodeId}, ignoring.", altTemplate, request.PublishedContent.Id); - // no allowed, back to default - var templateId = request.PublishedContent.TemplateId; - ITemplate? template = GetTemplate(templateId); - request.SetTemplate(template); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", template?.Id, template?.Alias); + _logger.LogDebug( + "FindTemplate: The alternative template with alias={AltTemplate} does not exist, ignoring.", + altTemplate); } } } - - if (!request.HasTemplate()) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("FindTemplate: No template was found."); - } - - // initial idea was: if we're not already 404 and UmbracoSettings.HandleMissingTemplateAs404 is true - // then reset _pcr.Document to null to force a 404. - // - // but: because we want to let MVC hijack routes even though no template is defined, we decide that - // a missing template is OK but the request will then be forwarded to MVC, which will need to take - // care of everything. - // - // so, don't set _pcr.Document to null here - } - } - - private ITemplate? GetTemplate(int? templateId) - { - if (templateId.HasValue == false || templateId.Value == default) - { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("GetTemplateModel: No template."); - } - return null; - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("GetTemplateModel: Get template id={TemplateId}", templateId); - } - - if (templateId == null) - { - throw new InvalidOperationException("The template is not set, the page cannot render."); - } - - ITemplate? template = _fileService.GetTemplate(templateId.Value); - if (template == null) - { - throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render."); - } - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - _logger.LogDebug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); - } - return template; - } - - /// - /// Follows external redirection through umbracoRedirect document property. - /// - /// As per legacy, if the redirect does not work, we just ignore it. - private void FollowExternalRedirect(IPublishedRequestBuilder request) - { - if (request.PublishedContent == null) - { - return; - } - - // don't try to find a redirect if the property doesn't exist - if (request.PublishedContent.HasProperty(Constants.Conventions.Content.Redirect) == false) - { - return; - } - - var redirectId = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.Redirect, defaultValue: -1); - var redirectUrl = "#"; - if (redirectId > 0) - { - redirectUrl = _publishedUrlProvider.GetUrl(redirectId); - } else { - // might be a UDI instead of an int Id - GuidUdi? redirectUdi = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.Redirect); - if (redirectUdi is not null) + _logger.LogWarning( + "FindTemplate: Alternative template {TemplateAlias} is not allowed on node {NodeId}, ignoring.", + altTemplate, + request.PublishedContent.Id); + + // no allowed, back to default + var templateId = request.PublishedContent.TemplateId; + ITemplate? template = GetTemplate(templateId); + request.SetTemplate(template); + if (_logger.IsEnabled(LogLevel.Debug)) { - redirectUrl = _publishedUrlProvider.GetUrl(redirectUdi.Guid); + _logger.LogDebug( + "FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", + template?.Id, + template?.Alias); } } + } - if (redirectUrl != "#") + if (!request.HasTemplate()) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - request.SetRedirect(redirectUrl); + _logger.LogDebug("FindTemplate: No template was found."); } + + // initial idea was: if we're not already 404 and UmbracoSettings.HandleMissingTemplateAs404 is true + // then reset _pcr.Document to null to force a 404. + // + // but: because we want to let MVC hijack routes even though no template is defined, we decide that + // a missing template is OK but the request will then be forwarded to MVC, which will need to take + // care of everything. + // + // so, don't set _pcr.Document to null here + } + } + + private ITemplate? GetTemplate(int? templateId) + { + if (templateId.HasValue == false || templateId.Value == default) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("GetTemplateModel: No template."); + } + + return null; + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("GetTemplateModel: Get template id={TemplateId}", templateId); + } + + if (templateId == null) + { + throw new InvalidOperationException("The template is not set, the page cannot render."); + } + + ITemplate? template = _fileService.GetTemplate(templateId.Value); + if (template == null) + { + throw new InvalidOperationException("The template with Id " + templateId + + " does not exist, the page cannot render."); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); + } + + return template; + } + + /// + /// Follows external redirection through umbracoRedirect document property. + /// + /// As per legacy, if the redirect does not work, we just ignore it. + private void FollowExternalRedirect(IPublishedRequestBuilder request) + { + if (request.PublishedContent == null) + { + return; + } + + // don't try to find a redirect if the property doesn't exist + if (request.PublishedContent.HasProperty(Constants.Conventions.Content.Redirect) == false) + { + return; + } + + var redirectId = request.PublishedContent.Value(_publishedValueFallback, Constants.Conventions.Content.Redirect, defaultValue: -1); + var redirectUrl = "#"; + if (redirectId > 0) + { + redirectUrl = _publishedUrlProvider.GetUrl(redirectId); + } + else + { + // might be a UDI instead of an int Id + GuidUdi? redirectUdi = + request.PublishedContent.Value( + _publishedValueFallback, + Constants.Conventions.Content.Redirect); + if (redirectUdi is not null) + { + redirectUrl = _publishedUrlProvider.GetUrl(redirectUdi.Guid); + } + } + + if (redirectUrl != "#") + { + request.SetRedirect(redirectUrl); } } } diff --git a/src/Umbraco.Core/Routing/RouteDirection.cs b/src/Umbraco.Core/Routing/RouteDirection.cs index 33dad7b081..7ba637c288 100644 --- a/src/Umbraco.Core/Routing/RouteDirection.cs +++ b/src/Umbraco.Core/Routing/RouteDirection.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// The direction of a route +/// +public enum RouteDirection { /// - /// The direction of a route + /// An inbound route used to map a URL to a content item /// - public enum RouteDirection - { - /// - /// An inbound route used to map a URL to a content item - /// - Inbound = 1, + Inbound = 1, - /// - /// An outbound route used to generate a URL for a content item - /// - Outbound = 2 - } + /// + /// An outbound route used to generate a URL for a content item + /// + Outbound = 2, } diff --git a/src/Umbraco.Core/Routing/RouteRequestOptions.cs b/src/Umbraco.Core/Routing/RouteRequestOptions.cs index 97792ebad3..960bf4bd36 100644 --- a/src/Umbraco.Core/Routing/RouteRequestOptions.cs +++ b/src/Umbraco.Core/Routing/RouteRequestOptions.cs @@ -1,29 +1,26 @@ -using System; +namespace Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Routing +/// +/// Options for routing an Umbraco request +/// +public struct RouteRequestOptions : IEquatable { /// - /// Options for routing an Umbraco request + /// Initializes a new instance of the struct. /// - public struct RouteRequestOptions : IEquatable - { - /// - /// Initializes a new instance of the struct. - /// - public RouteRequestOptions(RouteDirection direction) => RouteDirection = direction; + public RouteRequestOptions(RouteDirection direction) => RouteDirection = direction; - /// - /// Gets the - /// - public RouteDirection RouteDirection { get; } + /// + /// Gets the + /// + public RouteDirection RouteDirection { get; } - /// - public override bool Equals(object? obj) => obj is RouteRequestOptions options && Equals(options); + /// + public override bool Equals(object? obj) => obj is RouteRequestOptions options && Equals(options); - /// - public bool Equals(RouteRequestOptions other) => RouteDirection == other.RouteDirection; + /// + public bool Equals(RouteRequestOptions other) => RouteDirection == other.RouteDirection; - /// - public override int GetHashCode() => 15391035 + RouteDirection.GetHashCode(); - } + /// + public override int GetHashCode() => 15391035 + RouteDirection.GetHashCode(); } diff --git a/src/Umbraco.Core/Routing/SiteDomainMapper.cs b/src/Umbraco.Core/Routing/SiteDomainMapper.cs index a74d4532e1..b8ae10c3aa 100644 --- a/src/Umbraco.Core/Routing/SiteDomainMapper.cs +++ b/src/Umbraco.Core/Routing/SiteDomainMapper.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; -using System.Threading; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Routing @@ -235,8 +231,7 @@ namespace Umbraco.Cms.Core.Routing #region Map domains /// - public virtual DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, Uri current, - string? culture, string? defaultCulture) + public virtual DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, Uri current, string? culture, string? defaultCulture) { var currentAuthority = current.GetLeftPart(UriPartial.Authority); Dictionary? qualifiedSites = GetQualifiedSites(current); @@ -245,8 +240,7 @@ namespace Umbraco.Cms.Core.Routing } /// - public virtual IEnumerable MapDomains(IReadOnlyCollection domainAndUris, - Uri current, bool excludeDefault, string? culture, string? defaultCulture) + public virtual IEnumerable MapDomains(IReadOnlyCollection domainAndUris, Uri current, bool excludeDefault, string? culture, string? defaultCulture) { // TODO: ignoring cultures entirely? @@ -277,8 +271,7 @@ namespace Umbraco.Cms.Core.Routing { // it is illegal to call MapDomain if domainAndUris is empty // also, domainAndUris should NOT contain current, hence the test on hinted - DomainAndUri? mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture, - defaultCulture); // what GetUrl would get + DomainAndUri? mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture, defaultCulture); // what GetUrl would get ret = ret.Where(d => d != mainDomain); } } @@ -368,16 +361,19 @@ namespace Umbraco.Cms.Core.Routing kvp => kvp.Value.Select(d => new Uri(UriUtilityCore.StartWithScheme(d, current.Scheme)) .GetLeftPart(UriPartial.Authority)) - .ToArray() - ); + .ToArray()); // .ToDictionary will evaluate and create the dictionary immediately // the new value is .ToArray so it will also be evaluated immediately // therefore it is safe to return and exit the configuration lock } - private DomainAndUri? MapDomain(IReadOnlyCollection domainAndUris, - Dictionary? qualifiedSites, string currentAuthority, string? culture, string? defaultCulture) + private DomainAndUri? MapDomain( + IReadOnlyCollection domainAndUris, + Dictionary? qualifiedSites, + string currentAuthority, + string? culture, + string? defaultCulture) { if (domainAndUris == null) { diff --git a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs index 5d298b811a..fe1e83d254 100644 --- a/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs +++ b/src/Umbraco.Core/Routing/UmbracoRequestPaths.cs @@ -1,134 +1,129 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Utility for checking paths +/// +public class UmbracoRequestPaths { + private readonly string _apiMvcPath; + private readonly string _appPath; + private readonly string _backOfficeMvcPath; + private readonly string _backOfficePath; + private readonly List _defaultUmbPaths; + private readonly string _installPath; + private readonly string _mvcArea; + private readonly string _previewMvcPath; + private readonly string _surfaceMvcPath; + /// - /// Utility for checking paths + /// Initializes a new instance of the class. /// - public class UmbracoRequestPaths + public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvironment hostingEnvironment) { - private readonly string _backOfficePath; - private readonly string _mvcArea; - private readonly string _backOfficeMvcPath; - private readonly string _previewMvcPath; - private readonly string _surfaceMvcPath; - private readonly string _apiMvcPath; - private readonly string _installPath; - private readonly string _appPath; - private readonly List _defaultUmbPaths; + var applicationPath = hostingEnvironment.ApplicationVirtualPath; + _appPath = applicationPath.TrimStart(Constants.CharArrays.ForwardSlash); - /// - /// Initializes a new instance of the class. - /// - public UmbracoRequestPaths(IOptions globalSettings, IHostingEnvironment hostingEnvironment) + _backOfficePath = globalSettings.Value.GetBackOfficePath(hostingEnvironment) + .EnsureStartsWith('/').TrimStart(_appPath.EnsureStartsWith('/')).EnsureStartsWith('/'); + + _mvcArea = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); + _defaultUmbPaths = new List { "/" + _mvcArea, "/" + _mvcArea + "/" }; + _backOfficeMvcPath = "/" + _mvcArea + "/BackOffice/"; + _previewMvcPath = "/" + _mvcArea + "/Preview/"; + _surfaceMvcPath = "/" + _mvcArea + "/Surface/"; + _apiMvcPath = "/" + _mvcArea + "/Api/"; + _installPath = hostingEnvironment.ToAbsolute(Constants.SystemDirectories.Install); + } + + /// + /// Checks if the current uri is a back office request + /// + /// + /// + /// There are some special routes we need to check to properly determine this: + /// + /// + /// These are def back office: + /// /Umbraco/BackOffice = back office + /// /Umbraco/Preview = back office + /// + /// + /// If it's not any of the above then we cannot determine if it's back office or front-end + /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the + /// backoffice + /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. + /// + /// + /// These are def front-end: + /// /Umbraco/Surface = front-end + /// /Umbraco/Api = front-end + /// But if we've got this far we'll just have to assume it's front-end anyways. + /// + /// + public bool IsBackOfficeRequest(string absPath) + { + var fullUrlPath = absPath.TrimStart(Constants.CharArrays.ForwardSlash); + var urlPath = fullUrlPath.TrimStart(_appPath).EnsureStartsWith('/'); + + // check if this is in the umbraco back office + var isUmbracoPath = urlPath.InvariantStartsWith(_backOfficePath); + + // if not, then def not back office + if (isUmbracoPath == false) { - var applicationPath = hostingEnvironment.ApplicationVirtualPath; - _appPath = applicationPath.TrimStart(Constants.CharArrays.ForwardSlash); - - _backOfficePath = globalSettings.Value.GetBackOfficePath(hostingEnvironment) - .EnsureStartsWith('/').TrimStart(_appPath.EnsureStartsWith('/')).EnsureStartsWith('/'); - - _mvcArea = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); - _defaultUmbPaths = new List { "/" + _mvcArea, "/" + _mvcArea + "/" }; - _backOfficeMvcPath = "/" + _mvcArea + "/BackOffice/"; - _previewMvcPath = "/" + _mvcArea + "/Preview/"; - _surfaceMvcPath = "/" + _mvcArea + "/Surface/"; - _apiMvcPath = "/" + _mvcArea + "/Api/"; - _installPath = hostingEnvironment.ToAbsolute(Constants.SystemDirectories.Install); + return false; } - /// - /// Checks if the current uri is a back office request - /// - /// - /// - /// There are some special routes we need to check to properly determine this: - /// - /// - /// These are def back office: - /// /Umbraco/BackOffice = back office - /// /Umbraco/Preview = back office - /// - /// - /// If it's not any of the above then we cannot determine if it's back office or front-end - /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the backoffice - /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. - /// - /// - /// These are def front-end: - /// /Umbraco/Surface = front-end - /// /Umbraco/Api = front-end - /// But if we've got this far we'll just have to assume it's front-end anyways. - /// - /// - public bool IsBackOfficeRequest(string absPath) + // if its the normal /umbraco path + if (_defaultUmbPaths.Any(x => urlPath.InvariantEquals(x))) { - var fullUrlPath = absPath.TrimStart(Constants.CharArrays.ForwardSlash); - var urlPath = fullUrlPath.TrimStart(_appPath).EnsureStartsWith('/'); - - // check if this is in the umbraco back office - var isUmbracoPath = urlPath.InvariantStartsWith(_backOfficePath); - - // if not, then def not back office - if (isUmbracoPath == false) - { - return false; - } - - // if its the normal /umbraco path - if (_defaultUmbPaths.Any(x => urlPath.InvariantEquals(x))) - { - return true; - } - - // check for special back office paths - if (urlPath.InvariantStartsWith(_backOfficeMvcPath) - || urlPath.InvariantStartsWith(_previewMvcPath)) - { - return true; - } - - // check for special front-end paths - if (urlPath.InvariantStartsWith(_surfaceMvcPath) - || urlPath.InvariantStartsWith(_apiMvcPath)) - { - return false; - } - - // if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by - // checking how many parts the route has, for example, all PluginController routes will be routed like - // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} - // so if the path contains at a minimum 3 parts: Umbraco + MYPLUGINAREA + MYCONTROLLERNAME then we will have to assume it is a - // plugin controller for the front-end. - if (urlPath.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries).Length >= 3) - { - return false; - } - - // if its anything else we can assume it's back office return true; } - /// - /// Checks if the current uri is an install request - /// - public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_installPath); - - /// - /// Rudimentary check to see if it's not a server side request - /// - public bool IsClientSideRequest(string absPath) + // check for special back office paths + if (urlPath.InvariantStartsWith(_backOfficeMvcPath) + || urlPath.InvariantStartsWith(_previewMvcPath)) { - var ext = Path.GetExtension(absPath); - return !ext.IsNullOrWhiteSpace(); + return true; } + + // check for special front-end paths + if (urlPath.InvariantStartsWith(_surfaceMvcPath) + || urlPath.InvariantStartsWith(_apiMvcPath)) + { + return false; + } + + // if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by + // checking how many parts the route has, for example, all PluginController routes will be routed like + // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} + // so if the path contains at a minimum 3 parts: Umbraco + MYPLUGINAREA + MYCONTROLLERNAME then we will have to assume it is a + // plugin controller for the front-end. + if (urlPath.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries).Length >= 3) + { + return false; + } + + // if its anything else we can assume it's back office + return true; + } + + /// + /// Checks if the current uri is an install request + /// + public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_installPath); + + /// + /// Rudimentary check to see if it's not a server side request + /// + public bool IsClientSideRequest(string absPath) + { + var ext = Path.GetExtension(absPath); + return !ext.IsNullOrWhiteSpace(); } } diff --git a/src/Umbraco.Core/Routing/UmbracoRouteResult.cs b/src/Umbraco.Core/Routing/UmbracoRouteResult.cs index d41c7ad7c3..67690e11e8 100644 --- a/src/Umbraco.Core/Routing/UmbracoRouteResult.cs +++ b/src/Umbraco.Core/Routing/UmbracoRouteResult.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public enum UmbracoRouteResult { - public enum UmbracoRouteResult - { - /// - /// Routing was successful and a content item was matched - /// - Success, + /// + /// Routing was successful and a content item was matched + /// + Success, - /// - /// A redirection took place - /// - Redirect, + /// + /// A redirection took place + /// + Redirect, - /// - /// Nothing matched - /// - NotFound - } + /// + /// Nothing matched + /// + NotFound, } diff --git a/src/Umbraco.Core/Routing/UriUtility.cs b/src/Umbraco.Core/Routing/UriUtility.cs index b973bdd068..fb59ada249 100644 --- a/src/Umbraco.Core/Routing/UriUtility.cs +++ b/src/Umbraco.Core/Routing/UriUtility.cs @@ -1,201 +1,230 @@ -using System; using System.Text; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public sealed class UriUtility { - public sealed class UriUtility + private static string? _appPath; + private static string? _appPathPrefix; + + public UriUtility(IHostingEnvironment hostingEnvironment) { - static string? _appPath; - static string? _appPathPrefix; - - public UriUtility(IHostingEnvironment hostingEnvironment) + if (hostingEnvironment is null) { - if (hostingEnvironment is null) throw new ArgumentNullException(nameof(hostingEnvironment)); - ResetAppDomainAppVirtualPath(hostingEnvironment); + throw new ArgumentNullException(nameof(hostingEnvironment)); } - // internal for unit testing only - internal void SetAppDomainAppVirtualPath(string appPath) + ResetAppDomainAppVirtualPath(hostingEnvironment); + } + + // will be "/" or "/foo" + public string? AppPath => _appPath; + + // will be "" or "/foo" + public string? AppPathPrefix => _appPathPrefix; + + // adds the virtual directory if any + // see also VirtualPathUtility.ToAbsolute + // TODO: Does this do anything differently than IHostingEnvironment.ToAbsolute? Seems it does less, maybe should be removed? + public string ToAbsolute(string url) + { + // return ResolveUrl(url); + url = url.TrimStart(Constants.CharArrays.Tilde); + return _appPathPrefix + url; + } + + // internal for unit testing only + internal void SetAppDomainAppVirtualPath(string appPath) + { + _appPath = appPath ?? "/"; + _appPathPrefix = _appPath; + if (_appPathPrefix == "/") { - _appPath = appPath ?? "/"; - _appPathPrefix = _appPath; - if (_appPathPrefix == "/") - _appPathPrefix = String.Empty; + _appPathPrefix = string.Empty; + } + } + + internal void ResetAppDomainAppVirtualPath(IHostingEnvironment hostingEnvironment) => + SetAppDomainAppVirtualPath(hostingEnvironment.ApplicationVirtualPath); + + // strips the virtual directory if any + // see also VirtualPathUtility.ToAppRelative + public string ToAppRelative(string virtualPath) + { + if (_appPathPrefix is not null && virtualPath.InvariantStartsWith(_appPathPrefix) + && (virtualPath.Length == _appPathPrefix.Length || + virtualPath[_appPathPrefix.Length] == '/')) + { + virtualPath = virtualPath[_appPathPrefix.Length..]; } - internal void ResetAppDomainAppVirtualPath(IHostingEnvironment hostingEnvironment) + if (virtualPath.Length == 0) { - SetAppDomainAppVirtualPath(hostingEnvironment.ApplicationVirtualPath); + virtualPath = "/"; } - // will be "/" or "/foo" - public string? AppPath => _appPath; + return virtualPath; + } - // will be "" or "/foo" - public string? AppPathPrefix => _appPathPrefix; + // maps an internal umbraco uri to a public uri + // ie with virtual directory, .aspx if required... + public Uri UriFromUmbraco(Uri uri, RequestHandlerSettings requestConfig) + { + var path = uri.GetSafeAbsolutePath(); - // adds the virtual directory if any - // see also VirtualPathUtility.ToAbsolute - // TODO: Does this do anything differently than IHostingEnvironment.ToAbsolute? Seems it does less, maybe should be removed? - public string ToAbsolute(string url) + if (path != "/" && requestConfig.AddTrailingSlash) { - //return ResolveUrl(url); - url = url.TrimStart(Constants.CharArrays.Tilde); - return _appPathPrefix + url; + path = path.EnsureEndsWith("/"); } - // strips the virtual directory if any - // see also VirtualPathUtility.ToAppRelative - public string ToAppRelative(string virtualPath) + path = ToAbsolute(path); + + return uri.Rewrite(path); + } + + // maps a media umbraco uri to a public uri + // ie with virtual directory - that is all for media + public Uri MediaUriFromUmbraco(Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + path = ToAbsolute(path); + return uri.Rewrite(path); + } + + // maps a public uri to an internal umbraco uri + // ie no virtual directory, no .aspx, lowercase... + public Uri UriToUmbraco(Uri uri) + { + // TODO: This is critical code that executes on every request, we should + // look into if all of this is necessary? not really sure we need ToLower? + + // note: no need to decode uri here because we're returning a uri + // so it will be re-encoded anyway + var path = uri.GetSafeAbsolutePath(); + + path = path.ToLower(); + path = ToAppRelative(path); // strip vdir if any + + if (path != "/") { - if (_appPathPrefix is not null && virtualPath.InvariantStartsWith(_appPathPrefix) - && (virtualPath.Length == _appPathPrefix.Length || virtualPath[_appPathPrefix.Length] == '/')) + path = path.TrimEnd(Constants.CharArrays.ForwardSlash); + } + + return uri.Rewrite(path); + } + + #region ResolveUrl + + // http://www.codeproject.com/Articles/53460/ResolveUrl-in-ASP-NET-The-Perfect-Solution + // note + // if browsing http://example.com/sub/page1.aspx then + // ResolveUrl("page2.aspx") returns "/page2.aspx" + // Page.ResolveUrl("page2.aspx") returns "/sub/page2.aspx" (relative...) + public string ResolveUrl(string relativeUrl) + { + if (relativeUrl == null) + { + throw new ArgumentNullException("relativeUrl"); + } + + if (relativeUrl.Length == 0 || relativeUrl[0] == '/' || relativeUrl[0] == '\\') + { + return relativeUrl; + } + + var idxOfScheme = relativeUrl.IndexOf(@"://", StringComparison.Ordinal); + if (idxOfScheme != -1) + { + var idxOfQM = relativeUrl.IndexOf('?'); + if (idxOfQM == -1 || idxOfQM > idxOfScheme) { - virtualPath = virtualPath.Substring(_appPathPrefix.Length); - } - - if (virtualPath.Length == 0) - { - virtualPath = "/"; - } - - return virtualPath; - } - - // maps an internal umbraco uri to a public uri - // ie with virtual directory, .aspx if required... - public Uri UriFromUmbraco(Uri uri, RequestHandlerSettings requestConfig) - { - var path = uri.GetSafeAbsolutePath(); - - if (path != "/" && requestConfig.AddTrailingSlash) - path = path.EnsureEndsWith("/"); - - path = ToAbsolute(path); - - return uri.Rewrite(path); - } - - // maps a media umbraco uri to a public uri - // ie with virtual directory - that is all for media - public Uri MediaUriFromUmbraco(Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - path = ToAbsolute(path); - return uri.Rewrite(path); - } - - // maps a public uri to an internal umbraco uri - // ie no virtual directory, no .aspx, lowercase... - public Uri UriToUmbraco(Uri uri) - { - // TODO: This is critical code that executes on every request, we should - // look into if all of this is necessary? not really sure we need ToLower? - - // note: no need to decode uri here because we're returning a uri - // so it will be re-encoded anyway - var path = uri.GetSafeAbsolutePath(); - - path = path.ToLower(); - path = ToAppRelative(path); // strip vdir if any - - if (path != "/") - { - path = path.TrimEnd(Constants.CharArrays.ForwardSlash); - } - - return uri.Rewrite(path); - } - - #region ResolveUrl - - // http://www.codeproject.com/Articles/53460/ResolveUrl-in-ASP-NET-The-Perfect-Solution - // note - // if browsing http://example.com/sub/page1.aspx then - // ResolveUrl("page2.aspx") returns "/page2.aspx" - // Page.ResolveUrl("page2.aspx") returns "/sub/page2.aspx" (relative...) - // - public string ResolveUrl(string relativeUrl) - { - if (relativeUrl == null) throw new ArgumentNullException("relativeUrl"); - - if (relativeUrl.Length == 0 || relativeUrl[0] == '/' || relativeUrl[0] == '\\') return relativeUrl; - - int idxOfScheme = relativeUrl.IndexOf(@"://", StringComparison.Ordinal); - if (idxOfScheme != -1) - { - int idxOfQM = relativeUrl.IndexOf('?'); - if (idxOfQM == -1 || idxOfQM > idxOfScheme) return relativeUrl; } + } - StringBuilder sbUrl = new StringBuilder(); - sbUrl.Append(_appPathPrefix); - if (sbUrl.Length == 0 || sbUrl[sbUrl.Length - 1] != '/') sbUrl.Append('/'); + var sbUrl = new StringBuilder(); + sbUrl.Append(_appPathPrefix); + if (sbUrl.Length == 0 || sbUrl[^1] != '/') + { + sbUrl.Append('/'); + } - // found question mark already? query string, do not touch! - bool foundQM = false; - bool foundSlash; // the latest char was a slash? - if (relativeUrl.Length > 1 - && relativeUrl[0] == '~' - && (relativeUrl[1] == '/' || relativeUrl[1] == '\\')) + // found question mark already? query string, do not touch! + var foundQM = false; + bool foundSlash; // the latest char was a slash? + if (relativeUrl.Length > 1 + && relativeUrl[0] == '~' + && (relativeUrl[1] == '/' || relativeUrl[1] == '\\')) + { + relativeUrl = relativeUrl[2..]; + foundSlash = true; + } + else + { + foundSlash = false; + } + + foreach (var c in relativeUrl) + { + if (!foundQM) { - relativeUrl = relativeUrl.Substring(2); - foundSlash = true; - } - else foundSlash = false; - foreach (char c in relativeUrl) - { - if (!foundQM) + if (c == '?') { - if (c == '?') foundQM = true; - else + foundQM = true; + } + else + { + if (c == '/' || c == '\\') { - if (c == '/' || c == '\\') + if (foundSlash) { - if (foundSlash) continue; - else - { - sbUrl.Append('/'); - foundSlash = true; - continue; - } + continue; } - else if (foundSlash) foundSlash = false; + + sbUrl.Append('/'); + foundSlash = true; + continue; + } + + if (foundSlash) + { + foundSlash = false; } } - sbUrl.Append(c); } - return sbUrl.ToString(); + sbUrl.Append(c); } - #endregion + return sbUrl.ToString(); + } + #endregion - /// - /// Returns an full URL with the host, port, etc... - /// - /// An absolute path (i.e. starts with a '/' ) - /// - /// - /// - /// Based on http://stackoverflow.com/questions/3681052/get-absolute-url-from-relative-path-refactored-method - /// - internal Uri ToFullUrl(string absolutePath, Uri curentRequestUrl) + /// + /// Returns an full URL with the host, port, etc... + /// + /// An absolute path (i.e. starts with a '/' ) + /// + /// + /// + /// Based on http://stackoverflow.com/questions/3681052/get-absolute-url-from-relative-path-refactored-method + /// + internal Uri ToFullUrl(string absolutePath, Uri curentRequestUrl) + { + if (string.IsNullOrEmpty(absolutePath)) { - if (string.IsNullOrEmpty(absolutePath)) - throw new ArgumentNullException(nameof(absolutePath)); - - if (!absolutePath.StartsWith("/")) - throw new FormatException("The absolutePath specified does not start with a '/'"); - - return new Uri(absolutePath, UriKind.Relative).MakeAbsolute(curentRequestUrl); + throw new ArgumentNullException(nameof(absolutePath)); } + if (!absolutePath.StartsWith("/")) + { + throw new FormatException("The absolutePath specified does not start with a '/'"); + } + return new Uri(absolutePath, UriKind.Relative).MakeAbsolute(curentRequestUrl); } } diff --git a/src/Umbraco.Core/Routing/UrlInfo.cs b/src/Umbraco.Core/Routing/UrlInfo.cs index 3a5c277725..f5b208fb73 100644 --- a/src/Umbraco.Core/Routing/UrlInfo.cs +++ b/src/Umbraco.Core/Routing/UrlInfo.cs @@ -1,102 +1,117 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +/// +/// Represents infos for a URL. +/// +[DataContract(Name = "urlInfo", Namespace = "")] +public class UrlInfo : IEquatable { /// - /// Represents infos for a URL. + /// Initializes a new instance of the class. /// - [DataContract(Name = "urlInfo", Namespace = "")] - public class UrlInfo : IEquatable + public UrlInfo(string text, bool isUrl, string? culture) { - - /// - /// Creates a instance representing a true URL. - /// - public static UrlInfo Url(string text, string? culture = null) => new UrlInfo(text, true, culture); - - /// - /// Creates a instance representing a message. - /// - public static UrlInfo Message(string text, string? culture = null) => new UrlInfo(text, false, culture); - - /// - /// Initializes a new instance of the class. - /// - public UrlInfo(string text, bool isUrl, string? culture) + if (string.IsNullOrWhiteSpace(text)) { - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(text)); - IsUrl = isUrl; - Text = text; - Culture = culture; + throw new ArgumentException("Value cannot be null or whitespace.", nameof(text)); } - /// - /// Gets the culture. - /// - [DataMember(Name = "culture")] - public string? Culture { get; } + IsUrl = isUrl; + Text = text; + Culture = culture; + } - /// - /// Gets a value indicating whether the URL is a true URL. - /// - /// Otherwise, it is a message. - [DataMember(Name = "isUrl")] - public bool IsUrl { get; } + /// + /// Gets the culture. + /// + [DataMember(Name = "culture")] + public string? Culture { get; } - /// - /// Gets the text, which is either the URL, or a message. - /// - [DataMember(Name = "text")] - public string Text { get; } + /// + /// Gets a value indicating whether the URL is a true URL. + /// + /// Otherwise, it is a message. + [DataMember(Name = "isUrl")] + public bool IsUrl { get; } - /// - /// Checks equality - /// - /// - /// - /// - /// Compare both culture and Text as invariant strings since URLs are not case sensitive, nor are culture names within Umbraco - /// - public bool Equals(UrlInfo? other) + /// + /// Gets the text, which is either the URL, or a message. + /// + [DataMember(Name = "text")] + public string Text { get; } + + public static bool operator ==(UrlInfo left, UrlInfo right) => Equals(left, right); + + /// + /// Creates a instance representing a true URL. + /// + public static UrlInfo Url(string text, string? culture = null) => new(text, true, culture); + + /// + /// Checks equality + /// + /// + /// + /// + /// Compare both culture and Text as invariant strings since URLs are not case sensitive, nor are culture names within + /// Umbraco + /// + public bool Equals(UrlInfo? other) + { + if (ReferenceEquals(null, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return string.Equals(Culture, other.Culture, StringComparison.InvariantCultureIgnoreCase) && IsUrl == other.IsUrl && string.Equals(Text, other.Text, StringComparison.InvariantCultureIgnoreCase); + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((UrlInfo) obj); + return true; } - public override int GetHashCode() + return string.Equals(Culture, other.Culture, StringComparison.InvariantCultureIgnoreCase) && + IsUrl == other.IsUrl && string.Equals(Text, other.Text, StringComparison.InvariantCultureIgnoreCase); + } + + /// + /// Creates a instance representing a message. + /// + public static UrlInfo Message(string text, string? culture = null) => new(text, false, culture); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - unchecked - { - var hashCode = (Culture != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Culture) : 0); - hashCode = (hashCode * 397) ^ IsUrl.GetHashCode(); - hashCode = (hashCode * 397) ^ (Text != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Text) : 0); - return hashCode; - } + return false; } - public static bool operator ==(UrlInfo left, UrlInfo right) + if (ReferenceEquals(this, obj)) { - return Equals(left, right); + return true; } - public static bool operator !=(UrlInfo left, UrlInfo right) + if (obj.GetType() != GetType()) { - return !Equals(left, right); + return false; } - public override string ToString() + return Equals((UrlInfo)obj); + } + + public override int GetHashCode() + { + unchecked { - return Text; + var hashCode = Culture != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Culture) : 0; + hashCode = (hashCode * 397) ^ IsUrl.GetHashCode(); + hashCode = (hashCode * 397) ^ + (Text != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Text) : 0); + return hashCode; } } + + public static bool operator !=(UrlInfo left, UrlInfo right) => !Equals(left, right); + + public override string ToString() => Text; } diff --git a/src/Umbraco.Core/Routing/UrlProvider.cs b/src/Umbraco.Core/Routing/UrlProvider.cs index e19e388893..97385a144b 100644 --- a/src/Umbraco.Core/Routing/UrlProvider.cs +++ b/src/Umbraco.Core/Routing/UrlProvider.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -51,17 +48,17 @@ namespace Umbraco.Cms.Core.Routing private IPublishedContent? GetDocument(int id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return umbracoContext.Content?.GetById(id); } private IPublishedContent? GetDocument(Guid id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return umbracoContext.Content?.GetById(id); } private IPublishedContent? GetMedia(Guid id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return umbracoContext.Media?.GetById(id); } @@ -104,10 +101,14 @@ namespace Umbraco.Cms.Core.Routing public string GetUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, Uri? current = null) { if (content == null || content.ContentType.ItemType == PublishedItemType.Element) + { return "#"; + } if (mode == UrlMode.Default) + { mode = Mode; + } // this the ONLY place where we deal with default culture - IUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant @@ -115,25 +116,25 @@ namespace Umbraco.Cms.Core.Routing // We need to check all ancestors because urls are variant even for invariant content, if an ancestor is variant. if (culture == null && content.AncestorsOrSelf().Any(x => x.ContentType.VariesByCulture())) { - culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } if (current == null) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); current = umbracoContext.CleanedUmbracoUrl; } - var url = _urlProviders.Select(provider => provider.GetUrl(content, mode, culture, current)) + UrlInfo? url = _urlProviders.Select(provider => provider.GetUrl(content, mode, culture, current)) .FirstOrDefault(u => u is not null); return url?.Text ?? "#"; // legacy wants this } public string GetUrlFromRoute(int id, string? route, string? culture) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var provider = _urlProviders.OfType().FirstOrDefault(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + DefaultUrlProvider? provider = _urlProviders.OfType().FirstOrDefault(); var url = provider == null ? route // what else? : provider.GetUrlFromRoute(route, umbracoContext, id, umbracoContext.CleanedUmbracoUrl, Mode, culture)?.Text; @@ -156,7 +157,7 @@ namespace Umbraco.Cms.Core.Routing /// public IEnumerable GetOtherUrls(int id) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); return GetOtherUrls(id, umbracoContext.CleanedUmbracoUrl); } @@ -204,18 +205,24 @@ namespace Umbraco.Cms.Core.Routing /// The URL is absolute or relative depending on mode and on current. /// If the media is multi-lingual, gets the URL for the specified culture or, /// when no culture is specified, the current culture. - /// If the provider is unable to provide a URL, it returns . + /// If the provider is unable to provide a URL, it returns . /// public string GetMediaUrl(IPublishedContent? content, UrlMode mode = UrlMode.Default, string? culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri? current = null) { if (propertyAlias == null) + { throw new ArgumentNullException(nameof(propertyAlias)); + } if (content == null) - return ""; + { + return string.Empty; + } if (mode == UrlMode.Default) + { mode = Mode; + } // this the ONLY place where we deal with default culture - IMediaUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant @@ -223,21 +230,23 @@ namespace Umbraco.Cms.Core.Routing if (content.ContentType.VariesByCulture()) { if (culture == null) - culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + { + culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty; + } } if (current == null) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); current = umbracoContext.CleanedUmbracoUrl; } - var url = _mediaUrlProviders.Select(provider => + UrlInfo? url = _mediaUrlProviders.Select(provider => provider.GetMediaUrl(content, propertyAlias, mode, culture, current)) .FirstOrDefault(u => u is not null); - return url?.Text ?? ""; + return url?.Text ?? string.Empty; } #endregion diff --git a/src/Umbraco.Core/Routing/UrlProviderCollection.cs b/src/Umbraco.Core/Routing/UrlProviderCollection.cs index c17417c83c..0acb75264d 100644 --- a/src/Umbraco.Core/Routing/UrlProviderCollection.cs +++ b/src/Umbraco.Core/Routing/UrlProviderCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class UrlProviderCollection : BuilderCollectionBase { - public class UrlProviderCollection : BuilderCollectionBase + public UrlProviderCollection(Func> items) + : base(items) { - public UrlProviderCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Routing/UrlProviderCollectionBuilder.cs b/src/Umbraco.Core/Routing/UrlProviderCollectionBuilder.cs index ca6f703c8b..fe975272dd 100644 --- a/src/Umbraco.Core/Routing/UrlProviderCollectionBuilder.cs +++ b/src/Umbraco.Core/Routing/UrlProviderCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class UrlProviderCollectionBuilder : OrderedCollectionBuilderBase { - public class UrlProviderCollectionBuilder : OrderedCollectionBuilderBase - { - protected override UrlProviderCollectionBuilder This => this; - } + protected override UrlProviderCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index 5f28bee2b7..8e2a577f3a 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -6,250 +6,256 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UrlProviderExtensions { - public static class UrlProviderExtensions + /// + /// Gets the URLs of the content item. + /// + /// + /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list. + /// Contains all the URLs that we can figure out (based upon domains, etc). + /// + public static async Task> GetContentUrlsAsync( + this IContent content, + IPublishedRouter publishedRouter, + IUmbracoContext umbracoContext, + ILocalizationService localizationService, + ILocalizedTextService textService, + IContentService contentService, + IVariationContextAccessor variationContextAccessor, + ILogger logger, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider) { - /// - /// Gets the URLs of the content item. - /// - /// - /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list. - /// Contains all the URLs that we can figure out (based upon domains, etc). - /// - public static async Task> GetContentUrlsAsync( - this IContent content, - IPublishedRouter publishedRouter, - IUmbracoContext umbracoContext, - ILocalizationService localizationService, - ILocalizedTextService textService, - IContentService contentService, - IVariationContextAccessor variationContextAccessor, - ILogger logger, - UriUtility uriUtility, - IPublishedUrlProvider publishedUrlProvider) + ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(publishedRouter); + ArgumentNullException.ThrowIfNull(umbracoContext); + ArgumentNullException.ThrowIfNull(localizationService); + ArgumentNullException.ThrowIfNull(textService); + ArgumentNullException.ThrowIfNull(contentService); + ArgumentNullException.ThrowIfNull(variationContextAccessor); + ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(uriUtility); + ArgumentNullException.ThrowIfNull(publishedUrlProvider); + + var result = new List(); + + if (content.Published == false) { - ArgumentNullException.ThrowIfNull(content); - ArgumentNullException.ThrowIfNull(publishedRouter); - ArgumentNullException.ThrowIfNull(umbracoContext); - ArgumentNullException.ThrowIfNull(localizationService); - ArgumentNullException.ThrowIfNull(textService); - ArgumentNullException.ThrowIfNull(contentService); - ArgumentNullException.ThrowIfNull(variationContextAccessor); - ArgumentNullException.ThrowIfNull(logger); - ArgumentNullException.ThrowIfNull(uriUtility); - ArgumentNullException.ThrowIfNull(publishedUrlProvider); - - var result = new List(); - - if (content.Published == false) - { - result.Add(UrlInfo.Message(textService.Localize("content", "itemNotPublished"))); - return result; - } - - // build a list of URLs, for the back-office - // which will contain - // - the 'main' URLs, which is what .Url would return, for each culture - // - the 'other' URLs we know (based upon domains, etc) - // - // need to work through each installed culture: - // on invariant nodes, each culture returns the same URL segment but, - // we don't know if the branch to this content is invariant, so we need to ask - // for URLs for all cultures. - // and, not only for those assigned to domains in the branch, because we want - // to show what GetUrl() would return, for every culture. - var urls = new HashSet(); - var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList(); - - // get all URLs for all cultures - // in a HashSet, so de-duplicates too - foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider)) - { - urls.Add(cultureUrl); - } - - // return the real URLs first, then the messages - foreach (IGrouping urlGroup in urls.GroupBy(x => x.IsUrl).OrderByDescending(x => x.Key)) - { - // in some cases there will be the same URL for multiple cultures: - // * The entire branch is invariant - // * If there are less domain/cultures assigned to the branch than the number of cultures/languages installed - if (urlGroup.Key) - { - result.AddRange(urlGroup.DistinctBy(x => x.Text, StringComparer.OrdinalIgnoreCase).OrderBy(x => x.Text).ThenBy(x => x.Culture)); - } - else - { - result.AddRange(urlGroup); - } - } - - // get the 'other' URLs - ie not what you'd get with GetUrl() but URLs that would route to the document, nevertheless. - // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. - foreach (UrlInfo otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture)) - { - // avoid duplicates - if (urls.Add(otherUrl)) - { - result.Add(otherUrl); - } - } - + result.Add(UrlInfo.Message(textService.Localize("content", "itemNotPublished"))); return result; } - /// - /// Tries to return a for each culture for the content while detecting collisions/errors - /// - private static async Task> GetContentUrlsByCultureAsync( - IContent content, - IEnumerable cultures, - IPublishedRouter publishedRouter, - IUmbracoContext umbracoContext, - IContentService contentService, - ILocalizedTextService textService, - IVariationContextAccessor variationContextAccessor, - ILogger logger, - UriUtility uriUtility, - IPublishedUrlProvider publishedUrlProvider) + // build a list of URLs, for the back-office + // which will contain + // - the 'main' URLs, which is what .Url would return, for each culture + // - the 'other' URLs we know (based upon domains, etc) + // + // need to work through each installed culture: + // on invariant nodes, each culture returns the same URL segment but, + // we don't know if the branch to this content is invariant, so we need to ask + // for URLs for all cultures. + // and, not only for those assigned to domains in the branch, because we want + // to show what GetUrl() would return, for every culture. + var urls = new HashSet(); + var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList(); + + // get all URLs for all cultures + // in a HashSet, so de-duplicates too + foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider)) { - var result = new List(); - - foreach (var culture in cultures) - { - // if content is variant, and culture is not published, skip - if (content.ContentType.VariesByCulture() && !content.IsCulturePublished(culture)) - { - continue; - } - - // if it's variant and culture is published, or if it's invariant, proceed - string url; - try - { - url = publishedUrlProvider.GetUrl(content.Id, culture: culture); - } - catch (Exception ex) - { - logger.LogError(ex, "GetUrl exception."); - url = "#ex"; - } - - switch (url) - { - // deal with 'could not get the URL' - case "#": - result.Add(HandleCouldNotGetUrl(content, culture, contentService, textService)); - break; - - // deal with exceptions - case "#ex": - result.Add(UrlInfo.Message(textService.Localize("content", "getUrlException"), culture)); - break; - - // got a URL, deal with collisions, add URL - default: - // detect collisions, etc - Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility); - if (hasCollision.Success && hasCollision.Result is not null) - { - result.Add(hasCollision.Result); - } - else - { - result.Add(UrlInfo.Url(url, culture)); - } - - break; - } - } - - return result; + urls.Add(cultureUrl); } - private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService) + // return the real URLs first, then the messages + foreach (IGrouping urlGroup in urls.GroupBy(x => x.IsUrl).OrderByDescending(x => x.Key)) { - // document has a published version yet its URL is "#" => a parent must be - // unpublished, walk up the tree until we find it, and report. - IContent? parent = content; - do + // in some cases there will be the same URL for multiple cultures: + // * The entire branch is invariant + // * If there are less domain/cultures assigned to the branch than the number of cultures/languages installed + if (urlGroup.Key) { - parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null; + result.AddRange(urlGroup.DistinctBy(x => x.Text, StringComparer.OrdinalIgnoreCase).OrderBy(x => x.Text) + .ThenBy(x => x.Culture)); } - while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); - - if (parent == null) + else { - // oops, internal error - return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); + result.AddRange(urlGroup); } - - if (!parent.Published) - { - // totally not published - return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture); - } - - // culture not published - return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }), culture); } - private static async Task> DetectCollisionAsync( - ILogger logger, - IContent content, - string url, - string culture, - IUmbracoContext umbracoContext, - IPublishedRouter publishedRouter, - ILocalizedTextService textService, - IVariationContextAccessor variationContextAccessor, - UriUtility uriUtility) + // get the 'other' URLs - ie not what you'd get with GetUrl() but URLs that would route to the document, nevertheless. + // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. + foreach (UrlInfo otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text) + .ThenBy(x => x.Culture)) { - // test for collisions on the 'main' URL - var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); - if (uri.IsAbsoluteUri == false) + // avoid duplicates + if (urls.Add(otherUrl)) { - uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl); + result.Add(otherUrl); + } + } + + return result; + } + + /// + /// Tries to return a for each culture for the content while detecting collisions/errors + /// + private static async Task> GetContentUrlsByCultureAsync( + IContent content, + IEnumerable cultures, + IPublishedRouter publishedRouter, + IUmbracoContext umbracoContext, + IContentService contentService, + ILocalizedTextService textService, + IVariationContextAccessor variationContextAccessor, + ILogger logger, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider) + { + var result = new List(); + + foreach (var culture in cultures) + { + // if content is variant, and culture is not published, skip + if (content.ContentType.VariesByCulture() && !content.IsCulturePublished(culture)) + { + continue; } - uri = uriUtility.UriToUmbraco(uri); - IPublishedRequestBuilder builder = await publishedRouter.CreateRequestAsync(uri); - IPublishedRequest pcr = await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); - - if (!pcr.HasPublishedContent()) + // if it's variant and culture is published, or if it's invariant, proceed + string url; + try { - const string logMsg = nameof(DetectCollisionAsync) + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; - logger.LogDebug(logMsg, url, uri, culture); - - var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); - return Attempt.Succeed(urlInfo); + url = publishedUrlProvider.GetUrl(content.Id, culture: culture); + } + catch (Exception ex) + { + logger.LogError(ex, "GetUrl exception."); + url = "#ex"; } - if (pcr.IgnorePublishedContentCollisions) + switch (url) { - return Attempt.Fail(); + // deal with 'could not get the URL' + case "#": + result.Add(HandleCouldNotGetUrl(content, culture, contentService, textService)); + break; + + // deal with exceptions + case "#ex": + result.Add(UrlInfo.Message(textService.Localize("content", "getUrlException"), culture)); + break; + + // got a URL, deal with collisions, add URL + default: + // detect collisions, etc + Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility); + if (hasCollision.Success && hasCollision.Result is not null) + { + result.Add(hasCollision.Result); + } + else + { + result.Add(UrlInfo.Url(url, culture)); + } + + break; } + } - if (pcr.PublishedContent?.Id != content.Id) - { - IPublishedContent? o = pcr.PublishedContent; - var l = new List(); - while (o != null) - { - l.Add(o.Name(variationContextAccessor)!); - o = o.Parent; - } + return result; + } - l.Reverse(); - var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent?.Id + ")"; + private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService) + { + // document has a published version yet its URL is "#" => a parent must be + // unpublished, walk up the tree until we find it, and report. + IContent? parent = content; + do + { + parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null; + } + while (parent != null && parent.Published && + (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); - var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture); - return Attempt.Succeed(urlInfo); - } + if (parent == null) + { + // oops, internal error + return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); + } - // no collision + if (!parent.Published) + { + // totally not published + return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture); + } + + // culture not published + return UrlInfo.Message( + textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }), + culture); + } + + private static async Task> DetectCollisionAsync( + ILogger logger, + IContent content, + string url, + string culture, + IUmbracoContext umbracoContext, + IPublishedRouter publishedRouter, + ILocalizedTextService textService, + IVariationContextAccessor variationContextAccessor, + UriUtility uriUtility) + { + // test for collisions on the 'main' URL + var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri == false) + { + uri = uri.MakeAbsolute(umbracoContext.CleanedUmbracoUrl); + } + + uri = uriUtility.UriToUmbraco(uri); + IPublishedRequestBuilder builder = await publishedRouter.CreateRequestAsync(uri); + IPublishedRequest pcr = + await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); + + if (!pcr.HasPublishedContent()) + { + const string logMsg = nameof(DetectCollisionAsync) + + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; + logger.LogDebug(logMsg, url, uri, culture); + + var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); + return Attempt.Succeed(urlInfo); + } + + if (pcr.IgnorePublishedContentCollisions) + { return Attempt.Fail(); } + + if (pcr.PublishedContent?.Id != content.Id) + { + IPublishedContent? o = pcr.PublishedContent; + var l = new List(); + while (o != null) + { + l.Add(o.Name(variationContextAccessor)!); + o = o.Parent; + } + + l.Reverse(); + var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent?.Id + ")"; + + var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture); + return Attempt.Succeed(urlInfo); + } + + // no collision + return Attempt.Fail(); } } diff --git a/src/Umbraco.Core/Routing/WebPath.cs b/src/Umbraco.Core/Routing/WebPath.cs index a4da94ac79..7ecafff8a3 100644 --- a/src/Umbraco.Core/Routing/WebPath.cs +++ b/src/Umbraco.Core/Routing/WebPath.cs @@ -1,55 +1,53 @@ -using System; using System.Text; -namespace Umbraco.Cms.Core.Routing +namespace Umbraco.Cms.Core.Routing; + +public class WebPath { - public class WebPath + public const char PathSeparator = '/'; + + public static string Combine(params string[]? paths) { - public const char PathSeparator = '/'; - - public static string Combine(params string[]? paths) + if (paths == null) { - if (paths == null) - { - throw new ArgumentNullException(nameof(paths)); - } - - if (paths.Length == 0) - { - return string.Empty; - } - - var sb = new StringBuilder(); - - for (var index = 0; index < paths.Length; index++) - { - var path = paths[index]; - var start = 0; - var count = path.Length; - var isFirst = index == 0; - var isLast = index == paths.Length - 1; - - // don't trim start if it's the first - if (!isFirst && path[0] == PathSeparator) - { - start = 1; - } - - // always trim end - if (path[path.Length - 1] == PathSeparator) - { - count = path.Length - 1; - } - - sb.Append(path, start, count - start); - - if (!isLast) - { - sb.Append(PathSeparator); - } - } - - return sb.ToString(); + throw new ArgumentNullException(nameof(paths)); } + + if (paths.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(); + + for (var index = 0; index < paths.Length; index++) + { + var path = paths[index]; + var start = 0; + var count = path.Length; + var isFirst = index == 0; + var isLast = index == paths.Length - 1; + + // don't trim start if it's the first + if (!isFirst && path[0] == PathSeparator) + { + start = 1; + } + + // always trim end + if (path[^1] == PathSeparator) + { + count = path.Length - 1; + } + + sb.Append(path, start, count - start); + + if (!isLast) + { + sb.Append(PathSeparator); + } + } + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs index 6c45e4d969..8d7ec082be 100644 --- a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs +++ b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs @@ -5,31 +5,29 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +public class EssentialDirectoryCreator : INotificationHandler { - public class EssentialDirectoryCreator : INotificationHandler + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIOHelper _ioHelper; + + public EssentialDirectoryCreator(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) { - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly GlobalSettings _globalSettings; + _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; + _globalSettings = globalSettings.Value; + } - public EssentialDirectoryCreator(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IOptions globalSettings) - { - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - _globalSettings = globalSettings.Value; - } - - public void Handle(UmbracoApplicationStartingNotification notification) - { - // ensure we have some essential directories - // every other component can then initialize safely - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); - - } + public void Handle(UmbracoApplicationStartingNotification notification) + { + // ensure we have some essential directories + // every other component can then initialize safely + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); } } diff --git a/src/Umbraco.Core/Runtime/IMainDom.cs b/src/Umbraco.Core/Runtime/IMainDom.cs index 65c64857b3..59278e161c 100644 --- a/src/Umbraco.Core/Runtime/IMainDom.cs +++ b/src/Umbraco.Core/Runtime/IMainDom.cs @@ -1,39 +1,39 @@ -using System; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +/// +/// Represents the main AppDomain running for a given application. +/// +/// +/// There can be only one "main" AppDomain running for a given application at a time. +/// It is possible to register against the MainDom and be notified when it is released. +/// +public interface IMainDom { /// - /// Represents the main AppDomain running for a given application. + /// Gets a value indicating whether the current domain is the main domain. /// /// - /// There can be only one "main" AppDomain running for a given application at a time. - /// It is possible to register against the MainDom and be notified when it is released. + /// Acquire must be called first else this will always return false /// - public interface IMainDom - { - /// - /// Gets a value indicating whether the current domain is the main domain. - /// - /// - /// Acquire must be called first else this will always return false - /// - bool IsMainDom { get; } + bool IsMainDom { get; } - /// - /// Tries to acquire the MainDom, returns true if successful else false - /// - bool Acquire(IApplicationShutdownRegistry hostingEnvironment); + /// + /// Tries to acquire the MainDom, returns true if successful else false + /// + bool Acquire(IApplicationShutdownRegistry hostingEnvironment); - /// - /// Registers a resource that requires the current AppDomain to be the main domain to function. - /// - /// An action to execute when registering. - /// An action to execute before the AppDomain releases the main domain status. - /// An optional weight (lower goes first). - /// A value indicating whether it was possible to register. - /// If registering is successful, then the action - /// is guaranteed to execute before the AppDomain releases the main domain status. - bool Register(Action? install = null, Action? release = null, int weight = 100); - } + /// + /// Registers a resource that requires the current AppDomain to be the main domain to function. + /// + /// An action to execute when registering. + /// An action to execute before the AppDomain releases the main domain status. + /// An optional weight (lower goes first). + /// A value indicating whether it was possible to register. + /// + /// If registering is successful, then the action + /// is guaranteed to execute before the AppDomain releases the main domain status. + /// + bool Register(Action? install = null, Action? release = null, int weight = 100); } diff --git a/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs index 5b8fb819e6..cbfbffac4c 100644 --- a/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs +++ b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +/// +/// Defines a class which can generate a distinct key for a MainDom boundary. +/// +public interface IMainDomKeyGenerator { /// - /// Defines a class which can generate a distinct key for a MainDom boundary. + /// Returns a key that signifies a MainDom boundary. /// - public interface IMainDomKeyGenerator - { - /// - /// Returns a key that signifies a MainDom boundary. - /// - string GenerateKey(); - } + string GenerateKey(); } diff --git a/src/Umbraco.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs index b0b3394a01..7e58fa6533 100644 --- a/src/Umbraco.Core/Runtime/IMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -1,29 +1,25 @@ -using System; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Core.Runtime +/// +/// An application-wide distributed lock +/// +/// +/// Disposing releases the lock +/// +public interface IMainDomLock : IDisposable { /// - /// An application-wide distributed lock + /// Acquires an application-wide distributed lock /// - /// - /// Disposing releases the lock - /// - public interface IMainDomLock : IDisposable - { - /// - /// Acquires an application-wide distributed lock - /// - /// - /// - /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded - /// - Task AcquireLockAsync(int millisecondsTimeout); + /// + /// + /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded + /// + Task AcquireLockAsync(int millisecondsTimeout); - /// - /// Wait on a background thread to receive a signal from another AppDomain - /// - /// - Task ListenAsync(); - } + /// + /// Wait on a background thread to receive a signal from another AppDomain + /// + /// + Task ListenAsync(); } diff --git a/src/Umbraco.Core/Runtime/IUmbracoBootPermissionChecker.cs b/src/Umbraco.Core/Runtime/IUmbracoBootPermissionChecker.cs index 48ea6a5a48..b5c43cde4a 100644 --- a/src/Umbraco.Core/Runtime/IUmbracoBootPermissionChecker.cs +++ b/src/Umbraco.Core/Runtime/IUmbracoBootPermissionChecker.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +public interface IUmbracoBootPermissionChecker { - public interface IUmbracoBootPermissionChecker - { - void ThrowIfNotPermissions(); - } + void ThrowIfNotPermissions(); } diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 0198382b2a..83736914a2 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; @@ -27,7 +22,7 @@ namespace Umbraco.Cms.Core.Runtime private readonly IMainDomLock _mainDomLock; // our own lock for local consistency - private object _locko = new object(); + private object _locko = new(); private bool _isInitialized; // indicates whether... @@ -35,7 +30,7 @@ namespace Umbraco.Cms.Core.Runtime private volatile bool _signaled; // we have been signaled // actions to run before releasing the main domain - private readonly List> _callbacks = new List>(); + private readonly List> _callbacks = new(); private const int LockTimeoutMilliseconds = 40000; // 40 seconds @@ -114,14 +109,22 @@ namespace Umbraco.Cms.Core.Runtime lock (_locko) { _logger.LogDebug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source); - if (_signaled) return; - if (_isMainDom == false) return; // probably not needed + if (_signaled) + { + return; + } + + if (_isMainDom == false) + { + return; // probably not needed + } + _signaled = true; try { _logger.LogInformation("Stopping ({SignalSource})", source); - foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) + foreach (Action callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) { try { @@ -189,7 +192,8 @@ namespace Umbraco.Cms.Core.Runtime { // Listen for the signal from another AppDomain coming online to release the lock _listenTask = _mainDomLock.ListenAsync(); - _listenCompleteTask = _listenTask.ContinueWith(t => + _listenCompleteTask = _listenTask.ContinueWith( + t => { if (_listenTask.Exception != null) { @@ -201,7 +205,8 @@ namespace Umbraco.Cms.Core.Runtime } OnSignal("signal"); - }, TaskScheduler.Default); // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + }, + TaskScheduler.Default); // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html } catch (OperationCanceledException ex) { @@ -246,18 +251,18 @@ namespace Umbraco.Cms.Core.Runtime // This code added to correctly implement the disposable pattern. - private bool disposedValue = false; // To detect redundant calls + private bool _disposedValue; // To detect redundant calls protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposedValue) { if (disposing) { _mainDomLock.Dispose(); } - disposedValue = true; + _disposedValue = true; } } diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs index 5d2248906e..cdfd7b9305 100644 --- a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -1,104 +1,100 @@ -using System; using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Runtime +namespace Umbraco.Cms.Core.Runtime; + +/// +/// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain +/// +public class MainDomSemaphoreLock : IMainDomLock { - /// - /// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain - /// - public class MainDomSemaphoreLock : IMainDomLock + private readonly ILogger _logger; + + // event wait handle used to notify current main domain that it should + // release the lock because a new domain wants to be the main domain + private readonly EventWaitHandle _signal; + private readonly SystemLock _systemLock; + private IDisposable? _lockRelease; + + public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment) { - private readonly SystemLock _systemLock; - - // event wait handle used to notify current main domain that it should - // release the lock because a new domain wants to be the main domain - private readonly EventWaitHandle _signal; - private readonly ILogger _logger; - private IDisposable? _lockRelease; - - public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - throw new PlatformNotSupportedException("MainDomSemaphoreLock is only supported on Windows."); - } - - var mainDomId = MainDom.GetMainDomId(hostingEnvironment); - var lockName = "UMBRACO-" + mainDomId + "-MAINDOM-LCK"; - _systemLock = new SystemLock(lockName); - - var eventName = "UMBRACO-" + mainDomId + "-MAINDOM-EVT"; - _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); - _logger = logger; + throw new PlatformNotSupportedException("MainDomSemaphoreLock is only supported on Windows."); } - // WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread - public Task ListenAsync() => _signal.WaitOneAsync(); + var mainDomId = MainDom.GetMainDomId(hostingEnvironment); + var lockName = "UMBRACO-" + mainDomId + "-MAINDOM-LCK"; + _systemLock = new SystemLock(lockName); - public Task AcquireLockAsync(int millisecondsTimeout) - { - // signal other instances that we want the lock, then wait on the lock, - // which may timeout, and this is accepted - see comments below - - // signal, then wait for the lock, then make sure the event is - // reset (maybe there was noone listening..) - _signal.Set(); - - // if more than 1 instance reach that point, one will get the lock - // and the other one will timeout, which is accepted - - // This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. - try - { - _lockRelease = _systemLock.Lock(millisecondsTimeout); - return Task.FromResult(true); - } - catch (TimeoutException ex) - { - _logger.LogError(ex.Message); - return Task.FromResult(false); - } - finally - { - // we need to reset the event, because otherwise we would end up - // signaling ourselves and committing suicide immediately. - // only 1 instance can reach that point, but other instances may - // have started and be trying to get the lock - they will timeout, - // which is accepted - - _signal.Reset(); - } - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - _lockRelease?.Dispose(); - _signal.Close(); - _signal.Dispose(); - } - - disposedValue = true; - } - } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - } - #endregion + var eventName = "UMBRACO-" + mainDomId + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + _logger = logger; } + + // WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread + public Task ListenAsync() => _signal.WaitOneAsync(); + + public Task AcquireLockAsync(int millisecondsTimeout) + { + // signal other instances that we want the lock, then wait on the lock, + // which may timeout, and this is accepted - see comments below + + // signal, then wait for the lock, then make sure the event is + // reset (maybe there was noone listening..) + _signal.Set(); + + // if more than 1 instance reach that point, one will get the lock + // and the other one will timeout, which is accepted + + // This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. + try + { + _lockRelease = _systemLock.Lock(millisecondsTimeout); + return Task.FromResult(true); + } + catch (TimeoutException ex) + { + _logger.LogError(ex.Message); + return Task.FromResult(false); + } + finally + { + // we need to reset the event, because otherwise we would end up + // signaling ourselves and committing suicide immediately. + // only 1 instance can reach that point, but other instances may + // have started and be trying to get the lock - they will timeout, + // which is accepted + _signal.Reset(); + } + } + + #region IDisposable Support + + private bool disposedValue; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _lockRelease?.Dispose(); + _signal.Close(); + _signal.Dispose(); + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() => + + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + + #endregion } diff --git a/src/Umbraco.Core/RuntimeLevel.cs b/src/Umbraco.Core/RuntimeLevel.cs index d6687d4628..5b726045a9 100644 --- a/src/Umbraco.Core/RuntimeLevel.cs +++ b/src/Umbraco.Core/RuntimeLevel.cs @@ -1,40 +1,39 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Describes the levels in which the runtime can run. +/// +public enum RuntimeLevel { /// - /// Describes the levels in which the runtime can run. + /// The runtime has failed to boot and cannot run. /// - public enum RuntimeLevel - { - /// - /// The runtime has failed to boot and cannot run. - /// - BootFailed = -1, + BootFailed = -1, - /// - /// The level is unknown. - /// - Unknown = 0, + /// + /// The level is unknown. + /// + Unknown = 0, - /// - /// The runtime is booting. - /// - Boot = 1, + /// + /// The runtime is booting. + /// + Boot = 1, - /// - /// The runtime has detected that Umbraco is not installed at all, ie there is - /// no database, and is currently installing Umbraco. - /// - Install = 2, + /// + /// The runtime has detected that Umbraco is not installed at all, ie there is + /// no database, and is currently installing Umbraco. + /// + Install = 2, - /// - /// The runtime has detected an Umbraco install which needed to be upgraded, and - /// is currently upgrading Umbraco. - /// - Upgrade = 3, + /// + /// The runtime has detected an Umbraco install which needed to be upgraded, and + /// is currently upgrading Umbraco. + /// + Upgrade = 3, - /// - /// The runtime has detected an up-to-date Umbraco install and is running. - /// - Run = 100 - } + /// + /// The runtime has detected an up-to-date Umbraco install and is running. + /// + Run = 100, } diff --git a/src/Umbraco.Core/RuntimeLevelReason.cs b/src/Umbraco.Core/RuntimeLevelReason.cs index 94192c83b2..76f47e8a17 100644 --- a/src/Umbraco.Core/RuntimeLevelReason.cs +++ b/src/Umbraco.Core/RuntimeLevelReason.cs @@ -1,78 +1,77 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Describes the reason for the runtime level. +/// +public enum RuntimeLevelReason { /// - /// Describes the reason for the runtime level. + /// The reason is unknown. /// - public enum RuntimeLevelReason - { - /// - /// The reason is unknown. - /// - Unknown, + Unknown, - /// - /// The code version is lower than the version indicated in web.config, and - /// downgrading Umbraco is not supported. - /// - BootFailedCannotDowngrade, + /// + /// The code version is lower than the version indicated in web.config, and + /// downgrading Umbraco is not supported. + /// + BootFailedCannotDowngrade, - /// - /// The runtime cannot connect to the configured database. - /// - BootFailedCannotConnectToDatabase, + /// + /// The runtime cannot connect to the configured database. + /// + BootFailedCannotConnectToDatabase, - /// - /// The runtime can connect to the configured database, but it cannot - /// retrieve the migrations status. - /// - BootFailedCannotCheckUpgradeState, + /// + /// The runtime can connect to the configured database, but it cannot + /// retrieve the migrations status. + /// + BootFailedCannotCheckUpgradeState, - /// - /// An exception was thrown during boot. - /// - BootFailedOnException, + /// + /// An exception was thrown during boot. + /// + BootFailedOnException, - /// - /// Umbraco is not installed at all. - /// - InstallNoVersion, + /// + /// Umbraco is not installed at all. + /// + InstallNoVersion, - /// - /// A version is specified in web.config but the database is not configured. - /// - /// This is a weird state. - InstallNoDatabase, + /// + /// A version is specified in web.config but the database is not configured. + /// + /// This is a weird state. + InstallNoDatabase, - /// - /// A version is specified in web.config and a database is configured, but the - /// database is missing, and installing over a missing database has been enabled. - /// - InstallMissingDatabase, + /// + /// A version is specified in web.config and a database is configured, but the + /// database is missing, and installing over a missing database has been enabled. + /// + InstallMissingDatabase, - /// - /// A version is specified in web.config and a database is configured, but the - /// database is empty, and installing over an empty database has been enabled. - /// - InstallEmptyDatabase, + /// + /// A version is specified in web.config and a database is configured, but the + /// database is empty, and installing over an empty database has been enabled. + /// + InstallEmptyDatabase, - /// - /// Umbraco runs an old version. - /// - UpgradeOldVersion, + /// + /// Umbraco runs an old version. + /// + UpgradeOldVersion, - /// - /// Umbraco runs the current version but some migrations have not run. - /// - UpgradeMigrations, + /// + /// Umbraco runs the current version but some migrations have not run. + /// + UpgradeMigrations, - /// - /// Umbraco runs the current version but some package migrations have not run. - /// - UpgradePackageMigrations, + /// + /// Umbraco runs the current version but some package migrations have not run. + /// + UpgradePackageMigrations, - /// - /// Umbraco is running. - /// - Run - } + /// + /// Umbraco is running. + /// + Run, } diff --git a/src/Umbraco.Core/Scoping/ICoreScope.cs b/src/Umbraco.Core/Scoping/ICoreScope.cs index 8bb85ca29d..ef3cf91c4c 100644 --- a/src/Umbraco.Core/Scoping/ICoreScope.cs +++ b/src/Umbraco.Core/Scoping/ICoreScope.cs @@ -1,57 +1,56 @@ -using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; namespace Umbraco.Cms.Core.Scoping; /// -/// Represents a scope. +/// Represents a scope. /// public interface ICoreScope : IDisposable, IInstanceIdentifiable { /// - /// Gets the scope notification publisher + /// Gets the scope notification publisher /// IScopedNotificationPublisher Notifications { get; } /// - /// Gets the repositories cache mode. + /// Gets the repositories cache mode. /// RepositoryCacheMode RepositoryCacheMode { get; } /// - /// Gets the scope isolated cache. + /// Gets the scope isolated cache. /// IsolatedCaches IsolatedCaches { get; } /// - /// Completes the scope. + /// Completes the scope. /// /// A value indicating whether the scope has been successfully completed. /// Can return false if any child scope has not completed. bool Complete(); /// - /// Read-locks some lock objects. + /// Read-locks some lock objects. /// /// Array of lock object identifiers. void ReadLock(params int[] lockIds); /// - /// Write-locks some lock objects. + /// Write-locks some lock objects. /// /// Array of object identifiers. void WriteLock(params int[] lockIds); /// - /// Write-locks some lock objects. + /// Write-locks some lock objects. /// /// The database timeout in milliseconds /// The lock object identifier. void WriteLock(TimeSpan timeout, int lockId); /// - /// Read-locks some lock objects. + /// Read-locks some lock objects. /// /// The database timeout in milliseconds /// The lock object identifier. diff --git a/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs b/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs index d4fe496d38..792673453f 100644 --- a/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ICoreScopeProvider.cs @@ -2,49 +2,50 @@ using System.Data; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Core.Scoping +namespace Umbraco.Cms.Core.Scoping; + +/// +/// Provides scopes. +/// +public interface ICoreScopeProvider { /// - /// Provides scopes. + /// Gets the scope context. /// - public interface ICoreScopeProvider - { - /// - /// Creates an ambient scope. - /// - /// The transaction isolation level. - /// The repositories cache mode. - /// An optional events dispatcher. - /// An optional notification publisher. - /// A value indicating whether to scope the filesystems. - /// A value indicating whether this scope should always be registered in the call context. - /// A value indicating whether this scope is auto-completed. - /// The created ambient scope. - /// - /// The created scope becomes the ambient scope. - /// If an ambient scope already exists, it becomes the parent of the created scope. - /// When the created scope is disposed, the parent scope becomes the ambient scope again. - /// Parameters must be specified on the outermost scope, or must be compatible with the parents. - /// Auto-completed scopes should be used for read-only operations ONLY. Do not use them if you do not - /// understand the associated issues, such as the scope being completed even though an exception is thrown. - /// - ICoreScope CreateCoreScope( - IsolationLevel isolationLevel = IsolationLevel.Unspecified, - RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - IEventDispatcher? eventDispatcher = null, - IScopedNotificationPublisher? scopedNotificationPublisher = null, - bool? scopeFileSystems = null, - bool callContext = false, - bool autoComplete = false); + IScopeContext? Context { get; } - /// - /// Gets the scope context. - /// - IScopeContext? Context { get; } + /// + /// Creates an ambient scope. + /// + /// The transaction isolation level. + /// The repositories cache mode. + /// An optional events dispatcher. + /// An optional notification publisher. + /// A value indicating whether to scope the filesystems. + /// A value indicating whether this scope should always be registered in the call context. + /// A value indicating whether this scope is auto-completed. + /// The created ambient scope. + /// + /// The created scope becomes the ambient scope. + /// If an ambient scope already exists, it becomes the parent of the created scope. + /// When the created scope is disposed, the parent scope becomes the ambient scope again. + /// Parameters must be specified on the outermost scope, or must be compatible with the parents. + /// + /// Auto-completed scopes should be used for read-only operations ONLY. Do not use them if you do not + /// understand the associated issues, such as the scope being completed even though an exception is thrown. + /// + /// + ICoreScope CreateCoreScope( + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + IEventDispatcher? eventDispatcher = null, + IScopedNotificationPublisher? scopedNotificationPublisher = null, + bool? scopeFileSystems = null, + bool callContext = false, + bool autoComplete = false); - /// - /// Creates an instance of - /// - IQuery CreateQuery(); - } + /// + /// Creates an instance of + /// + IQuery CreateQuery(); } diff --git a/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs index 9d0bc9ceef..1942ecdc43 100644 --- a/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs +++ b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs @@ -1,16 +1,14 @@ -using System; +namespace Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Scoping +/// +/// Exposes an instance unique identifier. +/// +public interface IInstanceIdentifiable { /// - /// Exposes an instance unique identifier. + /// Gets the instance unique identifier. /// - public interface IInstanceIdentifiable - { - /// - /// Gets the instance unique identifier. - /// - Guid InstanceId { get; } - int CreatedThreadId { get; } - } + Guid InstanceId { get; } + + int CreatedThreadId { get; } } diff --git a/src/Umbraco.Core/Scoping/IScopeContext.cs b/src/Umbraco.Core/Scoping/IScopeContext.cs index 7f1302911a..26f17b31b0 100644 --- a/src/Umbraco.Core/Scoping/IScopeContext.cs +++ b/src/Umbraco.Core/Scoping/IScopeContext.cs @@ -1,52 +1,53 @@ -using System; +namespace Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Scoping +/// +/// Represents a scope context. +/// +/// +/// A scope context can enlist objects that will be attached to the scope, and available +/// for the duration of the scope. In addition, it can enlist actions, that will run when the +/// scope is exiting, and after the database transaction has been committed. +/// +public interface IScopeContext : IInstanceIdentifiable { /// - /// Represents a scope context. + /// Enlists an action. /// - /// A scope context can enlist objects that will be attached to the scope, and available - /// for the duration of the scope. In addition, it can enlist actions, that will run when the - /// scope is exiting, and after the database transaction has been committed. - public interface IScopeContext : IInstanceIdentifiable - { - /// - /// Enlists an action. - /// - /// The action unique identifier. - /// The action. - /// The optional action priority (default is 100, lower runs first). - /// - /// It is ok to enlist multiple action with the same key but only the first one will run. - /// The action boolean parameter indicates whether the scope completed or not. - /// - void Enlist(string key, Action action, int priority = 100); + /// The action unique identifier. + /// The action. + /// The optional action priority (default is 100, lower runs first). + /// + /// It is ok to enlist multiple action with the same key but only the first one will run. + /// The action boolean parameter indicates whether the scope completed or not. + /// + void Enlist(string key, Action action, int priority = 100); - /// - /// Enlists an object and action. - /// - /// The type of the object. - /// The object unique identifier. - /// A function providing the object. - /// The optional action. - /// The optional action priority (default is 100, lower runs first). - /// The object. - /// - /// On the first time an object is enlisted with a given key, the object is actually - /// created. Next calls just return the existing object. It is ok to enlist multiple objects - /// and action with the same key but only the first one is used, the others are ignored. - /// The action boolean parameter indicates whether the scope completed or not. - /// - T? Enlist(string key, Func creator, Action? action = null, int priority = 100); + /// + /// Enlists an object and action. + /// + /// The type of the object. + /// The object unique identifier. + /// A function providing the object. + /// The optional action. + /// The optional action priority (default is 100, lower runs first). + /// The object. + /// + /// + /// On the first time an object is enlisted with a given key, the object is actually + /// created. Next calls just return the existing object. It is ok to enlist multiple objects + /// and action with the same key but only the first one is used, the others are ignored. + /// + /// The action boolean parameter indicates whether the scope completed or not. + /// + T? Enlist(string key, Func creator, Action? action = null, int priority = 100); - /// - /// Gets an enlisted object. - /// - /// The type of the object. - /// The object unique identifier. - /// The enlisted object, if any, else the default value. - T? GetEnlisted(string key); + /// + /// Gets an enlisted object. + /// + /// The type of the object. + /// The object unique identifier. + /// The enlisted object, if any, else the default value. + T? GetEnlisted(string key); - void ScopeExit(bool completed); - } + void ScopeExit(bool completed); } diff --git a/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs index 78c50b628f..75361726f3 100644 --- a/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs +++ b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs @@ -1,36 +1,35 @@ -namespace Umbraco.Cms.Core.Scoping +namespace Umbraco.Cms.Core.Scoping; + +/// +/// Specifies the cache mode of repositories. +/// +public enum RepositoryCacheMode { /// - /// Specifies the cache mode of repositories. + /// Unspecified. /// - public enum RepositoryCacheMode - { - /// - /// Unspecified. - /// - Unspecified = 0, + Unspecified = 0, - /// - /// Default, full L2 cache. - /// - Default = 1, + /// + /// Default, full L2 cache. + /// + Default = 1, - /// - /// Scoped cache. - /// - /// - /// Reads from, and writes to, a scope-local cache. - /// Upon scope completion, clears the global L2 cache. - /// - Scoped = 2, + /// + /// Scoped cache. + /// + /// + /// Reads from, and writes to, a scope-local cache. + /// Upon scope completion, clears the global L2 cache. + /// + Scoped = 2, - /// - /// No cache. - /// - /// - /// Bypasses caches entirely. - /// Upon scope completion, clears the global L2 cache. - /// - None = 3 - } + /// + /// No cache. + /// + /// + /// Bypasses caches entirely. + /// Upon scope completion, clears the global L2 cache. + /// + None = 3, } diff --git a/src/Umbraco.Core/Sections/ContentSection.cs b/src/Umbraco.Core/Sections/ContentSection.cs index 828adea295..f8d46747b1 100644 --- a/src/Umbraco.Core/Sections/ContentSection.cs +++ b/src/Umbraco.Core/Sections/ContentSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office content section - /// - public class ContentSection : ISection - { - /// - public string Alias => Constants.Applications.Content; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Content"; - } +/// +/// Defines the back office content section +/// +public class ContentSection : ISection +{ + /// + public string Alias => Constants.Applications.Content; + + /// + public string Name => "Content"; } diff --git a/src/Umbraco.Core/Sections/FormsSection.cs b/src/Umbraco.Core/Sections/FormsSection.cs index e0fd1085ee..3ac36e4732 100644 --- a/src/Umbraco.Core/Sections/FormsSection.cs +++ b/src/Umbraco.Core/Sections/FormsSection.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office media section +/// +public class FormsSection : ISection { - /// - /// Defines the back office media section - /// - public class FormsSection : ISection - { - public string Alias => Constants.Applications.Forms; - public string Name => "Forms"; - } + public string Alias => Constants.Applications.Forms; + + public string Name => "Forms"; } diff --git a/src/Umbraco.Core/Sections/ISection.cs b/src/Umbraco.Core/Sections/ISection.cs index bbd380f57e..565955dfe9 100644 --- a/src/Umbraco.Core/Sections/ISection.cs +++ b/src/Umbraco.Core/Sections/ISection.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines a back office section. +/// +public interface ISection { /// - /// Defines a back office section. + /// Gets the alias of the section. /// - public interface ISection - { - /// - /// Gets the alias of the section. - /// - string Alias { get; } + string Alias { get; } - /// - /// Gets the name of the section. - /// - string Name { get; } - } + /// + /// Gets the name of the section. + /// + string Name { get; } } diff --git a/src/Umbraco.Core/Sections/MediaSection.cs b/src/Umbraco.Core/Sections/MediaSection.cs index 8732556a28..f5fd0a79b7 100644 --- a/src/Umbraco.Core/Sections/MediaSection.cs +++ b/src/Umbraco.Core/Sections/MediaSection.cs @@ -1,11 +1,11 @@ -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +/// +/// Defines the back office media section +/// +public class MediaSection : ISection { - /// - /// Defines the back office media section - /// - public class MediaSection : ISection - { - public string Alias => Constants.Applications.Media; - public string Name => "Media"; - } + public string Alias => Constants.Applications.Media; + + public string Name => "Media"; } diff --git a/src/Umbraco.Core/Sections/MembersSection.cs b/src/Umbraco.Core/Sections/MembersSection.cs index 1edbf12604..a2e98ac871 100644 --- a/src/Umbraco.Core/Sections/MembersSection.cs +++ b/src/Umbraco.Core/Sections/MembersSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office members section - /// - public class MembersSection : ISection - { - /// - public string Alias => Constants.Applications.Members; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Members"; - } +/// +/// Defines the back office members section +/// +public class MembersSection : ISection +{ + /// + public string Alias => Constants.Applications.Members; + + /// + public string Name => "Members"; } diff --git a/src/Umbraco.Core/Sections/PackagesSection.cs b/src/Umbraco.Core/Sections/PackagesSection.cs index 4852c11397..d65acfccec 100644 --- a/src/Umbraco.Core/Sections/PackagesSection.cs +++ b/src/Umbraco.Core/Sections/PackagesSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office packages section - /// - public class PackagesSection : ISection - { - /// - public string Alias => Constants.Applications.Packages; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Packages"; - } +/// +/// Defines the back office packages section +/// +public class PackagesSection : ISection +{ + /// + public string Alias => Constants.Applications.Packages; + + /// + public string Name => "Packages"; } diff --git a/src/Umbraco.Core/Sections/SectionCollection.cs b/src/Umbraco.Core/Sections/SectionCollection.cs index 5ff0157d14..83169a390d 100644 --- a/src/Umbraco.Core/Sections/SectionCollection.cs +++ b/src/Umbraco.Core/Sections/SectionCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +public class SectionCollection : BuilderCollectionBase { - public class SectionCollection : BuilderCollectionBase + public SectionCollection(Func> items) + : base(items) { - public SectionCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs b/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs index 219d634261..7644b1cc8c 100644 --- a/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs +++ b/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs @@ -1,24 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; -namespace Umbraco.Cms.Core.Sections +namespace Umbraco.Cms.Core.Sections; + +public class + SectionCollectionBuilder : OrderedCollectionBuilderBase { - public class SectionCollectionBuilder : OrderedCollectionBuilderBase + protected override SectionCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) { - protected override SectionCollectionBuilder This => this; + // get the manifest parser just-in-time - injecting it in the ctor would mean that + // simply getting the builder in order to configure the collection, would require + // its dependencies too, and that can create cycles or other oddities + IManifestParser manifestParser = factory.GetRequiredService(); - protected override IEnumerable CreateItems(IServiceProvider factory) - { - // get the manifest parser just-in-time - injecting it in the ctor would mean that - // simply getting the builder in order to configure the collection, would require - // its dependencies too, and that can create cycles or other oddities - var manifestParser = factory.GetRequiredService(); - - return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.Sections); - } + return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.Sections); } } diff --git a/src/Umbraco.Core/Sections/SettingsSection.cs b/src/Umbraco.Core/Sections/SettingsSection.cs index bc0a43cae1..3fe825c70d 100644 --- a/src/Umbraco.Core/Sections/SettingsSection.cs +++ b/src/Umbraco.Core/Sections/SettingsSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office settings section - /// - public class SettingsSection : ISection - { - /// - public string Alias => Constants.Applications.Settings; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Settings"; - } +/// +/// Defines the back office settings section +/// +public class SettingsSection : ISection +{ + /// + public string Alias => Constants.Applications.Settings; + + /// + public string Name => "Settings"; } diff --git a/src/Umbraco.Core/Sections/TranslationSection.cs b/src/Umbraco.Core/Sections/TranslationSection.cs index d739757e93..d11391c811 100644 --- a/src/Umbraco.Core/Sections/TranslationSection.cs +++ b/src/Umbraco.Core/Sections/TranslationSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office translation section - /// - public class TranslationSection : ISection - { - /// - public string Alias => Constants.Applications.Translation; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Translation"; - } +/// +/// Defines the back office translation section +/// +public class TranslationSection : ISection +{ + /// + public string Alias => Constants.Applications.Translation; + + /// + public string Name => "Translation"; } diff --git a/src/Umbraco.Core/Sections/UsersSection.cs b/src/Umbraco.Core/Sections/UsersSection.cs index 6969e9be3d..cea5047c81 100644 --- a/src/Umbraco.Core/Sections/UsersSection.cs +++ b/src/Umbraco.Core/Sections/UsersSection.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sections -{ - /// - /// Defines the back office users section - /// - public class UsersSection : ISection - { - /// - public string Alias => Constants.Applications.Users; +namespace Umbraco.Cms.Core.Sections; - /// - public string Name => "Users"; - } +/// +/// Defines the back office users section +/// +public class UsersSection : ISection +{ + /// + public string Alias => Constants.Applications.Users; + + /// + public string Name => "Users"; } diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 187d44b05d..2b8294e8db 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -4,32 +4,31 @@ using System.Globalization; using System.Security.Claims; using System.Security.Principal; -using System.Threading; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class AuthenticationExtensions { - public static class AuthenticationExtensions + /// + /// Ensures that the thread culture is set based on the back office user's culture + /// + public static void EnsureCulture(this IIdentity identity) { - /// - /// Ensures that the thread culture is set based on the back office user's culture - /// - public static void EnsureCulture(this IIdentity identity) + CultureInfo? culture = GetCulture(identity); + if (!(culture is null)) { - var culture = GetCulture(identity); - if (!(culture is null)) - { - Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture; - } - } - - public static CultureInfo? GetCulture(this IIdentity identity) - { - if (identity is ClaimsIdentity umbIdentity && umbIdentity.VerifyBackOfficeIdentity(out _) && umbIdentity.IsAuthenticated && umbIdentity.GetCultureString() is not null) - { - return CultureInfo.GetCultureInfo(umbIdentity.GetCultureString()!); - } - - return null; + Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture; } } + + public static CultureInfo? GetCulture(this IIdentity identity) + { + if (identity is ClaimsIdentity umbIdentity && umbIdentity.VerifyBackOfficeIdentity(out _) && + umbIdentity.IsAuthenticated && umbIdentity.GetCultureString() is not null) + { + return CultureInfo.GetCultureInfo(umbIdentity.GetCultureString()!); + } + + return null; + } } diff --git a/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs b/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs index c79fa87429..cece444588 100644 --- a/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs +++ b/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs @@ -1,22 +1,19 @@ -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +public class BackOfficeExternalLoginProviderErrors { - public class BackOfficeExternalLoginProviderErrors + // required for deserialization + public BackOfficeExternalLoginProviderErrors() { - // required for deserialization - public BackOfficeExternalLoginProviderErrors() - { - } - - public BackOfficeExternalLoginProviderErrors(string? authenticationType, IEnumerable errors) - { - AuthenticationType = authenticationType; - Errors = errors ?? Enumerable.Empty(); - } - - public string? AuthenticationType { get; set; } - public IEnumerable? Errors { get; set; } } + + public BackOfficeExternalLoginProviderErrors(string? authenticationType, IEnumerable errors) + { + AuthenticationType = authenticationType; + Errors = errors ?? Enumerable.Empty(); + } + + public string? AuthenticationType { get; set; } + + public IEnumerable? Errors { get; set; } } diff --git a/src/Umbraco.Core/Security/BackOfficeIdentityOptions.cs b/src/Umbraco.Core/Security/BackOfficeIdentityOptions.cs index e4eacaf9d6..913f4c6dde 100644 --- a/src/Umbraco.Core/Security/BackOfficeIdentityOptions.cs +++ b/src/Umbraco.Core/Security/BackOfficeIdentityOptions.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Identity; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Identity options specifically for the back office identity implementation +/// +public class BackOfficeIdentityOptions : IdentityOptions { - /// - /// Identity options specifically for the back office identity implementation - /// - public class BackOfficeIdentityOptions : IdentityOptions - { - } } diff --git a/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs index a59c1fb435..5466642a14 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// The result returned from the IBackOfficeUserPasswordChecker +/// +public enum BackOfficeUserPasswordCheckerResult { - /// - /// The result returned from the IBackOfficeUserPasswordChecker - /// - public enum BackOfficeUserPasswordCheckerResult - { - ValidCredentials, - InvalidCredentials, - FallbackToDefaultChecker - } + ValidCredentials, + InvalidCredentials, + FallbackToDefaultChecker, } diff --git a/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs b/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs index b9912a3911..4cb9e20dac 100644 --- a/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs +++ b/src/Umbraco.Core/Security/ClaimsPrincipalExtensions.cs @@ -1,91 +1,99 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Globalization; -using System.Linq; using System.Security.Claims; using System.Security.Principal; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ClaimsPrincipalExtensions { - public static class ClaimsPrincipalExtensions + public static bool IsBackOfficeAuthenticationType(this ClaimsIdentity? claimsIdentity) { - - public static bool IsBackOfficeAuthenticationType(this ClaimsIdentity claimsIdentity) + if (claimsIdentity is null) { - if (claimsIdentity is null) - { - return false; - } - - return claimsIdentity.IsAuthenticated && claimsIdentity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType; + return false; } - /// - /// This will return the current back office identity if the IPrincipal is the correct type and authenticated. - /// - /// - /// - public static ClaimsIdentity? GetUmbracoIdentity(this IPrincipal principal) - { - //If it's already a UmbracoBackOfficeIdentity - if (principal.Identity is ClaimsIdentity claimsIdentity - && claimsIdentity.IsBackOfficeAuthenticationType() - && claimsIdentity.VerifyBackOfficeIdentity(out var backOfficeIdentity)) - { - return backOfficeIdentity; - } - //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that - // We can have assigned more identities if it is a preview request. - if (principal is ClaimsPrincipal claimsPrincipal ) + return claimsIdentity.IsAuthenticated && + claimsIdentity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType; + } + + /// + /// This will return the current back office identity if the IPrincipal is the correct type and authenticated. + /// + /// + /// + public static ClaimsIdentity? GetUmbracoIdentity(this IPrincipal principal) + { + // If it's already a UmbracoBackOfficeIdentity + if (principal.Identity is ClaimsIdentity claimsIdentity + && claimsIdentity.IsBackOfficeAuthenticationType() + && claimsIdentity.VerifyBackOfficeIdentity(out ClaimsIdentity? backOfficeIdentity)) + { + return backOfficeIdentity; + } + + // Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that + // We can have assigned more identities if it is a preview request. + if (principal is ClaimsPrincipal claimsPrincipal) + { + ClaimsIdentity? identity = + claimsPrincipal.Identities.FirstOrDefault(x => x.IsBackOfficeAuthenticationType()); + if (identity is not null) { - var identity = claimsPrincipal.Identities.FirstOrDefault(x => x.IsBackOfficeAuthenticationType()); - if (identity is not null) + claimsIdentity = identity; + if (claimsIdentity.VerifyBackOfficeIdentity(out backOfficeIdentity)) { - claimsIdentity = identity; - if (claimsIdentity.VerifyBackOfficeIdentity(out backOfficeIdentity)) - { - return backOfficeIdentity; - } + return backOfficeIdentity; } } - - //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd - if (principal.Identity is ClaimsIdentity claimsIdentity2 - && claimsIdentity2.VerifyBackOfficeIdentity(out backOfficeIdentity)) - { - return backOfficeIdentity; - } - return null; } - /// - /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig authentication - /// - /// - /// - public static double GetRemainingAuthSeconds(this IPrincipal user) => user.GetRemainingAuthSeconds(DateTimeOffset.UtcNow); - - /// - /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig authentication - /// - /// - /// - /// - public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now) + // Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd + if (principal.Identity is ClaimsIdentity claimsIdentity2 + && claimsIdentity2.VerifyBackOfficeIdentity(out backOfficeIdentity)) { - var claimsPrincipal = user as ClaimsPrincipal; - if (claimsPrincipal == null) return 0; - - var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value; - if (ticketExpires.IsNullOrWhiteSpace()) return 0; - - var utcExpired = DateTimeOffset.Parse(ticketExpires!, null, DateTimeStyles.RoundtripKind); - - var secondsRemaining = utcExpired.Subtract(now).TotalSeconds; - return secondsRemaining; + return backOfficeIdentity; } + + return null; + } + + /// + /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig + /// authentication + /// + /// + /// + public static double GetRemainingAuthSeconds(this IPrincipal user) => + user.GetRemainingAuthSeconds(DateTimeOffset.UtcNow); + + /// + /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig + /// authentication + /// + /// + /// + /// + public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now) + { + if (user is not ClaimsPrincipal claimsPrincipal) + { + return 0; + } + + var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value; + if (ticketExpires.IsNullOrWhiteSpace()) + { + return 0; + } + + var utcExpired = DateTimeOffset.Parse(ticketExpires!, null, DateTimeStyles.RoundtripKind); + + var secondsRemaining = utcExpired.Subtract(now).TotalSeconds; + return secondsRemaining; } } diff --git a/src/Umbraco.Core/Security/ContentPermissions.cs b/src/Umbraco.Core/Security/ContentPermissions.cs index 73f9f4ccef..db27d100c6 100644 --- a/src/Umbraco.Core/Security/ContentPermissions.cs +++ b/src/Umbraco.Core/Security/ContentPermissions.cs @@ -1,290 +1,356 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Checks user access to content +/// +public class ContentPermissions { + private readonly AppCaches _appCaches; - /// - /// Checks user access to content - /// - public class ContentPermissions + public enum ContentAccess { - private readonly IUserService _userService; - private readonly IContentService _contentService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; + Granted, + Denied, + NotFound, + } - public enum ContentAccess + private readonly IContentService _contentService; + private readonly IEntityService _entityService; + private readonly IUserService _userService; + + public ContentPermissions( + IUserService userService, + IContentService contentService, + IEntityService entityService, + AppCaches appCaches) + { + _userService = userService; + _contentService = contentService; + _entityService = entityService; + _appCaches = appCaches; + } + + public static bool HasPathAccess(string? path, int[]? startNodeIds, int recycleBinId) + { + if (string.IsNullOrWhiteSpace(path)) { - Granted, - Denied, - NotFound + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); } - public ContentPermissions( - IUserService userService, - IContentService contentService, - IEntityService entityService, - AppCaches appCaches) + // check for no access + if (startNodeIds is null || startNodeIds.Length == 0) { - _userService = userService; - _contentService = contentService; - _entityService = entityService; - _appCaches = appCaches; - } - - public ContentAccess CheckPermissions( - IContent content, - IUser user, - char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); - - public ContentAccess CheckPermissions( - IContent? content, - IUser? user, - IReadOnlyList permissionsToCheck) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (content == null) return ContentAccess.NotFound; - - var hasPathAccess = user.HasPathAccess(content, _entityService, _appCaches); - - if (hasPathAccess == false) - return ContentAccess.Denied; - - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; - - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(content.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; - } - - public ContentAccess CheckPermissions( - IUmbracoEntity entity, - IUser? user, - char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); - - public ContentAccess CheckPermissions( - IUmbracoEntity entity, - IUser? user, - IReadOnlyList permissionsToCheck) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (entity == null) return ContentAccess.NotFound; - - var hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); - - if (hasPathAccess == false) - return ContentAccess.Denied; - - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; - - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(entity.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; - } - - /// - /// Checks if the user has access to the specified node and permissions set - /// - /// - /// - /// - /// - /// The item resolved if one was found for the id - /// - /// - public ContentAccess CheckPermissions( - int nodeId, - IUser user, - out IUmbracoEntity? entity, - IReadOnlyList? permissionsToCheck = null) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } - - bool? hasPathAccess = null; - entity = null; - - if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); - else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); - - if (hasPathAccess.HasValue) - return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; - - entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document); - if (entity == null) return ContentAccess.NotFound; - hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); - - if (hasPathAccess == false) - return ContentAccess.Denied; - - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; - - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(entity.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; - } - - /// - /// Checks if the user has access to the specified node and permissions set - /// - /// - /// - /// - /// - /// - /// The item resolved if one was found for the id - /// - /// - public ContentAccess CheckPermissions( - int nodeId, - IUser? user, - out IContent? contentItem, - IReadOnlyList? permissionsToCheck = null) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } - - bool? hasPathAccess = null; - contentItem = null; - - if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); - else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); - - if (hasPathAccess.HasValue) - return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; - - contentItem = _contentService.GetById(nodeId); - if (contentItem == null) return ContentAccess.NotFound; - hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches); - - if (hasPathAccess == false) - return ContentAccess.Denied; - - if (permissionsToCheck == null || permissionsToCheck.Count == 0) - return ContentAccess.Granted; - - //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck) - ? ContentAccess.Granted - : ContentAccess.Denied; - } - - private bool CheckPermissionsPath(string? path, IUser user, IReadOnlyList? permissionsToCheck = null) - { - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } - - //get the implicit/inherited permissions for the user for this path, - //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) - var permission = _userService.GetPermissionsForPath(user, path); - - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null - || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } - - public static bool HasPathAccess(string? path, int[]? startNodeIds, int recycleBinId) - { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - - // check for no access - if (startNodeIds is null || startNodeIds.Length == 0) - return false; - - // check for root access - if (startNodeIds.Contains(Constants.System.Root)) - return true; - - var formattedPath = string.Concat(",", path, ","); - - // only users with root access have access to the recycle bin, - // if the above check didn't pass then access is denied - if (formattedPath.Contains(string.Concat(",", recycleBinId.ToString(CultureInfo.InvariantCulture), ","))) - return false; - - // check for a start node in the path - return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x.ToString(CultureInfo.InvariantCulture), ","))); - } - - public static bool IsInBranchOfStartNode(string path, int[]? startNodeIds, string[]? startNodePaths, out bool hasPathAccess) - { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - - hasPathAccess = false; - - // check for no access - if (startNodeIds?.Length == 0) - return false; - - // check for root access - if (startNodeIds?.Contains(Constants.System.Root) ?? false) - { - hasPathAccess = true; - return true; - } - - //is it self? - var self = startNodePaths?.Any(x => x == path) ?? false; - if (self) - { - hasPathAccess = true; - return true; - } - - //is it ancestor? - var ancestor = startNodePaths?.Any(x => x.StartsWith(path)) ?? false; - if (ancestor) - { - //hasPathAccess = false; - return true; - } - - //is it descendant? - var descendant = startNodePaths?.Any(x => path.StartsWith(x)) ?? false; - if (descendant) - { - hasPathAccess = true; - return true; - } - return false; } + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + { + return true; + } + + var formattedPath = string.Concat(",", path, ","); + + // only users with root access have access to the recycle bin, + // if the above check didn't pass then access is denied + if (formattedPath.Contains(string.Concat(",", recycleBinId.ToString(CultureInfo.InvariantCulture), ","))) + { + return false; + } + + // check for a start node in the path + return startNodeIds.Any(x => + formattedPath.Contains(string.Concat(",", x.ToString(CultureInfo.InvariantCulture), ","))); + } + + public ContentAccess CheckPermissions( + IContent content, + IUser user, + char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); + + public ContentAccess CheckPermissions( + IContent? content, + IUser? user, + IReadOnlyList permissionsToCheck) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (content == null) + { + return ContentAccess.NotFound; + } + + var hasPathAccess = user.HasPathAccess(content, _entityService, _appCaches); + + if (hasPathAccess == false) + { + return ContentAccess.Denied; + } + + if (permissionsToCheck == null || permissionsToCheck.Count == 0) + { + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(content.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + public ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser? user, + char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); + + public ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser? user, + IReadOnlyList permissionsToCheck) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (entity == null) + { + return ContentAccess.NotFound; + } + + var hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); + + if (hasPathAccess == false) + { + return ContentAccess.Denied; + } + + if (permissionsToCheck == null || permissionsToCheck.Count == 0) + { + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + /// + /// Checks if the user has access to the specified node and permissions set + /// + /// + /// + /// + /// + /// The item resolved if one was found for the id + /// + /// + public ContentAccess CheckPermissions( + int nodeId, + IUser user, + out IUmbracoEntity? entity, + IReadOnlyList? permissionsToCheck = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } + + bool? hasPathAccess = null; + entity = null; + + if (nodeId == Constants.System.Root) + { + hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); + } + else if (nodeId == Constants.System.RecycleBinContent) + { + hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); + } + + if (hasPathAccess.HasValue) + { + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + } + + entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document); + if (entity == null) + { + return ContentAccess.NotFound; + } + + hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); + + if (hasPathAccess == false) + { + return ContentAccess.Denied; + } + + if (permissionsToCheck == null || permissionsToCheck.Count == 0) + { + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(entity.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + /// + /// Checks if the user has access to the specified node and permissions set + /// + /// + /// + /// + /// + /// + /// The item resolved if one was found for the id + /// + /// + public ContentAccess CheckPermissions( + int nodeId, + IUser? user, + out IContent? contentItem, + IReadOnlyList? permissionsToCheck = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } + + bool? hasPathAccess = null; + contentItem = null; + + if (nodeId == Constants.System.Root) + { + hasPathAccess = user.HasContentRootAccess(_entityService, _appCaches); + } + else if (nodeId == Constants.System.RecycleBinContent) + { + hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); + } + + if (hasPathAccess.HasValue) + { + return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; + } + + contentItem = _contentService.GetById(nodeId); + if (contentItem == null) + { + return ContentAccess.NotFound; + } + + hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches); + + if (hasPathAccess == false) + { + return ContentAccess.Denied; + } + + if (permissionsToCheck == null || permissionsToCheck.Count == 0) + { + return ContentAccess.Granted; + } + + // get the implicit/inherited permissions for the user for this path + return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck) + ? ContentAccess.Granted + : ContentAccess.Denied; + } + + private bool CheckPermissionsPath(string? path, IUser user, IReadOnlyList? permissionsToCheck = null) + { + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } + + // get the implicit/inherited permissions for the user for this path, + // if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + EntityPermissionSet permission = _userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + + return allowed; + } + + public static bool IsInBranchOfStartNode(string path, int[]? startNodeIds, string[]? startNodePaths, out bool hasPathAccess) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + } + + hasPathAccess = false; + + // check for no access + if (startNodeIds?.Length == 0) + { + return false; + } + + // check for root access + if (startNodeIds?.Contains(Constants.System.Root) ?? false) + { + hasPathAccess = true; + return true; + } + + // is it self? + var self = startNodePaths?.Any(x => x == path) ?? false; + if (self) + { + hasPathAccess = true; + return true; + } + + // is it ancestor? + var ancestor = startNodePaths?.Any(x => x.StartsWith(path)) ?? false; + if (ancestor) + { + // hasPathAccess = false; + return true; + } + + // is it descendant? + var descendant = startNodePaths?.Any(x => path.StartsWith(x)) ?? false; + if (descendant) + { + hasPathAccess = true; + return true; + } + + return false; } } diff --git a/src/Umbraco.Core/Security/ExternalLogin.cs b/src/Umbraco.Core/Security/ExternalLogin.cs index 631fe52b28..6eb3defc45 100644 --- a/src/Umbraco.Core/Security/ExternalLogin.cs +++ b/src/Umbraco.Core/Security/ExternalLogin.cs @@ -1,28 +1,24 @@ -using System; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +/// +public class ExternalLogin : IExternalLogin { + /// + /// Initializes a new instance of the class. + /// + public ExternalLogin(string loginProvider, string providerKey, string? userData = null) + { + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + ProviderKey = providerKey ?? throw new ArgumentNullException(nameof(providerKey)); + UserData = userData; + } /// - public class ExternalLogin : IExternalLogin - { - /// - /// Initializes a new instance of the class. - /// - public ExternalLogin(string loginProvider, string providerKey, string? userData = null) - { - LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); - ProviderKey = providerKey ?? throw new ArgumentNullException(nameof(providerKey)); - UserData = userData; - } + public string LoginProvider { get; } - /// - public string LoginProvider { get; } + /// + public string ProviderKey { get; } - /// - public string ProviderKey { get; } - - /// - public string? UserData { get; } - } + /// + public string? UserData { get; } } diff --git a/src/Umbraco.Core/Security/ExternalLoginToken.cs b/src/Umbraco.Core/Security/ExternalLoginToken.cs index 85089ddba6..df986d176f 100644 --- a/src/Umbraco.Core/Security/ExternalLoginToken.cs +++ b/src/Umbraco.Core/Security/ExternalLoginToken.cs @@ -1,27 +1,24 @@ -using System; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +/// +public class ExternalLoginToken : IExternalLoginToken { - /// - public class ExternalLoginToken : IExternalLoginToken + /// + /// Initializes a new instance of the class. + /// + public ExternalLoginToken(string loginProvider, string name, string value) { - /// - /// Initializes a new instance of the class. - /// - public ExternalLoginToken(string loginProvider, string name, string value) - { - LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); - Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); - } - - /// - public string LoginProvider { get; } - - /// - public string Name { get; } - - /// - public string Value { get; } + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + Value = value ?? throw new ArgumentNullException(nameof(value)); } + + /// + public string LoginProvider { get; } + + /// + public string Name { get; } + + /// + public string Value { get; } } diff --git a/src/Umbraco.Core/Security/IBackofficeSecurity.cs b/src/Umbraco.Core/Security/IBackofficeSecurity.cs index 12b29a0288..2de9104a95 100644 --- a/src/Umbraco.Core/Security/IBackofficeSecurity.cs +++ b/src/Umbraco.Core/Security/IBackofficeSecurity.cs @@ -1,44 +1,43 @@ using Umbraco.Cms.Core.Models.Membership; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public interface IBackOfficeSecurity { - public interface IBackOfficeSecurity - { - /// - /// Gets the current user. - /// - /// The current user that has been authenticated for the request. - /// If authentication hasn't taken place this will be null. - // TODO: This is used a lot but most of it can be refactored to not use this at all since the IUser instance isn't - // needed in most cases. Where an IUser is required this could be an ext method on the ClaimsIdentity/ClaimsPrincipal that passes in - // an IUserService, like HttpContext.User.GetUmbracoUser(_userService); - // This one isn't as easy to remove as the others below. - IUser? CurrentUser { get; } + /// + /// Gets the current user. + /// + /// The current user that has been authenticated for the request. + /// If authentication hasn't taken place this will be null. + // TODO: This is used a lot but most of it can be refactored to not use this at all since the IUser instance isn't + // needed in most cases. Where an IUser is required this could be an ext method on the ClaimsIdentity/ClaimsPrincipal that passes in + // an IUserService, like HttpContext.User.GetUmbracoUser(_userService); + // This one isn't as easy to remove as the others below. + IUser? CurrentUser { get; } - /// - /// Gets the current user's id. - /// - /// The current user's Id that has been authenticated for the request. - /// If authentication hasn't taken place this will be unsuccessful. - // TODO: This should just be an extension method on ClaimsIdentity - Attempt GetUserId(); + /// + /// Gets the current user's id. + /// + /// The current user's Id that has been authenticated for the request. + /// If authentication hasn't taken place this will be unsuccessful. + // TODO: This should just be an extension method on ClaimsIdentity + Attempt GetUserId(); - /// - /// Checks if the specified user as access to the app - /// - /// - /// - /// - /// If authentication hasn't taken place this will be unsuccessful. - // TODO: Should be part of IBackOfficeUserManager - bool UserHasSectionAccess(string section, IUser user); + /// + /// Checks if the specified user as access to the app + /// + /// + /// + /// + /// If authentication hasn't taken place this will be unsuccessful. + // TODO: Should be part of IBackOfficeUserManager + bool UserHasSectionAccess(string section, IUser user); - /// - /// Ensures that a back office user is logged in - /// - /// - /// This does not force authentication, that must be done before calls to this are made. - // TODO: Should be removed, this should not be necessary - bool IsAuthenticated(); - } + /// + /// Ensures that a back office user is logged in + /// + /// + /// This does not force authentication, that must be done before calls to this are made. + // TODO: Should be removed, this should not be necessary + bool IsAuthenticated(); } diff --git a/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs b/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs index 7ef33ecdc6..11ed86971e 100644 --- a/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs +++ b/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public interface IBackOfficeSecurityAccessor { - public interface IBackOfficeSecurityAccessor - { - IBackOfficeSecurity? BackOfficeSecurity { get; } - } + IBackOfficeSecurity? BackOfficeSecurity { get; } } diff --git a/src/Umbraco.Core/Security/IExternalLogin.cs b/src/Umbraco.Core/Security/IExternalLogin.cs index 0c09cecfc0..225b0390d3 100644 --- a/src/Umbraco.Core/Security/IExternalLogin.cs +++ b/src/Umbraco.Core/Security/IExternalLogin.cs @@ -1,23 +1,22 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Used to persist external login data for a user +/// +public interface IExternalLogin { /// - /// Used to persist external login data for a user + /// Gets the login provider /// - public interface IExternalLogin - { - /// - /// Gets the login provider - /// - string LoginProvider { get; } + string LoginProvider { get; } - /// - /// Gets the provider key - /// - string ProviderKey { get; } + /// + /// Gets the provider key + /// + string ProviderKey { get; } - /// - /// Gets the user data - /// - string? UserData { get; } - } + /// + /// Gets the user data + /// + string? UserData { get; } } diff --git a/src/Umbraco.Core/Security/IExternalLoginToken.cs b/src/Umbraco.Core/Security/IExternalLoginToken.cs index b3fd4b64b2..a5dba5a17e 100644 --- a/src/Umbraco.Core/Security/IExternalLoginToken.cs +++ b/src/Umbraco.Core/Security/IExternalLoginToken.cs @@ -1,23 +1,22 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Used to persist an external login token for a user +/// +public interface IExternalLoginToken { /// - /// Used to persist an external login token for a user + /// Gets the login provider /// - public interface IExternalLoginToken - { - /// - /// Gets the login provider - /// - string LoginProvider { get; } + string LoginProvider { get; } - /// - /// Gets the name of the token - /// - string Name { get; } + /// + /// Gets the name of the token + /// + string Name { get; } - /// - /// Gets the value of the token - /// - string Value { get; } - } + /// + /// Gets the value of the token + /// + string Value { get; } } diff --git a/src/Umbraco.Core/Security/IHtmlSanitizer.cs b/src/Umbraco.Core/Security/IHtmlSanitizer.cs index 9bcfe405dd..3faf3cfd4d 100644 --- a/src/Umbraco.Core/Security/IHtmlSanitizer.cs +++ b/src/Umbraco.Core/Security/IHtmlSanitizer.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public interface IHtmlSanitizer { - public interface IHtmlSanitizer - { - /// - /// Sanitizes HTML - /// - /// HTML to be sanitized - /// Sanitized HTML - string Sanitize(string html); - } + /// + /// Sanitizes HTML + /// + /// HTML to be sanitized + /// Sanitized HTML + string Sanitize(string html); } diff --git a/src/Umbraco.Core/Security/IIdentityUserLogin.cs b/src/Umbraco.Core/Security/IIdentityUserLogin.cs index c9eb64ceb3..51035b724c 100644 --- a/src/Umbraco.Core/Security/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IIdentityUserLogin.cs @@ -1,31 +1,30 @@ using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// An external login provider linked to a user +/// +/// The PK type for the user +public interface IIdentityUserLogin : IEntity, IRememberBeingDirty { /// - /// An external login provider linked to a user + /// Gets or sets the login provider for the login (i.e. Facebook, Google) /// - /// The PK type for the user - public interface IIdentityUserLogin : IEntity, IRememberBeingDirty - { - /// - /// Gets or sets the login provider for the login (i.e. Facebook, Google) - /// - string LoginProvider { get; set; } + string LoginProvider { get; set; } - /// - /// Gets or sets key representing the login for the provider - /// - string ProviderKey { get; set; } + /// + /// Gets or sets key representing the login for the provider + /// + string ProviderKey { get; set; } - /// - /// Gets or sets user or member key (Guid) for the user/member who owns this login - /// - string UserId { get; set; } // TODO: This should be able to be used by both users and members + /// + /// Gets or sets user or member key (Guid) for the user/member who owns this login + /// + string UserId { get; set; } // TODO: This should be able to be used by both users and members - /// - /// Gets or sets any arbitrary data for the user and external provider - /// - string? UserData { get; set; } - } + /// + /// Gets or sets any arbitrary data for the user and external provider + /// + string? UserData { get; set; } } diff --git a/src/Umbraco.Core/Security/IIdentityUserToken.cs b/src/Umbraco.Core/Security/IIdentityUserToken.cs index 0e7f22d72f..f2e17a19af 100644 --- a/src/Umbraco.Core/Security/IIdentityUserToken.cs +++ b/src/Umbraco.Core/Security/IIdentityUserToken.cs @@ -1,30 +1,29 @@ using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// An external login provider token +/// +public interface IIdentityUserToken : IEntity { /// - /// An external login provider token + /// Gets or sets user Id for the user who owns this token /// - public interface IIdentityUserToken : IEntity - { - /// - /// Gets or sets user Id for the user who owns this token - /// - string? UserId { get; set; } + string? UserId { get; set; } - /// - /// Gets or sets the login provider for the login (i.e. Facebook, Google) - /// - string LoginProvider { get; set; } + /// + /// Gets or sets the login provider for the login (i.e. Facebook, Google) + /// + string LoginProvider { get; set; } - /// - /// Gets or sets the token name - /// - string Name { get; set; } + /// + /// Gets or sets the token name + /// + string Name { get; set; } - /// - /// Gets or set the token value - /// - string Value { get; set; } - } + /// + /// Gets or set the token value + /// + string Value { get; set; } } diff --git a/src/Umbraco.Core/Security/IPasswordHasher.cs b/src/Umbraco.Core/Security/IPasswordHasher.cs index c0d436048e..5f3345ea73 100644 --- a/src/Umbraco.Core/Security/IPasswordHasher.cs +++ b/src/Umbraco.Core/Security/IPasswordHasher.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public interface IPasswordHasher { - public interface IPasswordHasher - { - /// - /// Hashes a password - /// - /// The password. - /// The password hashed. - string HashPassword(string password); - } + /// + /// Hashes a password + /// + /// The password. + /// The password hashed. + string HashPassword(string password); } diff --git a/src/Umbraco.Core/Security/IPublicAccessChecker.cs b/src/Umbraco.Core/Security/IPublicAccessChecker.cs index 6ec9eb7ade..d830d757f1 100644 --- a/src/Umbraco.Core/Security/IPublicAccessChecker.cs +++ b/src/Umbraco.Core/Security/IPublicAccessChecker.cs @@ -1,9 +1,6 @@ -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +public interface IPublicAccessChecker { - public interface IPublicAccessChecker - { - Task HasMemberAccessToContentAsync(int publishedContentId); - } + Task HasMemberAccessToContentAsync(int publishedContentId); } diff --git a/src/Umbraco.Core/Security/ITwoFactorProvider.cs b/src/Umbraco.Core/Security/ITwoFactorProvider.cs index f0da6c314a..8d2b12b6f8 100644 --- a/src/Umbraco.Core/Security/ITwoFactorProvider.cs +++ b/src/Umbraco.Core/Security/ITwoFactorProvider.cs @@ -1,22 +1,15 @@ -using System; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +public interface ITwoFactorProvider { - public interface ITwoFactorProvider - { - string ProviderName { get; } + string ProviderName { get; } - Task GetSetupDataAsync(Guid userOrMemberKey, string secret); - - bool ValidateTwoFactorPIN(string secret, string token); - - /// - /// - /// - /// Called to confirm the setup of two factor on the user. - bool ValidateTwoFactorSetup(string secret, string token); - } + Task GetSetupDataAsync(Guid userOrMemberKey, string secret); + bool ValidateTwoFactorPIN(string secret, string token); + /// + /// + /// Called to confirm the setup of two factor on the user. + bool ValidateTwoFactorSetup(string secret, string token); } diff --git a/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs index 225d46b268..83c11916b1 100644 --- a/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Core/Security/IdentityAuditEventArgs.cs @@ -1,88 +1,77 @@ -using System; +namespace Umbraco.Cms.Core.Security; -namespace Umbraco.Cms.Core.Security +public enum AuditEvent { + AccountLocked, + AccountUnlocked, + ForgotPasswordRequested, + ForgotPasswordChangedSuccess, + LoginFailed, + LoginRequiresVerification, + LoginSucces, + LogoutSuccess, + PasswordChanged, + PasswordReset, + ResetAccessFailedCount, + SendingUserInvite, +} + +/// +/// This class is used by events raised from the BackofficeUserManager +/// +public class IdentityAuditEventArgs : EventArgs +{ + /// + /// Default constructor + /// + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUser, string affectedUsername) + { + DateTimeUtc = DateTime.UtcNow; + Action = action; + IpAddress = ipAddress; + Comment = comment; + PerformingUser = performingUser; + AffectedUsername = affectedUsername; + AffectedUser = affectedUser; + } + + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUsername) + : this(action, ipAddress, performingUser, comment, Constants.Security.SuperUserIdAsString, affectedUsername) + { + } /// - /// This class is used by events raised from the BackofficeUserManager + /// The action that got triggered from the audit event /// - public class IdentityAuditEventArgs : EventArgs - { - /// - /// The action that got triggered from the audit event - /// - public AuditEvent Action { get; private set; } + public AuditEvent Action { get; } - /// - /// Current date/time in UTC format - /// - public DateTime DateTimeUtc { get; private set; } + /// + /// Current date/time in UTC format + /// + public DateTime DateTimeUtc { get; } - /// - /// The source IP address of the user performing the action - /// - public string IpAddress { get; private set; } + /// + /// The source IP address of the user performing the action + /// + public string IpAddress { get; } - /// - /// The user affected by the event raised - /// - public string AffectedUser { get; private set; } + /// + /// The user affected by the event raised + /// + public string AffectedUser { get; } - /// - /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 - /// - public string PerformingUser { get; private set; } + /// + /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 + /// + public string PerformingUser { get; } - /// - /// An optional comment about the action being logged - /// - public string Comment { get; private set; } + /// + /// An optional comment about the action being logged + /// + public string Comment { get; } - /// - /// This property is always empty except in the LoginFailed event for an unknown user trying to login - /// - public string AffectedUsername { get; private set; } - - - /// - /// Default constructor - /// - /// - /// - /// - /// - /// - public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUser, string affectedUsername) - { - DateTimeUtc = DateTime.UtcNow; - Action = action; - IpAddress = ipAddress; - Comment = comment; - PerformingUser = performingUser; - AffectedUsername = affectedUsername; - AffectedUser = affectedUser; - } - - public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string performingUser, string comment, string affectedUsername) - : this(action, ipAddress, performingUser, comment, Constants.Security.SuperUserIdAsString, affectedUsername) - { - } - - } - - public enum AuditEvent - { - AccountLocked, - AccountUnlocked, - ForgotPasswordRequested, - ForgotPasswordChangedSuccess, - LoginFailed, - LoginRequiresVerification, - LoginSucces, - LogoutSuccess, - PasswordChanged, - PasswordReset, - ResetAccessFailedCount, - SendingUserInvite - } + /// + /// This property is always empty except in the LoginFailed event for an unknown user trying to login + /// + public string AffectedUsername { get; } } diff --git a/src/Umbraco.Core/Security/IdentityUserLogin.cs b/src/Umbraco.Core/Security/IdentityUserLogin.cs index 402660ead9..ca821811cc 100644 --- a/src/Umbraco.Core/Security/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IdentityUserLogin.cs @@ -1,46 +1,43 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Entity type for a user's login (i.e. Facebook, Google) +/// +public class IdentityUserLogin : EntityBase, IIdentityUserLogin { + /// + /// Initializes a new instance of the class. + /// + public IdentityUserLogin(string loginProvider, string providerKey, string userId) + { + LoginProvider = loginProvider; + ProviderKey = providerKey; + UserId = userId; + } /// - /// Entity type for a user's login (i.e. Facebook, Google) + /// Initializes a new instance of the class. /// - public class IdentityUserLogin : EntityBase, IIdentityUserLogin + public IdentityUserLogin(int id, string loginProvider, string providerKey, string userId, DateTime createDate) { - /// - /// Initializes a new instance of the class. - /// - public IdentityUserLogin(string loginProvider, string providerKey, string userId) - { - LoginProvider = loginProvider; - ProviderKey = providerKey; - UserId = userId; - } - - /// - /// Initializes a new instance of the class. - /// - public IdentityUserLogin(int id, string loginProvider, string providerKey, string userId, DateTime createDate) - { - Id = id; - LoginProvider = loginProvider; - ProviderKey = providerKey; - UserId = userId; - CreateDate = createDate; - } - - /// - public string LoginProvider { get; set; } - - /// - public string ProviderKey { get; set; } - - /// - public string UserId { get; set; } - - /// - public string? UserData { get; set; } + Id = id; + LoginProvider = loginProvider; + ProviderKey = providerKey; + UserId = userId; + CreateDate = createDate; } + + /// + public string LoginProvider { get; set; } + + /// + public string ProviderKey { get; set; } + + /// + public string UserId { get; set; } + + /// + public string? UserData { get; set; } } diff --git a/src/Umbraco.Core/Security/IdentityUserToken.cs b/src/Umbraco.Core/Security/IdentityUserToken.cs index 014001a3a9..f4fcd46ace 100644 --- a/src/Umbraco.Core/Security/IdentityUserToken.cs +++ b/src/Umbraco.Core/Security/IdentityUserToken.cs @@ -1,44 +1,42 @@ -using System; using Umbraco.Cms.Core.Models.Entities; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public class IdentityUserToken : EntityBase, IIdentityUserToken { - public class IdentityUserToken : EntityBase, IIdentityUserToken + /// + /// Initializes a new instance of the class. + /// + public IdentityUserToken(string loginProvider, string? name, string? value, string? userId) { - /// - /// Initializes a new instance of the class. - /// - public IdentityUserToken(string loginProvider, string? name, string? value, string? userId) - { - LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); - Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); - UserId = userId; - } - - /// - /// Initializes a new instance of the class. - /// - public IdentityUserToken(int id, string? loginProvider, string? name, string? value, string userId, DateTime createDate) - { - Id = id; - LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); - Name = name ?? throw new ArgumentNullException(nameof(name)); - Value = value ?? throw new ArgumentNullException(nameof(value)); - UserId = userId; - CreateDate = createDate; - } - - /// - public string LoginProvider { get; set; } - - /// - public string Name { get; set; } - - /// - public string Value { get; set; } - - /// - public string? UserId { get; set; } + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + Value = value ?? throw new ArgumentNullException(nameof(value)); + UserId = userId; } + + /// + /// Initializes a new instance of the class. + /// + public IdentityUserToken(int id, string? loginProvider, string? name, string? value, string userId, DateTime createDate) + { + Id = id; + LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + Value = value ?? throw new ArgumentNullException(nameof(value)); + UserId = userId; + CreateDate = createDate; + } + + /// + public string LoginProvider { get; set; } + + /// + public string Name { get; set; } + + /// + public string Value { get; set; } + + /// + public string? UserId { get; set; } } diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs index b8c7596b2d..3b53509240 100644 --- a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs +++ b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs @@ -1,239 +1,241 @@ -using System; using System.ComponentModel; using System.Security.Cryptography; using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Handles password hashing and formatting for legacy hashing algorithms. +/// +/// +/// Should probably be internal. +/// +public class LegacyPasswordSecurity { + public static string GenerateSalt() + { + var numArray = new byte[16]; + new RNGCryptoServiceProvider().GetBytes(numArray); + return Convert.ToBase64String(numArray); + } + + // TODO: Remove v11 + // Used for tests + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] + public string HashPasswordForStorage(string algorithmType, string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentException("password cannot be empty", nameof(password)); + } + + var hashed = HashNewPassword(algorithmType, password, out string salt); + return FormatPasswordForStorage(algorithmType, hashed, salt); + } + + // TODO: Remove v11 + // Used for tests + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] + public string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt) + { + if (!SupportHashAlgorithm(algorithmType)) + { + throw new InvalidOperationException($"{algorithmType} is not supported"); + } + + return salt + hashedPassword; + } /// - /// Handles password hashing and formatting for legacy hashing algorithms. + /// Verifies if the password matches the expected hash+salt of the stored password string /// - /// - /// Should probably be internal. - /// - public class LegacyPasswordSecurity + /// The hashing algorithm for the stored password. + /// The password. + /// The value of the password stored in a data store. + /// + public bool VerifyPassword(string algorithm, string password, string dbPassword) { - // TODO: Remove v11 - // Used for tests - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] - public string HashPasswordForStorage(string algorithmType, string password) + if (string.IsNullOrWhiteSpace(dbPassword)) { - if (string.IsNullOrWhiteSpace(password)) - throw new ArgumentException("password cannot be empty", nameof(password)); - - string salt; - var hashed = HashNewPassword(algorithmType, password, out salt); - return FormatPasswordForStorage(algorithmType, hashed, salt); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword)); } - // TODO: Remove v11 - // Used for tests - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] - public string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt) + if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix)) { - if (!SupportHashAlgorithm(algorithmType)) - { - throw new InvalidOperationException($"{algorithmType} is not supported"); - } - - return salt + hashedPassword; + return false; } - /// - /// Verifies if the password matches the expected hash+salt of the stored password string - /// - /// The hashing algorithm for the stored password. - /// The password. - /// The value of the password stored in a data store. - /// - public bool VerifyPassword(string algorithm, string password, string dbPassword) + try { - if (string.IsNullOrWhiteSpace(dbPassword)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword)); - } - - if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix)) - { - return false; - } - - try - { - var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt); - var hashed = HashPassword(algorithm, password, salt); - return storedHashedPass == hashed; - } - catch (ArgumentOutOfRangeException) - { - //This can happen if the length of the password is wrong and a salt cannot be extracted. - return false; - } + var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt); + var hashed = HashPassword(algorithm, password, salt); + return storedHashedPass == hashed; } - - /// - /// Verify a legacy hashed password (HMACSHA1) - /// - public bool VerifyLegacyHashedPassword(string password, string dbPassword) + catch (ArgumentOutOfRangeException) { - var hashAlgorithm = new HMACSHA1 - { - //the legacy salt was actually the password :( - Key = Encoding.Unicode.GetBytes(password) - }; - - var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password))); - - return dbPassword == hashed; - } - - /// - /// Create a new password hash and a new salt - /// - /// The hashing algorithm for the password. - /// - /// - /// - // TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage - // TODO: Remove v11 - [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] - public string HashNewPassword(string algorithm, string newPassword, out string salt) - { - salt = GenerateSalt(); - return HashPassword(algorithm, newPassword, salt); - } - - /// - /// Parses out the hashed password and the salt from the stored password string value - /// - /// The hashing algorithm for the stored password. - /// - /// returns the salt - /// - public string ParseStoredHashPassword(string algorithm, string storedString, out string salt) - { - if (string.IsNullOrWhiteSpace(storedString)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString)); - } - - if (!SupportHashAlgorithm(algorithm)) - { - throw new InvalidOperationException($"{algorithm} is not supported"); - } - - var saltLen = GenerateSalt(); - salt = storedString.Substring(0, saltLen.Length); - return storedString.Substring(saltLen.Length); - } - - public static string GenerateSalt() - { - var numArray = new byte[16]; - new RNGCryptoServiceProvider().GetBytes(numArray); - return Convert.ToBase64String(numArray); - } - - /// - /// Hashes a password with a given salt - /// - /// The hashing algorithm for the password. - /// - /// - /// - private string HashPassword(string algorithmType, string pass, string salt) - { - if (!SupportHashAlgorithm(algorithmType)) - { - throw new InvalidOperationException($"{algorithmType} is not supported"); - } - - // This is the correct way to implement this (as per the sql membership provider) - - var bytes = Encoding.Unicode.GetBytes(pass); - var saltBytes = Convert.FromBase64String(salt); - byte[] inArray; - - using var hashAlgorithm = GetHashAlgorithm(algorithmType); - var algorithm = hashAlgorithm as KeyedHashAlgorithm; - if (algorithm != null) - { - var keyedHashAlgorithm = algorithm; - if (keyedHashAlgorithm.Key.Length == saltBytes.Length) - { - //if the salt bytes is the required key length for the algorithm, use it as-is - keyedHashAlgorithm.Key = saltBytes; - } - else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) - { - //if the salt bytes is too long for the required key length for the algorithm, reduce it - var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; - Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); - keyedHashAlgorithm.Key = numArray2; - } - else - { - //if the salt bytes is too short for the required key length for the algorithm, extend it - var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; - var dstOffset = 0; - while (dstOffset < numArray2.Length) - { - var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset); - Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count); - dstOffset += count; - } - keyedHashAlgorithm.Key = numArray2; - } - inArray = keyedHashAlgorithm.ComputeHash(bytes); - } - else - { - var buffer = new byte[saltBytes.Length + bytes.Length]; - Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length); - Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length); - inArray = hashAlgorithm.ComputeHash(buffer); - } - - return Convert.ToBase64String(inArray); - } - - /// - /// Return the hash algorithm to use based on the - /// - /// The hashing algorithm name. - /// - /// - private HashAlgorithm GetHashAlgorithm(string algorithm) - { - if (algorithm.IsNullOrWhiteSpace()) - throw new InvalidOperationException("No hash algorithm type specified"); - - var alg = HashAlgorithm.Create(algorithm); - if (alg == null) - throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved"); - - return alg; - } - - public bool SupportHashAlgorithm(string algorithm) - { - // This is for the v6-v8 hashing algorithm - if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName)) - { - return true; - } - - // Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes) - if (algorithm.InvariantEquals("SHA1")) - { - return true; - } - + // This can happen if the length of the password is wrong and a salt cannot be extracted. return false; } } + + /// + /// Verify a legacy hashed password (HMACSHA1) + /// + public bool VerifyLegacyHashedPassword(string password, string dbPassword) + { + var hashAlgorithm = new HMACSHA1 + { + // the legacy salt was actually the password :( + Key = Encoding.Unicode.GetBytes(password), + }; + + var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password))); + + return dbPassword == hashed; + } + + /// + /// Create a new password hash and a new salt + /// + /// The hashing algorithm for the password. + /// + /// + /// + // TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage + // TODO: Remove v11 + [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")] + public string HashNewPassword(string algorithm, string newPassword, out string salt) + { + salt = GenerateSalt(); + return HashPassword(algorithm, newPassword, salt); + } + + /// + /// Parses out the hashed password and the salt from the stored password string value + /// + /// The hashing algorithm for the stored password. + /// + /// returns the salt + /// + public string ParseStoredHashPassword(string algorithm, string storedString, out string salt) + { + if (string.IsNullOrWhiteSpace(storedString)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString)); + } + + if (!SupportHashAlgorithm(algorithm)) + { + throw new InvalidOperationException($"{algorithm} is not supported"); + } + + var saltLen = GenerateSalt(); + salt = storedString.Substring(0, saltLen.Length); + return storedString.Substring(saltLen.Length); + } + + public bool SupportHashAlgorithm(string algorithm) + { + // This is for the v6-v8 hashing algorithm + if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName)) + { + return true; + } + + // Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes) + if (algorithm.InvariantEquals("SHA1")) + { + return true; + } + + return false; + } + + /// + /// Hashes a password with a given salt + /// + /// The hashing algorithm for the password. + /// + /// + /// + private string HashPassword(string algorithmType, string pass, string salt) + { + if (!SupportHashAlgorithm(algorithmType)) + { + throw new InvalidOperationException($"{algorithmType} is not supported"); + } + + // This is the correct way to implement this (as per the sql membership provider) + var bytes = Encoding.Unicode.GetBytes(pass); + var saltBytes = Convert.FromBase64String(salt); + byte[] inArray; + + using HashAlgorithm hashAlgorithm = GetHashAlgorithm(algorithmType); + if (hashAlgorithm is KeyedHashAlgorithm algorithm) + { + KeyedHashAlgorithm keyedHashAlgorithm = algorithm; + if (keyedHashAlgorithm.Key.Length == saltBytes.Length) + { + // if the salt bytes is the required key length for the algorithm, use it as-is + keyedHashAlgorithm.Key = saltBytes; + } + else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) + { + // if the salt bytes is too long for the required key length for the algorithm, reduce it + var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; + Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); + keyedHashAlgorithm.Key = numArray2; + } + else + { + // if the salt bytes is too short for the required key length for the algorithm, extend it + var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; + var dstOffset = 0; + while (dstOffset < numArray2.Length) + { + var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset); + Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count); + dstOffset += count; + } + + keyedHashAlgorithm.Key = numArray2; + } + + inArray = keyedHashAlgorithm.ComputeHash(bytes); + } + else + { + var buffer = new byte[saltBytes.Length + bytes.Length]; + Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length); + Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length); + inArray = hashAlgorithm.ComputeHash(buffer); + } + + return Convert.ToBase64String(inArray); + } + + /// + /// Return the hash algorithm to use based on the + /// + /// The hashing algorithm name. + /// + /// + private HashAlgorithm GetHashAlgorithm(string algorithm) + { + if (algorithm.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("No hash algorithm type specified"); + } + + var alg = HashAlgorithm.Create(algorithm); + if (alg == null) + { + throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved"); + } + + return alg; + } } diff --git a/src/Umbraco.Core/Security/MediaPermissions.cs b/src/Umbraco.Core/Security/MediaPermissions.cs index d30ab90af2..c46d32f565 100644 --- a/src/Umbraco.Core/Security/MediaPermissions.cs +++ b/src/Umbraco.Core/Security/MediaPermissions.cs @@ -1,77 +1,84 @@ -using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Checks user access to media +/// +public class MediaPermissions { - /// - /// Checks user access to media - /// - public class MediaPermissions + private readonly AppCaches _appCaches; + + public enum MediaAccess { - private readonly IMediaService _mediaService; - private readonly IEntityService _entityService; - private readonly AppCaches _appCaches; + Granted, + Denied, + NotFound, + } - public enum MediaAccess + private readonly IEntityService _entityService; + private readonly IMediaService _mediaService; + + public MediaPermissions(IMediaService mediaService, IEntityService entityService, AppCaches appCaches) + { + _mediaService = mediaService; + _entityService = entityService; + _appCaches = appCaches; + } + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// + public MediaAccess CheckPermissions(IUser? user, int nodeId, out IMedia? media) + { + if (user == null) { - Granted, - Denied, - NotFound + throw new ArgumentNullException(nameof(user)); } - public MediaPermissions(IMediaService mediaService, IEntityService entityService, AppCaches appCaches) + media = null; + + if (nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) { - _mediaService = mediaService; - _entityService = entityService; - _appCaches = appCaches; + media = _mediaService.GetById(nodeId); } - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - public MediaAccess CheckPermissions(IUser? user, int nodeId, out IMedia? media) + if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) { - if (user == null) throw new ArgumentNullException(nameof(user)); - - media = null; - - if (nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - media = _mediaService.GetById(nodeId); - } - - if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - return MediaAccess.NotFound; - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasMediaRootAccess(_entityService, _appCaches) - : (nodeId == Constants.System.RecycleBinMedia) - ? user.HasMediaBinAccess(_entityService, _appCaches) - : user.HasPathAccess(media, _entityService, _appCaches); - - return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + return MediaAccess.NotFound; } - public MediaAccess CheckPermissions(IMedia? media, IUser? user) + var hasPathAccess = nodeId == Constants.System.Root + ? user.HasMediaRootAccess(_entityService, _appCaches) + : nodeId == Constants.System.RecycleBinMedia + ? user.HasMediaBinAccess(_entityService, _appCaches) + : user.HasPathAccess(media, _entityService, _appCaches); + + return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + } + + public MediaAccess CheckPermissions(IMedia? media, IUser? user) + { + if (user == null) { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (media == null) return MediaAccess.NotFound; - - var hasPathAccess = user.HasPathAccess(media, _entityService, _appCaches); - - return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + throw new ArgumentNullException(nameof(user)); } + + if (media == null) + { + return MediaAccess.NotFound; + } + + var hasPathAccess = user.HasPathAccess(media, _entityService, _appCaches); + + return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; } } diff --git a/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs index 2ada23631a..5892f786a7 100644 --- a/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs +++ b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs @@ -1,10 +1,6 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public class NoopHtmlSanitizer : IHtmlSanitizer { - public class NoopHtmlSanitizer : IHtmlSanitizer - { - public string Sanitize(string html) - { - return html; - } - } + public string Sanitize(string html) => html; } diff --git a/src/Umbraco.Core/Security/PasswordGenerator.cs b/src/Umbraco.Core/Security/PasswordGenerator.cs index 55a6ba1a51..0a3e8925ad 100644 --- a/src/Umbraco.Core/Security/PasswordGenerator.cs +++ b/src/Umbraco.Core/Security/PasswordGenerator.cs @@ -1,161 +1,213 @@ -using System; -using System.Linq; using System.Security.Cryptography; using Umbraco.Cms.Core.Configuration; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +/// +/// Generates a password +/// +/// +/// This uses logic copied from the old MembershipProvider.GeneratePassword logic +/// +public class PasswordGenerator { + private readonly IPasswordConfiguration _passwordConfiguration; + + public PasswordGenerator(IPasswordConfiguration passwordConfiguration) => + _passwordConfiguration = passwordConfiguration; + + public string GeneratePassword() + { + var password = PasswordStore.GeneratePassword( + _passwordConfiguration.RequiredLength, + _passwordConfiguration.GetMinNonAlphaNumericChars()); + + var random = new Random(); + + var passwordChars = password.ToCharArray(); + + if (_passwordConfiguration.RequireDigit && + passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x))) + { + password += Convert.ToChar(random.Next(48, 58)); // 0-9 + } + + if (_passwordConfiguration.RequireLowercase && + passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x))) + { + password += Convert.ToChar(random.Next(97, 123)); // a-z + } + + if (_passwordConfiguration.RequireUppercase && + passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x))) + { + password += Convert.ToChar(random.Next(65, 91)); // A-Z + } + + if (_passwordConfiguration.RequireNonLetterOrDigit && + passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x))) + { + password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./ + } + + return password; + } + /// - /// Generates a password + /// Internal class copied from ASP.NET Framework MembershipProvider /// /// - /// This uses logic copied from the old MembershipProvider.GeneratePassword logic + /// See https://stackoverflow.com/a/39855417/694494 + + /// https://github.com/Microsoft/referencesource/blob/master/System.Web/Security/Membership.cs /// - public class PasswordGenerator + private static class PasswordStore { - private readonly IPasswordConfiguration _passwordConfiguration; + private static readonly char[] Punctuations = "!@#$%^&*()_-+=[{]};:>|./?".ToCharArray(); + private static readonly char[] StartingChars = { '<', '&' }; - public PasswordGenerator(IPasswordConfiguration passwordConfiguration) + /// Generates a random password of the specified length. + /// A random password of the specified length. + /// + /// The number of characters in the generated password. The length must be between 1 and 128 + /// characters. + /// + /// + /// The minimum number of non-alphanumeric characters (such as @, #, !, %, + /// &, and so on) in the generated password. + /// + /// + /// is less than 1 or greater than 128 -or- + /// is less than 0 or greater than . + /// + public static string GeneratePassword(int length, int numberOfNonAlphanumericCharacters) { - _passwordConfiguration = passwordConfiguration; - } - public string GeneratePassword() - { - var password = PasswordStore.GeneratePassword( - _passwordConfiguration.RequiredLength, - _passwordConfiguration.GetMinNonAlphaNumericChars()); + if (length < 1 || length > 128) + { + throw new ArgumentException("password length incorrect", nameof(length)); + } - var random = new Random(); + if (numberOfNonAlphanumericCharacters > length || numberOfNonAlphanumericCharacters < 0) + { + throw new ArgumentException( + "min required non alphanumeric characters incorrect", + nameof(numberOfNonAlphanumericCharacters)); + } - var passwordChars = password.ToCharArray(); + string s; + do + { + var data = new byte[length]; + var chArray = new char[length]; + var num1 = 0; + new RNGCryptoServiceProvider().GetBytes(data); + for (var index = 0; index < length; ++index) + { + var num2 = data[index] % 87; + if (num2 < 10) + { + chArray[index] = (char)(48 + num2); + } + else if (num2 < 36) + { + chArray[index] = (char)(65 + num2 - 10); + } + else if (num2 < 62) + { + chArray[index] = (char)(97 + num2 - 36); + } + else + { + chArray[index] = Punctuations[num2 - 62]; + ++num1; + } + } - if (_passwordConfiguration.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x))) - password += Convert.ToChar(random.Next(48, 58)); // 0-9 + if (num1 < numberOfNonAlphanumericCharacters) + { + var random = new Random(); + for (var index1 = 0; index1 < numberOfNonAlphanumericCharacters - num1; ++index1) + { + int index2; + do + { + index2 = random.Next(0, length); + } + while (!char.IsLetterOrDigit(chArray[index2])); - if (_passwordConfiguration.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x))) - password += Convert.ToChar(random.Next(97, 123)); // a-z + chArray[index2] = Punctuations[random.Next(0, Punctuations.Length)]; + } + } - if (_passwordConfiguration.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x))) - password += Convert.ToChar(random.Next(65, 91)); // A-Z + s = new string(chArray); + } + while (IsDangerousString(s, out int matchIndex)); - if (_passwordConfiguration.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x))) - password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./ - - return password; + return s; } - /// - /// Internal class copied from ASP.NET Framework MembershipProvider - /// - /// - /// See https://stackoverflow.com/a/39855417/694494 + https://github.com/Microsoft/referencesource/blob/master/System.Web/Security/Membership.cs - /// - private static class PasswordStore + private static bool IsDangerousString(string s, out int matchIndex) { - private static readonly char[] Punctuations = "!@#$%^&*()_-+=[{]};:>|./?".ToCharArray(); - private static readonly char[] StartingChars = new char[] { '<', '&' }; - /// Generates a random password of the specified length. - /// A random password of the specified length. - /// The number of characters in the generated password. The length must be between 1 and 128 characters. - /// The minimum number of non-alphanumeric characters (such as @, #, !, %, &, and so on) in the generated password. - /// - /// is less than 1 or greater than 128 -or- is less than 0 or greater than . - public static string GeneratePassword(int length, int numberOfNonAlphanumericCharacters) + // bool inComment = false; + matchIndex = 0; + + for (var i = 0; ;) { - if (length < 1 || length > 128) - throw new ArgumentException("password length incorrect", nameof(length)); - if (numberOfNonAlphanumericCharacters > length || numberOfNonAlphanumericCharacters < 0) - throw new ArgumentException("min required non alphanumeric characters incorrect", nameof(numberOfNonAlphanumericCharacters)); - string s; - int matchIndex; - do + // Look for the start of one of our patterns + var n = s.IndexOfAny(StartingChars, i); + + // If not found, the string is safe + if (n < 0) { - var data = new byte[length]; - var chArray = new char[length]; - var num1 = 0; - new RNGCryptoServiceProvider().GetBytes(data); - for (var index = 0; index < length; ++index) - { - var num2 = (int)data[index] % 87; - if (num2 < 10) - chArray[index] = (char)(48 + num2); - else if (num2 < 36) - chArray[index] = (char)(65 + num2 - 10); - else if (num2 < 62) - { - chArray[index] = (char)(97 + num2 - 36); - } - else - { - chArray[index] = Punctuations[num2 - 62]; - ++num1; - } - } - if (num1 < numberOfNonAlphanumericCharacters) - { - var random = new Random(); - for (var index1 = 0; index1 < numberOfNonAlphanumericCharacters - num1; ++index1) - { - int index2; - do - { - index2 = random.Next(0, length); - } - while (!char.IsLetterOrDigit(chArray[index2])); - chArray[index2] = Punctuations[random.Next(0, Punctuations.Length)]; - } - } - s = new string(chArray); + return false; } - while (IsDangerousString(s, out matchIndex)); - return s; - } - private static bool IsDangerousString(string s, out int matchIndex) - { - //bool inComment = false; - matchIndex = 0; - - for (var i = 0; ;) + // If it's the last char, it's safe + if (n == s.Length - 1) { - - // Look for the start of one of our patterns - var n = s.IndexOfAny(StartingChars, i); - - // If not found, the string is safe - if (n < 0) return false; - - // If it's the last char, it's safe - if (n == s.Length - 1) return false; - - matchIndex = n; - - switch (s[n]) - { - case '<': - // If the < is followed by a letter or '!', it's unsafe (looks like a tag or HTML comment) - if (IsAtoZ(s[n + 1]) || s[n + 1] == '!' || s[n + 1] == '/' || s[n + 1] == '?') return true; - break; - case '&': - // If the & is followed by a #, it's unsafe (e.g. S) - if (s[n + 1] == '#') return true; - break; - } - - // Continue searching - i = n + 1; + return false; } + + matchIndex = n; + + switch (s[n]) + { + case '<': + // If the < is followed by a letter or '!', it's unsafe (looks like a tag or HTML comment) + if (IsAtoZ(s[n + 1]) || s[n + 1] == '!' || s[n + 1] == '/' || s[n + 1] == '?') + { + return true; + } + + break; + case '&': + // If the & is followed by a #, it's unsafe (e.g. S) + if (s[n + 1] == '#') + { + return true; + } + + break; + } + + // Continue searching + i = n + 1; + } + } + + private static bool IsAtoZ(char c) + { + if (c >= 97 && c <= 122) + { + return true; } - private static bool IsAtoZ(char c) + if (c >= 65) { - if ((int)c >= 97 && (int)c <= 122) - return true; - if ((int)c >= 65) - return (int)c <= 90; - return false; + return c <= 90; } + + return false; } } } diff --git a/src/Umbraco.Core/Security/PublicAccessStatus.cs b/src/Umbraco.Core/Security/PublicAccessStatus.cs index b92c0ff57a..9026b11fd5 100644 --- a/src/Umbraco.Core/Security/PublicAccessStatus.cs +++ b/src/Umbraco.Core/Security/PublicAccessStatus.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public enum PublicAccessStatus { - public enum PublicAccessStatus - { - NotLoggedIn, - AccessDenied, - NotApproved, - LockedOut, - AccessAccepted - } + NotLoggedIn, + AccessDenied, + NotApproved, + LockedOut, + AccessAccepted, } diff --git a/src/Umbraco.Core/Security/UpdateMemberProfileResult.cs b/src/Umbraco.Core/Security/UpdateMemberProfileResult.cs index b6b6c241e4..0809f6c501 100644 --- a/src/Umbraco.Core/Security/UpdateMemberProfileResult.cs +++ b/src/Umbraco.Core/Security/UpdateMemberProfileResult.cs @@ -1,24 +1,18 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public class UpdateMemberProfileResult { - public class UpdateMemberProfileResult + private UpdateMemberProfileResult() { - private UpdateMemberProfileResult() - { - } - - public UpdateMemberProfileStatus Status { get; private set; } - - public string? ErrorMessage { get; private set; } - - public static UpdateMemberProfileResult Success() - { - return new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Success }; - } - - public static UpdateMemberProfileResult Error(string message) - { - return new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Error, ErrorMessage = message }; - } } + public UpdateMemberProfileStatus Status { get; private set; } + + public string? ErrorMessage { get; private set; } + + public static UpdateMemberProfileResult Success() => + new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Success }; + + public static UpdateMemberProfileResult Error(string message) => + new UpdateMemberProfileResult { Status = UpdateMemberProfileStatus.Error, ErrorMessage = message }; } diff --git a/src/Umbraco.Core/Security/UpdateMemberProfileStatus.cs b/src/Umbraco.Core/Security/UpdateMemberProfileStatus.cs index df805d3096..74fb52e697 100644 --- a/src/Umbraco.Core/Security/UpdateMemberProfileStatus.cs +++ b/src/Umbraco.Core/Security/UpdateMemberProfileStatus.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.Security +namespace Umbraco.Cms.Core.Security; + +public enum UpdateMemberProfileStatus { - public enum UpdateMemberProfileStatus - { - Success, - Error, - } + Success, + Error, } diff --git a/src/Umbraco.Core/Semver/Semver.cs b/src/Umbraco.Core/Semver/Semver.cs index 5a04553f1b..3c33f43087 100644 --- a/src/Umbraco.Core/Semver/Semver.cs +++ b/src/Umbraco.Core/Semver/Semver.cs @@ -1,5 +1,4 @@ -using System; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; #if !NETSTANDARD using System.Globalization; using System.Runtime.Serialization; @@ -30,8 +29,8 @@ THE SOFTWARE. namespace Umbraco.Cms.Core.Semver { /// - /// A semantic version implementation. - /// Conforms to v2.0.0 of http://semver.org/ + /// A semantic version implementation. + /// Conforms to v2.0.0 of http://semver.org/ /// #if NETSTANDARD public sealed class SemVersion : IComparable, IComparable @@ -40,8 +39,9 @@ namespace Umbraco.Cms.Core.Semver public sealed class SemVersion : IComparable, IComparable, ISerializable #endif { - static Regex parseEx = - new Regex(@"^(?\d+)" + + private static Regex parseEx = + new( + @"^(?\d+)" + @"(\.(?\d+))?" + @"(\.(?\d+))?" + @"(\-(?
[0-9A-Za-z\-\.]+))?" +
@@ -54,15 +54,19 @@ namespace Umbraco.Cms.Core.Semver
 
 #if !NETSTANDARD
         /// 
-        /// Initializes a new instance of the  class.
+        ///     Initializes a new instance of the  class.
         /// 
         /// 
         /// 
         /// 
         private SemVersion(SerializationInfo info, StreamingContext context)
         {
-            if (info == null) throw new ArgumentNullException("info");
-            var semVersion = Parse(info.GetString("SemVersion")!);
+            if (info == null)
+            {
+                throw new ArgumentNullException("info");
+            }
+
+            SemVersion semVersion = Parse(info.GetString("SemVersion")!);
             Major = semVersion.Major;
             Minor = semVersion.Minor;
             Patch = semVersion.Patch;
@@ -72,7 +76,7 @@ namespace Umbraco.Cms.Core.Semver
 #endif
 
         /// 
-        /// Initializes a new instance of the  class.
+        ///     Initializes a new instance of the  class.
         /// 
         /// The major version.
         /// The minor version.
@@ -81,46 +85,50 @@ namespace Umbraco.Cms.Core.Semver
         /// The build eg ("nightly.232").
         public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = "", string build = "")
         {
-            this.Major = major;
-            this.Minor = minor;
-            this.Patch = patch;
+            Major = major;
+            Minor = minor;
+            Patch = patch;
 
-            this.Prerelease = prerelease ?? "";
-            this.Build = build ?? "";
+            Prerelease = prerelease ?? string.Empty;
+            Build = build ?? string.Empty;
         }
 
         /// 
-        /// Initializes a new instance of the  class.
+        ///     Initializes a new instance of the  class.
         /// 
-        /// The  that is used to initialize
-        /// the Major, Minor, Patch and Build properties.
+        /// 
+        ///     The  that is used to initialize
+        ///     the Major, Minor, Patch and Build properties.
+        /// 
         public SemVersion(Version version)
         {
             if (version == null)
+            {
                 throw new ArgumentNullException("version");
+            }
 
-            this.Major = version.Major;
-            this.Minor = version.Minor;
+            Major = version.Major;
+            Minor = version.Minor;
 
             if (version.Revision >= 0)
             {
-                this.Patch = version.Revision;
+                Patch = version.Revision;
             }
 
-            this.Prerelease = String.Empty;
+            Prerelease = string.Empty;
 
             if (version.Build > 0)
             {
-                this.Build = version.Build.ToString();
+                Build = version.Build.ToString();
             }
             else
             {
-                this.Build = String.Empty;
+                Build = string.Empty;
             }
         }
 
         /// 
-        /// Parses the specified string to a semantic version.
+        ///     Parses the specified string to a semantic version.
         /// 
         /// The version string.
         /// If set to true minor and patch version are required, else they default to 0.
@@ -128,9 +136,11 @@ namespace Umbraco.Cms.Core.Semver
         /// When a invalid version string is passed.
         public static SemVersion Parse(string version, bool strict = false)
         {
-            var match = parseEx.Match(version);
+            Match match = parseEx.Match(version);
             if (!match.Success)
+            {
                 throw new ArgumentException("Invalid version.", "version");
+            }
 
 #if NETSTANDARD
             var major = int.Parse(match.Groups["major"].Value);
@@ -138,8 +148,8 @@ namespace Umbraco.Cms.Core.Semver
             var major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
 #endif
 
-            var minorMatch = match.Groups["minor"];
-            int minor = 0;
+            Group minorMatch = match.Groups["minor"];
+            var minor = 0;
             if (minorMatch.Success)
             {
 #if NETSTANDARD
@@ -153,8 +163,8 @@ namespace Umbraco.Cms.Core.Semver
                 throw new InvalidOperationException("Invalid version (no minor version given in strict mode)");
             }
 
-            var patchMatch = match.Groups["patch"];
-            int patch = 0;
+            Group patchMatch = match.Groups["patch"];
+            var patch = 0;
             if (patchMatch.Success)
             {
 #if NETSTANDARD
@@ -175,12 +185,14 @@ namespace Umbraco.Cms.Core.Semver
         }
 
         /// 
-        /// Parses the specified string to a semantic version.
+        ///     Parses the specified string to a semantic version.
         /// 
         /// The version string.
-        /// When the method returns, contains a SemVersion instance equivalent
-        /// to the version string passed in, if the version string was valid, or null if the
-        /// version string was not valid.
+        /// 
+        ///     When the method returns, contains a SemVersion instance equivalent
+        ///     to the version string passed in, if the version string was valid, or null if the
+        ///     version string was not valid.
+        /// 
         /// If set to true minor and patch version are required, else they default to 0.
         /// False when a invalid version string is passed, otherwise true.
         public static bool TryParse(string version, out SemVersion? semver, bool strict = false)
@@ -198,7 +210,7 @@ namespace Umbraco.Cms.Core.Semver
         }
 
         /// 
-        /// Tests the specified versions for equality.
+        ///     Tests the specified versions for equality.
         /// 
         /// The first version.
         /// The second version.
@@ -206,26 +218,34 @@ namespace Umbraco.Cms.Core.Semver
         public static bool Equals(SemVersion versionA, SemVersion versionB)
         {
             if (ReferenceEquals(versionA, null))
+            {
                 return ReferenceEquals(versionB, null);
+            }
+
             return versionA.Equals(versionB);
         }
 
         /// 
-        /// Compares the specified versions.
+        ///     Compares the specified versions.
         /// 
         /// The version to compare to.
         /// The version to compare against.
-        /// If versionA < versionB < 0, if versionA > versionB > 0,
-        /// if versionA is equal to versionB 0.
+        /// 
+        ///     If versionA < versionB < 0, if versionA > versionB > 0,
+        ///     if versionA is equal to versionB 0.
+        /// 
         public static int Compare(SemVersion versionA, SemVersion versionB)
         {
             if (ReferenceEquals(versionA, null))
+            {
                 return ReferenceEquals(versionB, null) ? 0 : -1;
+            }
+
             return versionA.CompareTo(versionB);
         }
 
         /// 
-        /// Make a copy of the current instance with optional altered fields.
+        ///     Make a copy of the current instance with optional altered fields.
         /// 
         /// The major version.
         /// The minor version.
@@ -233,194 +253,224 @@ namespace Umbraco.Cms.Core.Semver
         /// The prerelease text.
         /// The build text.
         /// The new version object.
-        public SemVersion Change(int? major = null, int? minor = null, int? patch = null,
-            string? prerelease = null, string? build = null)
-        {
-            return new SemVersion(
-                major ?? this.Major,
-                minor ?? this.Minor,
-                patch ?? this.Patch,
-                prerelease ?? this.Prerelease,
-                build ?? this.Build);
-        }
+        public SemVersion Change(int? major = null, int? minor = null, int? patch = null, string? prerelease = null, string? build = null) =>
+            new(
+                major ?? Major,
+                minor ?? Minor,
+                patch ?? Patch,
+                prerelease ?? Prerelease,
+                build ?? Build);
 
         /// 
-        /// Gets the major version.
+        ///     Gets the major version.
         /// 
         /// 
-        /// The major version.
+        ///     The major version.
         /// 
         public int Major { get; private set; }
 
         /// 
-        /// Gets the minor version.
+        ///     Gets the minor version.
         /// 
         /// 
-        /// The minor version.
+        ///     The minor version.
         /// 
         public int Minor { get; private set; }
 
         /// 
-        /// Gets the patch version.
+        ///     Gets the patch version.
         /// 
         /// 
-        /// The patch version.
+        ///     The patch version.
         /// 
         public int Patch { get; private set; }
 
         /// 
-        /// Gets the pre-release version.
+        ///     Gets the pre-release version.
         /// 
         /// 
-        /// The pre-release version.
+        ///     The pre-release version.
         /// 
         public string Prerelease { get; private set; }
 
         /// 
-        /// Gets the build version.
+        ///     Gets the build version.
         /// 
         /// 
-        /// The build version.
+        ///     The build version.
         /// 
         public string Build { get; private set; }
 
         /// 
-        /// Returns a  that represents this instance.
+        ///     Returns a  that represents this instance.
         /// 
         /// 
-        /// A  that represents this instance.
+        ///     A  that represents this instance.
         /// 
         public override string ToString()
         {
-            var version = "" + Major + "." + Minor + "." + Patch;
-            if (!String.IsNullOrEmpty(Prerelease))
+            var version = string.Empty + Major + "." + Minor + "." + Patch;
+            if (!string.IsNullOrEmpty(Prerelease))
+            {
                 version += "-" + Prerelease;
-            if (!String.IsNullOrEmpty(Build))
+            }
+
+            if (!string.IsNullOrEmpty(Build))
+            {
                 version += "+" + Build;
+            }
+
             return version;
         }
 
         /// 
-        /// Compares the current instance with another object of the same type and returns an integer that indicates
-        /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
-        /// other object.
+        ///     Compares the current instance with another object of the same type and returns an integer that indicates
+        ///     whether the current instance precedes, follows, or occurs in the same position in the sort order as the
+        ///     other object.
         /// 
         /// An object to compare with this instance.
         /// 
-        /// A value that indicates the relative order of the objects being compared.
-        /// The return value has these meanings: Value Meaning Less than zero
-        ///  This instance precedes  in the sort order.
-        ///  Zero This instance occurs in the same position in the sort order as . i
-        ///  Greater than zero This instance follows  in the sort order.
+        ///     A value that indicates the relative order of the objects being compared.
+        ///     The return value has these meanings: Value Meaning Less than zero
+        ///     This instance precedes  in the sort order.
+        ///     Zero This instance occurs in the same position in the sort order as . i
+        ///     Greater than zero This instance follows  in the sort order.
         /// 
-        public int CompareTo(object? obj)
-        {
-            return CompareTo((SemVersion?)obj);
-        }
+        public int CompareTo(object? obj) => CompareTo((SemVersion?)obj);
 
         /// 
-        /// Compares the current instance with another object of the same type and returns an integer that indicates
-        /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
-        /// other object.
+        ///     Compares the current instance with another object of the same type and returns an integer that indicates
+        ///     whether the current instance precedes, follows, or occurs in the same position in the sort order as the
+        ///     other object.
         /// 
         /// An object to compare with this instance.
         /// 
-        /// A value that indicates the relative order of the objects being compared.
-        /// The return value has these meanings: Value Meaning Less than zero
-        ///  This instance precedes  in the sort order.
-        ///  Zero This instance occurs in the same position in the sort order as . i
-        ///  Greater than zero This instance follows  in the sort order.
+        ///     A value that indicates the relative order of the objects being compared.
+        ///     The return value has these meanings: Value Meaning Less than zero
+        ///     This instance precedes  in the sort order.
+        ///     Zero This instance occurs in the same position in the sort order as . i
+        ///     Greater than zero This instance follows  in the sort order.
         /// 
         public int CompareTo(SemVersion? other)
         {
             if (ReferenceEquals(other, null))
+            {
                 return 1;
+            }
 
-            var r = this.CompareByPrecedence(other);
+            var r = CompareByPrecedence(other);
             if (r != 0)
+            {
                 return r;
+            }
 
-            r = CompareComponent(this.Build, other.Build);
+            r = CompareComponent(Build, other.Build);
             return r;
         }
 
         /// 
-        /// Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
+        ///     Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
         /// 
         /// The semantic version.
         /// true if the version precedence matches.
-        public bool PrecedenceMatches(SemVersion other)
-        {
-            return CompareByPrecedence(other) == 0;
-        }
+        public bool PrecedenceMatches(SemVersion other) => CompareByPrecedence(other) == 0;
 
         /// 
-        /// Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
+        ///     Compares to semantic versions by precedence. This does the same as a Equals, but ignores the build information.
         /// 
         /// The semantic version.
         /// 
-        /// A value that indicates the relative order of the objects being compared.
-        /// The return value has these meanings: Value Meaning Less than zero
-        ///  This instance precedes  in the version precedence.
-        ///  Zero This instance has the same precedence as . i
-        ///  Greater than zero This instance has creater precedence as .
+        ///     A value that indicates the relative order of the objects being compared.
+        ///     The return value has these meanings: Value Meaning Less than zero
+        ///     This instance precedes  in the version precedence.
+        ///     Zero This instance has the same precedence as . i
+        ///     Greater than zero This instance has creater precedence as .
         /// 
         public int CompareByPrecedence(SemVersion other)
         {
             if (ReferenceEquals(other, null))
+            {
                 return 1;
+            }
 
-            var r = this.Major.CompareTo(other.Major);
-            if (r != 0) return r;
+            var r = Major.CompareTo(other.Major);
+            if (r != 0)
+            {
+                return r;
+            }
 
-            r = this.Minor.CompareTo(other.Minor);
-            if (r != 0) return r;
+            r = Minor.CompareTo(other.Minor);
+            if (r != 0)
+            {
+                return r;
+            }
 
-            r = this.Patch.CompareTo(other.Patch);
-            if (r != 0) return r;
+            r = Patch.CompareTo(other.Patch);
+            if (r != 0)
+            {
+                return r;
+            }
 
-            r = CompareComponent(this.Prerelease, other.Prerelease, true);
+            r = CompareComponent(Prerelease, other.Prerelease, true);
             return r;
         }
 
-        static int CompareComponent(string a, string b, bool lower = false)
+        private static int CompareComponent(string a, string b, bool lower = false)
         {
-            var aEmpty = String.IsNullOrEmpty(a);
-            var bEmpty = String.IsNullOrEmpty(b);
+            var aEmpty = string.IsNullOrEmpty(a);
+            var bEmpty = string.IsNullOrEmpty(b);
             if (aEmpty && bEmpty)
+            {
                 return 0;
+            }
 
             if (aEmpty)
+            {
                 return lower ? 1 : -1;
+            }
+
             if (bEmpty)
+            {
                 return lower ? -1 : 1;
+            }
 
             var aComps = a.Split('.');
             var bComps = b.Split('.');
 
             var minLen = Math.Min(aComps.Length, bComps.Length);
-            for (int i = 0; i < minLen; i++)
+            for (var i = 0; i < minLen; i++)
             {
                 var ac = aComps[i];
                 var bc = bComps[i];
                 int anum, bnum;
-                var isanum = Int32.TryParse(ac, out anum);
-                var isbnum = Int32.TryParse(bc, out bnum);
+                var isanum = int.TryParse(ac, out anum);
+                var isbnum = int.TryParse(bc, out bnum);
                 int r;
                 if (isanum && isbnum)
                 {
                     r = anum.CompareTo(bnum);
-                    if (r != 0) return anum.CompareTo(bnum);
+                    if (r != 0)
+                    {
+                        return anum.CompareTo(bnum);
+                    }
                 }
                 else
                 {
                     if (isanum)
+                    {
                         return -1;
+                    }
+
                     if (isbnum)
+                    {
                         return 1;
-                    r = String.CompareOrdinal(ac, bc);
+                    }
+
+                    r = string.CompareOrdinal(ac, bc);
                     if (r != 0)
+                    {
                         return r;
+                    }
                 }
             }
 
@@ -428,44 +478,48 @@ namespace Umbraco.Cms.Core.Semver
         }
 
         /// 
-        /// Determines whether the specified  is equal to this instance.
+        ///     Determines whether the specified  is equal to this instance.
         /// 
         /// The  to compare with this instance.
         /// 
-        ///   true if the specified  is equal to this instance; otherwise, false.
+        ///     true if the specified  is equal to this instance; otherwise, false.
         /// 
         public override bool Equals(object? obj)
         {
             if (ReferenceEquals(obj, null))
+            {
                 return false;
+            }
 
             if (ReferenceEquals(this, obj))
+            {
                 return true;
+            }
 
             var other = (SemVersion)obj;
 
-            return this.Major == other.Major &&
-                this.Minor == other.Minor &&
-                this.Patch == other.Patch &&
-                string.Equals(this.Prerelease, other.Prerelease, StringComparison.Ordinal) &&
-                string.Equals(this.Build, other.Build, StringComparison.Ordinal);
+            return Major == other.Major &&
+                   Minor == other.Minor &&
+                   Patch == other.Patch &&
+                   string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal) &&
+                   string.Equals(Build, other.Build, StringComparison.Ordinal);
         }
 
         /// 
-        /// Returns a hash code for this instance.
+        ///     Returns a hash code for this instance.
         /// 
         /// 
-        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+        ///     A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
         /// 
         public override int GetHashCode()
         {
             unchecked
             {
-                int result = this.Major.GetHashCode();
-                result = result * 31 + this.Minor.GetHashCode();
-                result = result * 31 + this.Patch.GetHashCode();
-                result = result * 31 + this.Prerelease.GetHashCode();
-                result = result * 31 + this.Build.GetHashCode();
+                var result = Major.GetHashCode();
+                result = (result * 31) + Minor.GetHashCode();
+                result = (result * 31) + Patch.GetHashCode();
+                result = (result * 31) + Prerelease.GetHashCode();
+                result = (result * 31) + Build.GetHashCode();
                 return result;
             }
         }
@@ -474,85 +528,68 @@ namespace Umbraco.Cms.Core.Semver
         [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
         public void GetObjectData(SerializationInfo info, StreamingContext context)
         {
-            if (info == null) throw new ArgumentNullException("info");
+            if (info == null)
+            {
+                throw new ArgumentNullException("info");
+            }
+
             info.AddValue("SemVersion", ToString());
         }
 #endif
 
         /// 
-        /// Implicit conversion from string to SemVersion.
+        ///     Implicit conversion from string to SemVersion.
         /// 
         /// The semantic version.
         /// The SemVersion object.
-        public static implicit operator SemVersion(string version)
-        {
-            return SemVersion.Parse(version);
-        }
+        public static implicit operator SemVersion(string version) => Parse(version);
 
         /// 
-        /// The override of the equals operator.
+        ///     The override of the equals operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is equal to right true, else false.
-        public static bool operator ==(SemVersion left, SemVersion right)
-        {
-            return SemVersion.Equals(left, right);
-        }
+        public static bool operator ==(SemVersion left, SemVersion right) => Equals(left, right);
 
         /// 
-        /// The override of the un-equal operator.
+        ///     The override of the un-equal operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is not equal to right true, else false.
-        public static bool operator !=(SemVersion left, SemVersion right)
-        {
-            return !SemVersion.Equals(left, right);
-        }
+        public static bool operator !=(SemVersion left, SemVersion right) => !Equals(left, right);
 
         /// 
-        /// The override of the greater operator.
+        ///     The override of the greater operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is greater than right true, else false.
-        public static bool operator >(SemVersion left, SemVersion right)
-        {
-            return SemVersion.Compare(left, right) > 0;
-        }
+        public static bool operator >(SemVersion left, SemVersion right) => Compare(left, right) > 0;
 
         /// 
-        /// The override of the greater than or equal operator.
+        ///     The override of the greater than or equal operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is greater than or equal to right true, else false.
-        public static bool operator >=(SemVersion left, SemVersion right)
-        {
-            return left == right || left > right;
-        }
+        public static bool operator >=(SemVersion left, SemVersion right) => left == right || left > right;
 
         /// 
-        /// The override of the less operator.
+        ///     The override of the less operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is less than right true, else false.
-        public static bool operator <(SemVersion left, SemVersion right)
-        {
-            return SemVersion.Compare(left, right) < 0;
-        }
+        public static bool operator <(SemVersion left, SemVersion right) => Compare(left, right) < 0;
 
         /// 
-        /// The override of the less than or equal operator.
+        ///     The override of the less than or equal operator.
         /// 
         /// The left value.
         /// The right value.
         /// If left is less than or equal to right true, else false.
-        public static bool operator <=(SemVersion left, SemVersion right)
-        {
-            return left == right || left < right;
-        }
+        public static bool operator <=(SemVersion left, SemVersion right) => left == right || left < right;
     }
 }
diff --git a/src/Umbraco.Core/Serialization/IConfigurationEditorJsonSerializer.cs b/src/Umbraco.Core/Serialization/IConfigurationEditorJsonSerializer.cs
index dee2e4c5db..9a0429a75e 100644
--- a/src/Umbraco.Core/Serialization/IConfigurationEditorJsonSerializer.cs
+++ b/src/Umbraco.Core/Serialization/IConfigurationEditorJsonSerializer.cs
@@ -1,7 +1,5 @@
-namespace Umbraco.Cms.Core.Serialization
-{
-    public interface IConfigurationEditorJsonSerializer : IJsonSerializer
-    {
+namespace Umbraco.Cms.Core.Serialization;
 
-    }
+public interface IConfigurationEditorJsonSerializer : IJsonSerializer
+{
 }
diff --git a/src/Umbraco.Core/Serialization/IJsonSerializer.cs b/src/Umbraco.Core/Serialization/IJsonSerializer.cs
index 051055b564..5a31a2cf97 100644
--- a/src/Umbraco.Core/Serialization/IJsonSerializer.cs
+++ b/src/Umbraco.Core/Serialization/IJsonSerializer.cs
@@ -1,11 +1,10 @@
-namespace Umbraco.Cms.Core.Serialization
+namespace Umbraco.Cms.Core.Serialization;
+
+public interface IJsonSerializer
 {
-    public interface IJsonSerializer
-    {
-        string Serialize(object? input);
+    string Serialize(object? input);
 
-        T? Deserialize(string input);
+    T? Deserialize(string input);
 
-        T? DeserializeSubset(string input, string key);
-    }
+    T? DeserializeSubset(string input, string key);
 }
diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/AuditService.cs
index f7560afa93..046c5fff3d 100644
--- a/src/Umbraco.Core/Services/AuditService.cs
+++ b/src/Umbraco.Core/Services/AuditService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -8,231 +5,295 @@ using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services.Implement
+namespace Umbraco.Cms.Core.Services.Implement;
+
+public sealed class AuditService : RepositoryService, IAuditService
 {
-    public sealed class AuditService : RepositoryService, IAuditService
+    private readonly IAuditEntryRepository _auditEntryRepository;
+    private readonly IAuditRepository _auditRepository;
+    private readonly Lazy _isAvailable;
+
+    public AuditService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IAuditRepository auditRepository,
+        IAuditEntryRepository auditEntryRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly Lazy _isAvailable;
-        private readonly IAuditRepository _auditRepository;
-        private readonly IAuditEntryRepository _auditEntryRepository;
+        _auditRepository = auditRepository;
+        _auditEntryRepository = auditEntryRepository;
+        _isAvailable = new Lazy(DetermineIsAvailable);
+    }
 
-        public AuditService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IAuditRepository auditRepository, IAuditEntryRepository auditEntryRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    public void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            _auditRepository = auditRepository;
-            _auditEntryRepository = auditEntryRepository;
-            _isAvailable = new Lazy(DetermineIsAvailable);
+            _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, comment, parameters));
+            scope.Complete();
+        }
+    }
+
+    public IEnumerable GetLogs(int objectId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IEnumerable result = _auditRepository.Get(Query().Where(x => x.Id == objectId));
+            scope.Complete();
+            return result;
+        }
+    }
+
+    public IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IEnumerable result = sinceDate.HasValue == false
+                ? _auditRepository.Get(type, Query().Where(x => x.UserId == userId))
+                : _auditRepository.Get(
+                    type,
+                    Query().Where(x => x.UserId == userId && x.CreateDate >= sinceDate.Value));
+            scope.Complete();
+            return result;
+        }
+    }
+
+    public IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IEnumerable result = sinceDate.HasValue == false
+                ? _auditRepository.Get(type, Query())
+                : _auditRepository.Get(type, Query().Where(x => x.CreateDate >= sinceDate.Value));
+            scope.Complete();
+            return result;
+        }
+    }
+
+    public void CleanLogs(int maximumAgeOfLogsInMinutes)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _auditRepository.CleanLogs(maximumAgeOfLogsInMinutes);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Returns paged items in the audit trail for a given entity
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     By default this will always be ordered descending (newest first)
+    /// 
+    /// 
+    ///     Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
+    ///     or the custom filter
+    ///     so we need to do that here
+    /// 
+    /// 
+    ///     Optional filter to be applied
+    /// 
+    /// 
+    public IEnumerable GetPagedItemsByEntity(
+        int entityId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        Direction orderDirection = Direction.Descending,
+        AuditType[]? auditTypeFilter = null,
+        IQuery? customFilter = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        public void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null)
+        if (pageSize <= 0)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, comment, parameters));
-                scope.Complete();
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        public IEnumerable GetLogs(int objectId)
+        if (entityId == Constants.System.Root || entityId <= 0)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var result = _auditRepository.Get(Query().Where(x => x.Id == objectId));
-                scope.Complete();
-                return result;
-            }
+            totalRecords = 0;
+            return Enumerable.Empty();
         }
 
-        public IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null)
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var result = sinceDate.HasValue == false
-                    ? _auditRepository.Get(type, Query().Where(x => x.UserId == userId))
-                    : _auditRepository.Get(type, Query().Where(x => x.UserId == userId && x.CreateDate >= sinceDate.Value));
-                scope.Complete();
-                return result;
-            }
+            IQuery query = Query().Where(x => x.Id == entityId);
+
+            return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
+        }
+    }
+
+    /// 
+    ///     Returns paged items in the audit trail for a given user
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     By default this will always be ordered descending (newest first)
+    /// 
+    /// 
+    ///     Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
+    ///     or the custom filter
+    ///     so we need to do that here
+    /// 
+    /// 
+    ///     Optional filter to be applied
+    /// 
+    /// 
+    public IEnumerable GetPagedItemsByUser(
+        int userId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        Direction orderDirection = Direction.Descending,
+        AuditType[]? auditTypeFilter = null,
+        IQuery? customFilter = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        public IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null)
+        if (pageSize <= 0)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var result = sinceDate.HasValue == false
-                    ? _auditRepository.Get(type, Query())
-                    : _auditRepository.Get(type, Query().Where(x => x.CreateDate >= sinceDate.Value));
-                scope.Complete();
-                return result;
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        public void CleanLogs(int maximumAgeOfLogsInMinutes)
+        if (userId < Constants.Security.SuperUserId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _auditRepository.CleanLogs(maximumAgeOfLogsInMinutes);
-                scope.Complete();
-            }
+            totalRecords = 0;
+            return Enumerable.Empty();
         }
 
-        /// 
-        /// Returns paged items in the audit trail for a given entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// By default this will always be ordered descending (newest first)
-        /// 
-        /// 
-        /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter
-        /// so we need to do that here
-        /// 
-        /// 
-        /// Optional filter to be applied
-        /// 
-        /// 
-        public IEnumerable GetPagedItemsByEntity(int entityId, long pageIndex, int pageSize, out long totalRecords,
-            Direction orderDirection = Direction.Descending,
-            AuditType[]? auditTypeFilter = null,
-            IQuery? customFilter = null)
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+            IQuery query = Query().Where(x => x.UserId == userId);
 
-            if (entityId == Cms.Core.Constants.System.Root || entityId <= 0)
-            {
-                totalRecords = 0;
-                return Enumerable.Empty();
-            }
+            return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
+        }
+    }
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.Id == entityId);
-
-                return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
-            }
+    /// 
+    public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails)
+    {
+        if (performingUserId < 0 && performingUserId != Constants.Security.SuperUserId)
+        {
+            throw new ArgumentOutOfRangeException(nameof(performingUserId));
         }
 
-        /// 
-        /// Returns paged items in the audit trail for a given user
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// By default this will always be ordered descending (newest first)
-        /// 
-        /// 
-        /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter
-        /// so we need to do that here
-        /// 
-        /// 
-        /// Optional filter to be applied
-        /// 
-        /// 
-        public IEnumerable GetPagedItemsByUser(int userId, long pageIndex, int pageSize, out long totalRecords, Direction orderDirection = Direction.Descending, AuditType[]? auditTypeFilter = null, IQuery? customFilter = null)
+        if (string.IsNullOrWhiteSpace(perfomingDetails))
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
-
-            if (userId < Cms.Core.Constants.Security.SuperUserId)
-            {
-                totalRecords = 0;
-                return Enumerable.Empty();
-            }
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.UserId == userId);
-
-                return _auditRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderDirection, auditTypeFilter, customFilter);
-            }
+            throw new ArgumentException("Value cannot be null or whitespace.", nameof(perfomingDetails));
         }
 
-        /// 
-        public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails)
+        if (string.IsNullOrWhiteSpace(eventType))
         {
-            if (performingUserId < 0 && performingUserId != Cms.Core.Constants.Security.SuperUserId) throw new ArgumentOutOfRangeException(nameof(performingUserId));
-            if (string.IsNullOrWhiteSpace(perfomingDetails)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(perfomingDetails));
-            if (string.IsNullOrWhiteSpace(eventType)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventType));
-            if (string.IsNullOrWhiteSpace(eventDetails)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventDetails));
+            throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventType));
+        }
 
-            //we need to truncate the data else we'll get SQL errors
-            affectedDetails = affectedDetails?.Substring(0, Math.Min(affectedDetails.Length, Constants.Audit.DetailsLength));
-            eventDetails = eventDetails.Substring(0, Math.Min(eventDetails.Length, Constants.Audit.DetailsLength));
+        if (string.IsNullOrWhiteSpace(eventDetails))
+        {
+            throw new ArgumentException("Value cannot be null or whitespace.", nameof(eventDetails));
+        }
 
-            //validate the eventType - must contain a forward slash, no spaces, no special chars
-            var eventTypeParts = eventType.ToCharArray();
-            if (eventTypeParts.Contains('/') == false || eventTypeParts.All(c => char.IsLetterOrDigit(c) || c == '/' || c == '-') == false)
-                throw new ArgumentException(nameof(eventType) + " must contain only alphanumeric characters, hyphens and at least one '/' defining a category");
-            if (eventType.Length > Constants.Audit.EventTypeLength)
-                throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(eventType));
-            if (performingIp != null && performingIp.Length > Constants.Audit.IpLength)
-                throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(performingIp));
+        // we need to truncate the data else we'll get SQL errors
+        affectedDetails =
+            affectedDetails?[..Math.Min(affectedDetails.Length, Constants.Audit.DetailsLength)];
+        eventDetails = eventDetails[..Math.Min(eventDetails.Length, Constants.Audit.DetailsLength)];
 
-            var entry = new AuditEntry
-            {
-                PerformingUserId = performingUserId,
-                PerformingDetails = perfomingDetails,
-                PerformingIp = performingIp,
-                EventDateUtc = eventDateUtc,
-                AffectedUserId = affectedUserId,
-                AffectedDetails = affectedDetails,
-                EventType = eventType,
-                EventDetails = eventDetails,
-            };
+        // validate the eventType - must contain a forward slash, no spaces, no special chars
+        var eventTypeParts = eventType.ToCharArray();
+        if (eventTypeParts.Contains('/') == false ||
+            eventTypeParts.All(c => char.IsLetterOrDigit(c) || c == '/' || c == '-') == false)
+        {
+            throw new ArgumentException(nameof(eventType) +
+                                        " must contain only alphanumeric characters, hyphens and at least one '/' defining a category");
+        }
 
-            if (_isAvailable.Value == false) return entry;
+        if (eventType.Length > Constants.Audit.EventTypeLength)
+        {
+            throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(eventType));
+        }
 
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _auditEntryRepository.Save(entry);
-                scope.Complete();
-            }
+        if (performingIp != null && performingIp.Length > Constants.Audit.IpLength)
+        {
+            throw new ArgumentException($"Must be max {Constants.Audit.EventTypeLength} chars.", nameof(performingIp));
+        }
 
+        var entry = new AuditEntry
+        {
+            PerformingUserId = performingUserId,
+            PerformingDetails = perfomingDetails,
+            PerformingIp = performingIp,
+            EventDateUtc = eventDateUtc,
+            AffectedUserId = affectedUserId,
+            AffectedDetails = affectedDetails,
+            EventType = eventType,
+            EventDetails = eventDetails,
+        };
+
+        if (_isAvailable.Value == false)
+        {
             return entry;
         }
 
-        // TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
-        internal IEnumerable? GetAll()
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            if (_isAvailable.Value == false) return Enumerable.Empty();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.GetMany();
-            }
+            _auditEntryRepository.Save(entry);
+            scope.Complete();
         }
 
-        // TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
-        internal IEnumerable GetPage(long pageIndex, int pageCount, out long records)
-        {
-            if (_isAvailable.Value == false)
-            {
-                records = 0;
-                return Enumerable.Empty();
-            }
+        return entry;
+    }
 
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.GetPage(pageIndex, pageCount, out records);
-            }
+    // TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
+    internal IEnumerable? GetAll()
+    {
+        if (_isAvailable.Value == false)
+        {
+            return Enumerable.Empty();
         }
 
-        /// 
-        /// Determines whether the repository is available.
-        /// 
-        private bool DetermineIsAvailable()
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _auditEntryRepository.IsAvailable();
-            }
+            return _auditEntryRepository.GetMany();
+        }
+    }
+
+    // TODO: Currently used in testing only, not part of the interface, need to add queryable methods to the interface instead
+    internal IEnumerable GetPage(long pageIndex, int pageCount, out long records)
+    {
+        if (_isAvailable.Value == false)
+        {
+            records = 0;
+            return Enumerable.Empty();
+        }
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _auditEntryRepository.GetPage(pageIndex, pageCount, out records);
+        }
+    }
+
+    /// 
+    ///     Determines whether the repository is available.
+    /// 
+    private bool DetermineIsAvailable()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _auditEntryRepository.IsAvailable();
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/BasicAuthService.cs b/src/Umbraco.Core/Services/BasicAuthService.cs
index d81469fac0..02f955bad6 100644
--- a/src/Umbraco.Core/Services/BasicAuthService.cs
+++ b/src/Umbraco.Core/Services/BasicAuthService.cs
@@ -1,4 +1,3 @@
-using System;
 using System.Net;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Options;
@@ -6,58 +5,57 @@ using Microsoft.Extensions.Primitives;
 using Umbraco.Cms.Core.Configuration.Models;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 
-namespace Umbraco.Cms.Core.Services.Implement
+namespace Umbraco.Cms.Core.Services.Implement;
+
+public class BasicAuthService : IBasicAuthService
 {
-    public class BasicAuthService : IBasicAuthService
-    {
-        private readonly IIpAddressUtilities _ipAddressUtilities;
-        private BasicAuthSettings _basicAuthSettings;
+    private readonly IIpAddressUtilities _ipAddressUtilities;
+    private BasicAuthSettings _basicAuthSettings;
 
-        // Scheduled for removal in v12
-        [Obsolete("Please use the contructor that takes an IIpadressUtilities instead")]
-        public BasicAuthService(IOptionsMonitor optionsMonitor)
+    // Scheduled for removal in v12
+    [Obsolete("Please use the contructor that takes an IIpadressUtilities instead")]
+    public BasicAuthService(IOptionsMonitor optionsMonitor)
         : this(optionsMonitor, StaticServiceProvider.Instance.GetRequiredService())
+    {
+        _basicAuthSettings = optionsMonitor.CurrentValue;
+
+        optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
+    }
+
+    public BasicAuthService(IOptionsMonitor optionsMonitor, IIpAddressUtilities ipAddressUtilities)
+    {
+        _ipAddressUtilities = ipAddressUtilities;
+        _basicAuthSettings = optionsMonitor.CurrentValue;
+
+        optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
+    }
+
+    public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled;
+    public bool IsRedirectToLoginPageEnabled() => _basicAuthSettings.RedirectToLoginPage;
+
+    public bool IsIpAllowListed(IPAddress clientIpAddress)
+    {
+        foreach (var allowedIpString in _basicAuthSettings.AllowedIPs)
         {
-            _basicAuthSettings = optionsMonitor.CurrentValue;
-
-            optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
-        }
-
-        public BasicAuthService(IOptionsMonitor optionsMonitor, IIpAddressUtilities ipAddressUtilities)
-        {
-            _ipAddressUtilities = ipAddressUtilities;
-            _basicAuthSettings = optionsMonitor.CurrentValue;
-
-            optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings);
-        }
-
-        public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled;
-        public bool IsRedirectToLoginPageEnabled() => _basicAuthSettings.RedirectToLoginPage;
-
-        public bool IsIpAllowListed(IPAddress clientIpAddress)
-        {
-            foreach (var allowedIpString in _basicAuthSettings.AllowedIPs)
+            if (_ipAddressUtilities.IsAllowListed(clientIpAddress, allowedIpString))
             {
-                if (_ipAddressUtilities.IsAllowListed(clientIpAddress, allowedIpString))
-                {
-                    return true;
-                }
+                return true;
             }
+        }
 
+        return false;
+    }
+
+    public bool HasCorrectSharedSecret(IDictionary headers)
+    {
+        var headerName = _basicAuthSettings.SharedSecret.HeaderName;
+        var sharedSecret = _basicAuthSettings.SharedSecret.Value;
+
+        if (string.IsNullOrWhiteSpace(headerName) || string.IsNullOrWhiteSpace(sharedSecret))
+        {
             return false;
         }
 
-        public bool HasCorrectSharedSecret(IDictionary headers)
-        {
-            var headerName = _basicAuthSettings.SharedSecret.HeaderName;
-            var sharedSecret = _basicAuthSettings.SharedSecret.Value;
-
-            if (string.IsNullOrWhiteSpace(headerName) || string.IsNullOrWhiteSpace(sharedSecret))
-            {
-                return false;
-            }
-
-            return headers.TryGetValue(headerName, out var value) && value.Equals(sharedSecret);
-        }
+        return headers.TryGetValue(headerName, out StringValues value) && value.Equals(sharedSecret);
     }
 }
diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs
index f823406818..f3fe533373 100644
--- a/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs
+++ b/src/Umbraco.Core/Services/Changes/ContentTypeChange.cs
@@ -1,21 +1,17 @@
-using System.Collections.Generic;
-using System.Linq;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services.Changes
+namespace Umbraco.Cms.Core.Services.Changes;
+
+public class ContentTypeChange
+    where TItem : class, IContentTypeComposition
 {
-    public class ContentTypeChange
-        where TItem : class, IContentTypeComposition
+    public ContentTypeChange(TItem item, ContentTypeChangeTypes changeTypes)
     {
-        public ContentTypeChange(TItem item, ContentTypeChangeTypes changeTypes)
-        {
-            Item = item;
-            ChangeTypes = changeTypes;
-        }
-
-        public TItem Item { get; }
-
-        public ContentTypeChangeTypes ChangeTypes { get; set; }
+        Item = item;
+        ChangeTypes = changeTypes;
     }
 
+    public TItem Item { get; }
+
+    public ContentTypeChangeTypes ChangeTypes { get; set; }
 }
diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs
index 9489e52d42..d45a2267bc 100644
--- a/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs
+++ b/src/Umbraco.Core/Services/Changes/ContentTypeChangeExtensions.cs
@@ -1,33 +1,21 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System.Collections.Generic;
-using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+public static class ContentTypeChangeExtensions
 {
-    public static class ContentTypeChangeExtensions
-    {
+    public static bool HasType(this ContentTypeChangeTypes change, ContentTypeChangeTypes type) =>
+        (change & type) != ContentTypeChangeTypes.None;
 
-        public static bool HasType(this ContentTypeChangeTypes change, ContentTypeChangeTypes type)
-        {
-            return (change & type) != ContentTypeChangeTypes.None;
-        }
+    public static bool HasTypesAll(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
+        (change & types) == types;
 
-        public static bool HasTypesAll(this ContentTypeChangeTypes change, ContentTypeChangeTypes types)
-        {
-            return (change & types) == types;
-        }
+    public static bool HasTypesAny(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
+        (change & types) != ContentTypeChangeTypes.None;
 
-        public static bool HasTypesAny(this ContentTypeChangeTypes change, ContentTypeChangeTypes types)
-        {
-            return (change & types) != ContentTypeChangeTypes.None;
-        }
-
-        public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types)
-        {
-            return (change & types) == ContentTypeChangeTypes.None;
-        }
-    }
+    public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
+        (change & types) == ContentTypeChangeTypes.None;
 }
diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs
index cd4965dc2b..4346a278cc 100644
--- a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs
+++ b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs
@@ -1,30 +1,27 @@
-using System;
+namespace Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services.Changes
+[Flags]
+public enum ContentTypeChangeTypes : byte
 {
-    [Flags]
-    public enum ContentTypeChangeTypes : byte
-    {
-        None = 0,
+    None = 0,
 
-        /// 
-        /// Item type has been created, no impact
-        /// 
-        Create = 1,
+    /// 
+    ///     Item type has been created, no impact
+    /// 
+    Create = 1,
 
-        /// 
-        /// Content type changes impact only the Content type being saved
-        /// 
-        RefreshMain = 2,
+    /// 
+    ///     Content type changes impact only the Content type being saved
+    /// 
+    RefreshMain = 2,
 
-        /// 
-        /// Content type changes impacts the content type being saved and others used that are composed of it
-        /// 
-        RefreshOther = 4, // changed, other change
+    /// 
+    ///     Content type changes impacts the content type being saved and others used that are composed of it
+    /// 
+    RefreshOther = 4, // changed, other change
 
-        /// 
-        /// Content type was removed
-        /// 
-        Remove = 8
-    }
+    /// 
+    ///     Content type was removed
+    /// 
+    Remove = 8,
 }
diff --git a/src/Umbraco.Core/Services/Changes/DomainChangeTypes.cs b/src/Umbraco.Core/Services/Changes/DomainChangeTypes.cs
index 25bf48e55a..303461f48f 100644
--- a/src/Umbraco.Core/Services/Changes/DomainChangeTypes.cs
+++ b/src/Umbraco.Core/Services/Changes/DomainChangeTypes.cs
@@ -1,10 +1,9 @@
-namespace Umbraco.Cms.Core.Services.Changes
+namespace Umbraco.Cms.Core.Services.Changes;
+
+public enum DomainChangeTypes : byte
 {
-    public enum DomainChangeTypes : byte
-    {
-        None = 0,
-        RefreshAll = 1,
-        Refresh = 2,
-        Remove = 3
-    }
+    None = 0,
+    RefreshAll = 1,
+    Refresh = 2,
+    Remove = 3,
 }
diff --git a/src/Umbraco.Core/Services/Changes/TreeChange.cs b/src/Umbraco.Core/Services/Changes/TreeChange.cs
index f306a796cc..bb722dce24 100644
--- a/src/Umbraco.Core/Services/Changes/TreeChange.cs
+++ b/src/Umbraco.Core/Services/Changes/TreeChange.cs
@@ -1,36 +1,28 @@
-using System.Collections.Generic;
-using System.Linq;
+namespace Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services.Changes
+public class TreeChange
 {
-    public class TreeChange
+    public TreeChange(TItem changedItem, TreeChangeTypes changeTypes)
     {
-        public TreeChange(TItem changedItem, TreeChangeTypes changeTypes)
+        Item = changedItem;
+        ChangeTypes = changeTypes;
+    }
+
+    public TItem Item { get; }
+
+    public TreeChangeTypes ChangeTypes { get; }
+
+    public EventArgs ToEventArgs() => new EventArgs(this);
+
+    public class EventArgs : System.EventArgs
+    {
+        public EventArgs(IEnumerable> changes) => Changes = changes.ToArray();
+
+        public EventArgs(TreeChange change)
+            : this(new[] { change })
         {
-            Item = changedItem;
-            ChangeTypes = changeTypes;
         }
 
-        public TItem Item { get; }
-        public TreeChangeTypes ChangeTypes { get; }
-
-        public EventArgs ToEventArgs()
-        {
-            return new EventArgs(this);
-        }
-
-        public class EventArgs : System.EventArgs
-        {
-            public EventArgs(IEnumerable> changes)
-            {
-                Changes = changes.ToArray();
-            }
-
-            public EventArgs(TreeChange change)
-                : this(new[] { change })
-            { }
-
-            public IEnumerable> Changes { get; private set; }
-        }
+        public IEnumerable> Changes { get; }
     }
 }
diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs
index 5de6ae9847..1dc972eb7a 100644
--- a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs
+++ b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs
@@ -1,36 +1,23 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+public static class TreeChangeExtensions
 {
-    public static class TreeChangeExtensions
-    {
-        public static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes)
-        {
-            return new TreeChange.EventArgs(changes);
-        }
+    public static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes) =>
+        new TreeChange.EventArgs(changes);
 
-        public static bool HasType(this TreeChangeTypes change, TreeChangeTypes type)
-        {
-            return (change & type) != TreeChangeTypes.None;
-        }
+    public static bool HasType(this TreeChangeTypes change, TreeChangeTypes type) =>
+        (change & type) != TreeChangeTypes.None;
 
-        public static bool HasTypesAll(this TreeChangeTypes change, TreeChangeTypes types)
-        {
-            return (change & types) == types;
-        }
+    public static bool HasTypesAll(this TreeChangeTypes change, TreeChangeTypes types) => (change & types) == types;
 
-        public static bool HasTypesAny(this TreeChangeTypes change, TreeChangeTypes types)
-        {
-            return (change & types) != TreeChangeTypes.None;
-        }
+    public static bool HasTypesAny(this TreeChangeTypes change, TreeChangeTypes types) =>
+        (change & types) != TreeChangeTypes.None;
 
-        public static bool HasTypesNone(this TreeChangeTypes change, TreeChangeTypes types)
-        {
-            return (change & types) == TreeChangeTypes.None;
-        }
-    }
+    public static bool HasTypesNone(this TreeChangeTypes change, TreeChangeTypes types) =>
+        (change & types) == TreeChangeTypes.None;
 }
diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs b/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs
index 9ef231ac06..85db740a56 100644
--- a/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs
+++ b/src/Umbraco.Core/Services/Changes/TreeChangeTypes.cs
@@ -1,25 +1,22 @@
-using System;
+namespace Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services.Changes
+[Flags]
+public enum TreeChangeTypes : byte
 {
-    [Flags]
-    public enum TreeChangeTypes : byte
-    {
-        None = 0,
+    None = 0,
 
-        // all items have been refreshed
-        RefreshAll = 1,
+    // all items have been refreshed
+    RefreshAll = 1,
 
-        // an item node has been refreshed
-        // with only local impact
-        RefreshNode = 2,
+    // an item node has been refreshed
+    // with only local impact
+    RefreshNode = 2,
 
-        // an item node has been refreshed
-        // with branch impact
-        RefreshBranch = 4,
+    // an item node has been refreshed
+    // with branch impact
+    RefreshBranch = 4,
 
-        // an item node has been removed
-        // never to return
-        Remove = 8,
-    }
+    // an item node has been removed
+    // never to return
+    Remove = 8,
 }
diff --git a/src/Umbraco.Core/Services/ConsentService.cs b/src/Umbraco.Core/Services/ConsentService.cs
index d37e2e4d0f..d7bb7af13e 100644
--- a/src/Umbraco.Core/Services/ConsentService.cs
+++ b/src/Umbraco.Core/Services/ConsentService.cs
@@ -1,81 +1,113 @@
-using System;
-using System.Collections.Generic;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Implements .
+/// 
+internal class ConsentService : RepositoryService, IConsentService
 {
+    private readonly IConsentRepository _consentRepository;
+
     /// 
-    /// Implements .
+    ///     Initializes a new instance of the  class.
     /// 
-    internal class ConsentService : RepositoryService, IConsentService
+    public ConsentService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IConsentRepository consentRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _consentRepository = consentRepository;
+
+    /// 
+    public IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null)
     {
-        private readonly IConsentRepository _consentRepository;
-
-        /// 
-        /// Initializes a new instance of the  class.
-        /// 
-        public ConsentService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IConsentRepository consentRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        // prevent stupid states
+        var v = 0;
+        if ((state & ConsentState.Pending) > 0)
         {
-            _consentRepository = consentRepository;
+            v++;
         }
 
-        /// 
-        public IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null)
+        if ((state & ConsentState.Granted) > 0)
         {
-            // prevent stupid states
-            var v = 0;
-            if ((state & ConsentState.Pending) > 0) v++;
-            if ((state & ConsentState.Granted) > 0) v++;
-            if ((state & ConsentState.Revoked) > 0) v++;
-            if (v != 1)
-                throw new ArgumentException("Invalid state.", nameof(state));
-
-            var consent = new Consent
-            {
-                Current = true,
-                Source = source,
-                Context = context,
-                Action = action,
-                CreateDate = DateTime.Now,
-                State = state,
-                Comment = comment
-            };
-
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _consentRepository.ClearCurrent(source, context, action);
-                _consentRepository.Save(consent);
-                scope.Complete();
-            }
-
-            return consent;
+            v++;
         }
 
-        /// 
-        public IEnumerable LookupConsent(string? source = null, string? context = null, string? action = null,
-            bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false,
-            bool includeHistory = false)
+        if ((state & ConsentState.Revoked) > 0)
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
+            v++;
+        }
+
+        if (v != 1)
+        {
+            throw new ArgumentException("Invalid state.", nameof(state));
+        }
+
+        var consent = new Consent
+        {
+            Current = true,
+            Source = source,
+            Context = context,
+            Action = action,
+            CreateDate = DateTime.Now,
+            State = state,
+            Comment = comment,
+        };
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _consentRepository.ClearCurrent(source, context, action);
+            _consentRepository.Save(consent);
+            scope.Complete();
+        }
+
+        return consent;
+    }
+
+    /// 
+    public IEnumerable LookupConsent(
+        string? source = null,
+        string? context = null,
+        string? action = null,
+        bool sourceStartsWith = false,
+        bool contextStartsWith = false,
+        bool actionStartsWith = false,
+        bool includeHistory = false)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query();
+
+            if (string.IsNullOrWhiteSpace(source) == false)
             {
-                var query = Query();
-
-                if (string.IsNullOrWhiteSpace(source) == false)
-                    query = sourceStartsWith ? query.Where(x => x.Source!.StartsWith(source)) : query.Where(x => x.Source == source);
-                if (string.IsNullOrWhiteSpace(context) == false)
-                    query = contextStartsWith ? query.Where(x => x.Context!.StartsWith(context)) : query.Where(x => x.Context == context);
-                if (string.IsNullOrWhiteSpace(action) == false)
-                    query = actionStartsWith ? query.Where(x => x.Action!.StartsWith(action)) : query.Where(x => x.Action == action);
-                if (includeHistory == false)
-                    query = query.Where(x => x.Current);
-
-                return _consentRepository.Get(query);
+                query = sourceStartsWith
+                    ? query.Where(x => x.Source!.StartsWith(source))
+                    : query.Where(x => x.Source == source);
             }
+
+            if (string.IsNullOrWhiteSpace(context) == false)
+            {
+                query = contextStartsWith
+                    ? query.Where(x => x.Context!.StartsWith(context))
+                    : query.Where(x => x.Context == context);
+            }
+
+            if (string.IsNullOrWhiteSpace(action) == false)
+            {
+                query = actionStartsWith
+                    ? query.Where(x => x.Action!.StartsWith(action))
+                    : query.Where(x => x.Action == action);
+            }
+
+            if (includeHistory == false)
+            {
+                query = query.Where(x => x.Current);
+            }
+
+            return _consentRepository.Get(query);
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index a2fa13a346..1cc0081cbb 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Exceptions;
@@ -16,1483 +13,1506 @@ using Umbraco.Cms.Core.Services.Changes;
 using Umbraco.Cms.Core.Strings;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Implements the content service.
+/// 
+public class ContentService : RepositoryService, IContentService
 {
-    /// 
-    ///     Implements the content service.
-    /// 
-    public class ContentService : RepositoryService, IContentService
+    private readonly IAuditRepository _auditRepository;
+    private readonly IContentTypeRepository _contentTypeRepository;
+    private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
+    private readonly IDocumentRepository _documentRepository;
+    private readonly IEntityRepository _entityRepository;
+    private readonly ILanguageRepository _languageRepository;
+    private readonly ILogger _logger;
+    private readonly Lazy _propertyValidationService;
+    private readonly IShortStringHelper _shortStringHelper;
+    private IQuery? _queryNotTrashed;
+
+    #region Constructors
+
+    public ContentService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IDocumentRepository documentRepository,
+        IEntityRepository entityRepository,
+        IAuditRepository auditRepository,
+        IContentTypeRepository contentTypeRepository,
+        IDocumentBlueprintRepository documentBlueprintRepository,
+        ILanguageRepository languageRepository,
+        Lazy propertyValidationService,
+        IShortStringHelper shortStringHelper)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IAuditRepository _auditRepository;
-        private readonly IContentTypeRepository _contentTypeRepository;
-        private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
-        private readonly IDocumentRepository _documentRepository;
-        private readonly IEntityRepository _entityRepository;
-        private readonly ILanguageRepository _languageRepository;
-        private readonly ILogger _logger;
-        private readonly Lazy _propertyValidationService;
-        private readonly IShortStringHelper _shortStringHelper;
-        private IQuery? _queryNotTrashed;
+        _documentRepository = documentRepository;
+        _entityRepository = entityRepository;
+        _auditRepository = auditRepository;
+        _contentTypeRepository = contentTypeRepository;
+        _documentBlueprintRepository = documentBlueprintRepository;
+        _languageRepository = languageRepository;
+        _propertyValidationService = propertyValidationService;
+        _shortStringHelper = shortStringHelper;
+        _logger = loggerFactory.CreateLogger();
+    }
 
-        #region Constructors
+    #endregion
 
-        public ContentService(ICoreScopeProvider provider, ILoggerFactory loggerFactory,
-            IEventMessagesFactory eventMessagesFactory,
-            IDocumentRepository documentRepository, IEntityRepository entityRepository,
-            IAuditRepository auditRepository,
-            IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository,
-            ILanguageRepository languageRepository,
-            Lazy propertyValidationService, IShortStringHelper shortStringHelper)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    #region Static queries
+
+    // lazy-constructed because when the ctor runs, the query factory may not be ready
+    private IQuery QueryNotTrashed =>
+        _queryNotTrashed ??= Query().Where(x => x.Trashed == false);
+
+    #endregion
+
+    #region Rollback
+
+    public OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        // Get the current copy of the node
+        IContent? content = GetById(id);
+
+        // Get the version
+        IContent? version = GetVersion(versionId);
+
+        // Good old null checks
+        if (content == null || version == null || content.Trashed)
         {
-            _documentRepository = documentRepository;
-            _entityRepository = entityRepository;
-            _auditRepository = auditRepository;
-            _contentTypeRepository = contentTypeRepository;
-            _documentBlueprintRepository = documentBlueprintRepository;
-            _languageRepository = languageRepository;
-            _propertyValidationService = propertyValidationService;
-            _shortStringHelper = shortStringHelper;
-            _logger = loggerFactory.CreateLogger();
+            return new OperationResult(OperationResultType.FailedCannot, evtMsgs);
         }
 
-        #endregion
+        // Store the result of doing the save of content for the rollback
+        OperationResult rollbackSaveResult;
 
-        #region Static queries
-
-        // lazy-constructed because when the ctor runs, the query factory may not be ready
-
-        private IQuery QueryNotTrashed =>
-            _queryNotTrashed ??= Query().Where(x => x.Trashed == false);
-
-        #endregion
-
-        #region Rollback
-
-        public OperationResult Rollback(int id, int versionId, string culture = "*",
-            int userId = Constants.Security.SuperUserId)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            // Get the current copy of the node
-            IContent? content = GetById(id);
-
-            // Get the version
-            IContent? version = GetVersion(versionId);
-
-            // Good old null checks
-            if (content == null || version == null || content.Trashed)
+            var rollingBackNotification = new ContentRollingBackNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(rollingBackNotification))
             {
-                return new OperationResult(OperationResultType.FailedCannot, evtMsgs);
-            }
-
-            // Store the result of doing the save of content for the rollback
-            OperationResult rollbackSaveResult;
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var rollingBackNotification = new ContentRollingBackNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(rollingBackNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(evtMsgs);
-                }
-
-                // Copy the changes from the version
-                content.CopyFrom(version, culture);
-
-                // Save the content for the rollback
-                rollbackSaveResult = Save(content, userId);
-
-                // Depending on the save result - is what we log & audit along with what we return
-                if (rollbackSaveResult.Success == false)
-                {
-                    // Log the error/warning
-                    _logger.LogError(
-                        "User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId,
-                        id, versionId);
-                }
-                else
-                {
-                    scope.Notifications.Publish(
-                        new ContentRolledBackNotification(content, evtMsgs).WithStateFrom(rollingBackNotification));
-
-                    // Logging & Audit message
-                    _logger.LogInformation("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'",
-                        userId, id, versionId);
-                    Audit(AuditType.RollBack, userId, id,
-                        $"Content '{content.Name}' was rolled back to version '{versionId}'");
-                }
-
                 scope.Complete();
+                return OperationResult.Cancel(evtMsgs);
             }
 
-            return rollbackSaveResult;
-        }
+            // Copy the changes from the version
+            content.CopyFrom(version, culture);
 
-        #endregion
+            // Save the content for the rollback
+            rollbackSaveResult = Save(content, userId);
 
-        #region Count
-
-        public int CountPublished(string? contentTypeAlias = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            // Depending on the save result - is what we log & audit along with what we return
+            if (rollbackSaveResult.Success == false)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.CountPublished(contentTypeAlias);
+                // Log the error/warning
+                _logger.LogError(
+                    "User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId);
             }
-        }
-
-        public int Count(string? contentTypeAlias = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            else
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Count(contentTypeAlias);
+                scope.Notifications.Publish(
+                    new ContentRolledBackNotification(content, evtMsgs).WithStateFrom(rollingBackNotification));
+
+                // Logging & Audit message
+                _logger.LogInformation("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId);
+                Audit(AuditType.RollBack, userId, id, $"Content '{content.Name}' was rolled back to version '{versionId}'");
             }
+
+            scope.Complete();
         }
 
-        public int CountChildren(int parentId, string? contentTypeAlias = null)
+        return rollbackSaveResult;
+    }
+
+    #endregion
+
+    #region Count
+
+    public int CountPublished(string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountPublished(contentTypeAlias);
+        }
+    }
+
+    public int Count(string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Count(contentTypeAlias);
+        }
+    }
+
+    public int CountChildren(int parentId, string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountChildren(parentId, contentTypeAlias);
+        }
+    }
+
+    public int CountDescendants(int parentId, string? contentTypeAlias = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.CountDescendants(parentId, contentTypeAlias);
+        }
+    }
+
+    #endregion
+
+    #region Permissions
+
+    /// 
+    ///     Used to bulk update the permissions set for a content item. This will replace all permissions
+    ///     assigned to an entity with a list of user id & permission pairs.
+    /// 
+    /// 
+    public void SetPermissions(EntityPermissionSet permissionSet)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.ReplaceContentPermissions(permissionSet);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Assigns a single permission to the current content item for the specified group ids
+    /// 
+    /// 
+    /// 
+    /// 
+    public void SetPermission(IContent entity, char permission, IEnumerable groupIds)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.AssignEntityPermission(entity, permission, groupIds);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Returns implicit/inherited permissions assigned to the content item for all user groups
+    /// 
+    /// 
+    /// 
+    public EntityPermissionCollection GetPermissions(IContent content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPermissionsForEntity(content.Id);
+        }
+    }
+
+    #endregion
+
+    #region Create
+
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Content should based on.
+    /// 
+    /// 
+    ///     Note that using this method will simply return a new IContent without any identity
+    ///     as it has not yet been persisted. It is intended as a shortcut to creating new content objects
+    ///     that does not invoke a save operation against the database.
+    /// 
+    /// Name of the Content object
+    /// Id of Parent for the new Content
+    /// Alias of the 
+    /// Optional id of the user creating the content
+    /// 
+    ///     
+    /// 
+    public IContent Create(string name, Guid parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        IContent? parent = GetById(parentId);
+        return Create(name, parent, contentTypeAlias, userId);
+    }
+
+    /// 
+    ///     Creates an  object of a specified content type.
+    /// 
+    /// 
+    ///     This method simply returns a new, non-persisted, IContent without any identity. It
+    ///     is intended as a shortcut to creating new content objects that does not invoke a save
+    ///     operation against the database.
+    /// 
+    /// The name of the content object.
+    /// The identifier of the parent, or -1.
+    /// The alias of the content type.
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent Create(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        IContentType contentType = GetContentType(contentTypeAlias);
+        return Create(name, parentId, contentType, userId);
+    }
+
+    /// 
+    ///     Creates an  object of a specified content type.
+    /// 
+    /// 
+    ///     This method simply returns a new, non-persisted, IContent without any identity. It
+    ///     is intended as a shortcut to creating new content objects that does not invoke a save
+    ///     operation against the database.
+    /// 
+    /// The name of the content object.
+    /// The identifier of the parent, or -1.
+    /// The content type of the content
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId)
+    {
+        if (contentType is null)
+        {
+            throw new ArgumentException("Content type must be specified", nameof(contentType));
+        }
+
+        IContent? parent = parentId > 0 ? GetById(parentId) : null;
+        if (parentId > 0 && parent is null)
+        {
+            throw new ArgumentException("No content with that id.", nameof(parentId));
+        }
+
+        var content = new Content(name, parentId, contentType, userId);
+
+        return content;
+    }
+
+    /// 
+    ///     Creates an  object of a specified content type, under a parent.
+    /// 
+    /// 
+    ///     This method simply returns a new, non-persisted, IContent without any identity. It
+    ///     is intended as a shortcut to creating new content objects that does not invoke a save
+    ///     operation against the database.
+    /// 
+    /// The name of the content object.
+    /// The parent content object.
+    /// The alias of the content type.
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent Create(string name, IContent? parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        if (parent == null)
+        {
+            throw new ArgumentNullException(nameof(parent));
+        }
+
+        IContentType contentType = GetContentType(contentTypeAlias);
+        if (contentType == null)
+        {
+            throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
+        }
+
+        var content = new Content(name, parent, contentType, userId);
+
+        return content;
+    }
+
+    /// 
+    ///     Creates an  object of a specified content type.
+    /// 
+    /// This method returns a new, persisted, IContent with an identity.
+    /// The name of the content object.
+    /// The identifier of the parent, or -1.
+    /// The alias of the content type.
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            // locking the content tree secures content types too
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            IContentType contentType = GetContentType(contentTypeAlias); // + locks
+            if (contentType == null)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.CountChildren(parentId, contentTypeAlias);
+                throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
             }
-        }
 
-        public int CountDescendants(int parentId, string? contentTypeAlias = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            IContent? parent = parentId > 0 ? GetById(parentId) : null; // + locks
+            if (parentId > 0 && parent == null)
             {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.CountDescendants(parentId, contentTypeAlias);
-            }
-        }
-
-        #endregion
-
-        #region Permissions
-
-        /// 
-        ///     Used to bulk update the permissions set for a content item. This will replace all permissions
-        ///     assigned to an entity with a list of user id & permission pairs.
-        /// 
-        /// 
-        public void SetPermissions(EntityPermissionSet permissionSet)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.ReplaceContentPermissions(permissionSet);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        ///     Assigns a single permission to the current content item for the specified group ids
-        /// 
-        /// 
-        /// 
-        /// 
-        public void SetPermission(IContent entity, char permission, IEnumerable groupIds)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.AssignEntityPermission(entity, permission, groupIds);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        ///     Returns implicit/inherited permissions assigned to the content item for all user groups
-        /// 
-        /// 
-        /// 
-        public EntityPermissionCollection GetPermissions(IContent content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetPermissionsForEntity(content.Id);
-            }
-        }
-
-        #endregion
-
-        #region Create
-
-        /// 
-        ///     Creates an  object using the alias of the 
-        ///     that this Content should based on.
-        /// 
-        /// 
-        ///     Note that using this method will simply return a new IContent without any identity
-        ///     as it has not yet been persisted. It is intended as a shortcut to creating new content objects
-        ///     that does not invoke a save operation against the database.
-        /// 
-        /// Name of the Content object
-        /// Id of Parent for the new Content
-        /// Alias of the 
-        /// Optional id of the user creating the content
-        /// 
-        ///     
-        /// 
-        public IContent Create(string name, Guid parentId, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
-        {
-            // TODO: what about culture?
-
-            IContent? parent = GetById(parentId);
-            return Create(name, parent, contentTypeAlias, userId);
-        }
-
-        /// 
-        ///     Creates an  object of a specified content type.
-        /// 
-        /// 
-        ///     This method simply returns a new, non-persisted, IContent without any identity. It
-        ///     is intended as a shortcut to creating new content objects that does not invoke a save
-        ///     operation against the database.
-        /// 
-        /// The name of the content object.
-        /// The identifier of the parent, or -1.
-        /// The alias of the content type.
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent Create(string name, int parentId, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
-        {
-            // TODO: what about culture?
-
-            IContentType contentType = GetContentType(contentTypeAlias);
-            return Create(name, parentId, contentType, userId);
-        }
-
-        /// 
-        ///     Creates an  object of a specified content type.
-        /// 
-        /// 
-        ///     This method simply returns a new, non-persisted, IContent without any identity. It
-        ///     is intended as a shortcut to creating new content objects that does not invoke a save
-        ///     operation against the database.
-        /// 
-        /// The name of the content object.
-        /// The identifier of the parent, or -1.
-        /// The content type of the content
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent Create(string name, int parentId, IContentType contentType,
-            int userId = Constants.Security.SuperUserId)
-        {
-            if (contentType is null)
-            {
-                throw new ArgumentException("Content type must be specified", nameof(contentType));
+                throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback
             }
 
-            IContent? parent = parentId > 0 ? GetById(parentId) : null;
-            if (parentId > 0 && parent is null)
-            {
-                throw new ArgumentException("No content with that id.", nameof(parentId));
-            }
+            Content content = parentId > 0
+                ? new Content(name, parent!, contentType, userId)
+                : new Content(name, parentId, contentType, userId);
 
-            var content = new Content(name, parentId, contentType, userId);
+            Save(content, userId);
 
             return content;
         }
+    }
 
-        /// 
-        ///     Creates an  object of a specified content type, under a parent.
-        /// 
-        /// 
-        ///     This method simply returns a new, non-persisted, IContent without any identity. It
-        ///     is intended as a shortcut to creating new content objects that does not invoke a save
-        ///     operation against the database.
-        /// 
-        /// The name of the content object.
-        /// The parent content object.
-        /// The alias of the content type.
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent Create(string name, IContent? parent, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
+    /// 
+    ///     Creates an  object of a specified content type, under a parent.
+    /// 
+    /// This method returns a new, persisted, IContent with an identity.
+    /// The name of the content object.
+    /// The parent content object.
+    /// The alias of the content type.
+    /// The optional id of the user creating the content.
+    /// The content object.
+    public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: what about culture?
+        if (parent == null)
         {
-            // TODO: what about culture?
+            throw new ArgumentNullException(nameof(parent));
+        }
 
-            if (parent == null)
-            {
-                throw new ArgumentNullException(nameof(parent));
-            }
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            // locking the content tree secures content types too
+            scope.WriteLock(Constants.Locks.ContentTree);
 
-            IContentType contentType = GetContentType(contentTypeAlias);
+            IContentType contentType = GetContentType(contentTypeAlias); // + locks
             if (contentType == null)
             {
-                throw new ArgumentException("No content type with that alias.",
-                    nameof(contentTypeAlias)); // causes rollback
+                throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
             }
 
             var content = new Content(name, parent, contentType, userId);
 
+            Save(content, userId);
+
             return content;
         }
+    }
 
-        /// 
-        ///     Creates an  object of a specified content type.
-        /// 
-        /// This method returns a new, persisted, IContent with an identity.
-        /// The name of the content object.
-        /// The identifier of the parent, or -1.
-        /// The alias of the content type.
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent CreateAndSave(string name, int parentId, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
+    #endregion
+
+    #region Get, Has, Is
+
+    /// 
+    ///     Gets an  object by Id
+    /// 
+    /// Id of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IContent? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            // TODO: what about culture?
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Get(id);
+        }
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // locking the content tree secures content types too
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                IContentType contentType = GetContentType(contentTypeAlias); // + locks
-                if (contentType == null)
-                {
-                    throw new ArgumentException("No content type with that alias.",
-                        nameof(contentTypeAlias)); // causes rollback
-                }
-
-                IContent? parent = parentId > 0 ? GetById(parentId) : null; // + locks
-                if (parentId > 0 && parent == null)
-                {
-                    throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback
-                }
-
-                Content content = parentId > 0
-                    ? new Content(name, parent!, contentType, userId)
-                    : new Content(name, parentId, contentType, userId);
-
-                Save(content, userId);
-
-                return content;
-            }
+    /// 
+    ///     Gets an  object by Id
+    /// 
+    /// Ids of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IEnumerable GetByIds(IEnumerable ids)
+    {
+        var idsA = ids.ToArray();
+        if (idsA.Length == 0)
+        {
+            return Enumerable.Empty();
         }
 
-        /// 
-        ///     Creates an  object of a specified content type, under a parent.
-        /// 
-        /// This method returns a new, persisted, IContent with an identity.
-        /// The name of the content object.
-        /// The parent content object.
-        /// The alias of the content type.
-        /// The optional id of the user creating the content.
-        /// The content object.
-        public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias,
-            int userId = Constants.Security.SuperUserId)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            // TODO: what about culture?
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IEnumerable items = _documentRepository.GetMany(idsA);
+            var index = items.ToDictionary(x => x.Id, x => x);
+            return idsA.Select(x => index.TryGetValue(x, out IContent? c) ? c : null).WhereNotNull();
+        }
+    }
 
-            if (parent == null)
-            {
-                throw new ArgumentNullException(nameof(parent));
-            }
+    /// 
+    ///     Gets an  object by its 'UniqueId'
+    /// 
+    /// Guid key of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IContent? GetById(Guid key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Get(key);
+        }
+    }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // locking the content tree secures content types too
-                scope.WriteLock(Constants.Locks.ContentTree);
+    /// 
+    public ContentScheduleCollection GetContentScheduleByContentId(int contentId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetContentSchedule(contentId);
+        }
+    }
 
-                IContentType contentType = GetContentType(contentTypeAlias); // + locks
-                if (contentType == null)
-                {
-                    throw new ArgumentException("No content type with that alias.",
-                        nameof(contentTypeAlias)); // causes rollback
-                }
+    /// 
+    public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.PersistContentSchedule(content, contentSchedule);
+        }
+    }
 
-                var content = new Content(name, parent, contentType, userId);
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt IContentServiceBase.Save(IEnumerable contents, int userId) =>
+        Attempt.Succeed(Save(contents, userId));
 
-                Save(content, userId);
-
-                return content;
-            }
+    /// 
+    ///     Gets  objects by Ids
+    /// 
+    /// Ids of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    public IEnumerable GetByIds(IEnumerable ids)
+    {
+        Guid[] idsA = ids.ToArray();
+        if (idsA.Length == 0)
+        {
+            return Enumerable.Empty();
         }
 
-        #endregion
-
-        #region Get, Has, Is
-
-        /// 
-        ///     Gets an  object by Id
-        /// 
-        /// Id of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IContent? GetById(int id)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Get(id);
-            }
-        }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IEnumerable? items = _documentRepository.GetMany(idsA);
 
-        /// 
-        ///     Gets an  object by Id
-        /// 
-        /// Ids of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IEnumerable GetByIds(IEnumerable ids)
-        {
-            var idsA = ids.ToArray();
-            if (idsA.Length == 0)
+            if (items is not null)
             {
-                return Enumerable.Empty();
-            }
+                var index = items.ToDictionary(x => x.Key, x => x);
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IEnumerable items = _documentRepository.GetMany(idsA);
-                var index = items.ToDictionary(x => x.Id, x => x);
                 return idsA.Select(x => index.TryGetValue(x, out IContent? c) ? c : null).WhereNotNull();
             }
+
+            return Enumerable.Empty();
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedOfType(
+        int contentTypeId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        ///     Gets an  object by its 'UniqueId'
-        /// 
-        /// Guid key of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IContent? GetById(Guid key)
+        if (pageSize <= 0)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Get(key);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        /// 
-        public ContentScheduleCollection GetContentScheduleByContentId(int contentId)
+        if (ordering == null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.ContentTree);
-                return _documentRepository.GetContentSchedule(contentId);
-            }
+            ordering = Ordering.By("sortOrder");
         }
 
-        /// 
-        public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.WriteLock(Cms.Core.Constants.Locks.ContentTree);
-                _documentRepository.PersistContentSchedule(content, contentSchedule);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPage(
+                Query()?.Where(x => x.ContentTypeId == contentTypeId),
+                pageIndex,
+                pageSize,
+                out totalRecords,
+                filter,
+                ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        Attempt IContentServiceBase.Save(IEnumerable contents, int userId) =>
-            Attempt.Succeed(Save(contents, userId));
-
-        /// 
-        ///     Gets  objects by Ids
-        /// 
-        /// Ids of the Content to retrieve
-        /// 
-        ///     
-        /// 
-        public IEnumerable GetByIds(IEnumerable ids)
+        if (pageSize <= 0)
         {
-            Guid[] idsA = ids.ToArray();
-            if (idsA.Length == 0)
-            {
-                return Enumerable.Empty();
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
+        }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IEnumerable? items = _documentRepository.GetMany(idsA);
+        if (ordering == null)
+        {
+            ordering = Ordering.By("sortOrder");
+        }
 
-                if (items is not null)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetPage(
+                Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)),
+                pageIndex,
+                pageSize,
+                out totalRecords,
+                filter,
+                ordering);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of  objects by Level
+    /// 
+    /// The level to retrieve Content from
+    /// An Enumerable list of  objects
+    /// Contrary to most methods, this method filters out trashed content items.
+    public IEnumerable GetByLevel(int level)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery? query = Query().Where(x => x.Level == level && x.Trashed == false);
+            return _documentRepository.Get(query);
+        }
+    }
+
+    /// 
+    ///     Gets a specific version of an  item.
+    /// 
+    /// Id of the version to retrieve
+    /// An  item
+    public IContent? GetVersion(int versionId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetVersion(versionId);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of an  objects versions by Id
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    public IEnumerable GetVersions(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetAllVersions(id);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of an  objects versions by Id
+    /// 
+    /// An Enumerable list of  objects
+    public IEnumerable GetVersionsSlim(int id, int skip, int take)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetAllVersionsSlim(id, skip, take);
+        }
+    }
+
+    /// 
+    ///     Gets a list of all version Ids for the given content item ordered so latest is first
+    /// 
+    /// 
+    /// The maximum number of rows to return
+    /// 
+    public IEnumerable GetVersionIds(int id, int maxRows)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _documentRepository.GetVersionIds(id, maxRows);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of  objects, which are ancestors of the current content.
+    /// 
+    /// Id of the  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    public IEnumerable GetAncestors(int id)
+    {
+        // intentionally not locking
+        IContent? content = GetById(id);
+        if (content is null)
+        {
+            return Enumerable.Empty();
+        }
+
+        return GetAncestors(content);
+    }
+
+    /// 
+    ///     Gets a collection of  objects, which are ancestors of the current content.
+    /// 
+    ///  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    public IEnumerable GetAncestors(IContent content)
+    {
+        // null check otherwise we get exceptions
+        if (content.Path.IsNullOrWhiteSpace())
+        {
+            return Enumerable.Empty();
+        }
+
+        var ids = content.GetAncestorIds()?.ToArray();
+        if (ids?.Any() == false)
+        {
+            return new List();
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetMany(ids!);
+        }
+    }
+
+    /// 
+    ///     Gets a collection of published  objects by Parent Id
+    /// 
+    /// Id of the Parent to retrieve Children from
+    /// An Enumerable list of published  objects
+    public IEnumerable GetPublishedChildren(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery? query = Query().Where(x => x.ParentId == id && x.Published);
+            return _documentRepository.Get(query).OrderBy(x => x.SortOrder);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
+    {
+        if (pageIndex < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
+        }
+
+        if (pageSize <= 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
+        }
+
+        if (ordering == null)
+        {
+            ordering = Ordering.By("sortOrder");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+
+            IQuery? query = Query()?.Where(x => x.ParentId == id);
+            return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
+    {
+        if (ordering == null)
+        {
+            ordering = Ordering.By("Path");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+
+            // if the id is System Root, then just get all
+            if (id != Constants.System.Root)
+            {
+                TreeEntityPath[] contentPath =
+                    _entityRepository.GetAllPaths(Constants.ObjectTypes.Document, id).ToArray();
+                if (contentPath.Length == 0)
                 {
-                    var index = items.ToDictionary(x => x.Key, x => x);
-
-                    return idsA.Select(x => index.TryGetValue(x, out IContent? c) ? c : null).WhereNotNull();
+                    totalChildren = 0;
+                    return Enumerable.Empty();
                 }
 
-                return Enumerable.Empty();
+                return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
             }
-        }
 
-        /// 
-        public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize,
-            out long totalRecords
-            , IQuery? filter = null, Ordering? ordering = null)
+            return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
+        }
+    }
+
+    private IQuery? GetPagedDescendantQuery(string contentPath)
+    {
+        IQuery? query = Query();
+        if (!contentPath.IsNullOrWhiteSpace())
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
-
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
-
-            if (ordering == null)
-            {
-                ordering = Ordering.By("sortOrder");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetPage(
-                    Query()?.Where(x => x.ContentTypeId == contentTypeId),
-                    pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
+            query?.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
         }
 
-        /// 
-        public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize,
-            out long totalRecords, IQuery? filter, Ordering? ordering = null)
+        return query;
+    }
+
+    private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize, out long totalChildren, IQuery? filter, Ordering? ordering)
+    {
+        if (pageIndex < 0)
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
-
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
-
-            if (ordering == null)
-            {
-                ordering = Ordering.By("sortOrder");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetPage(
-                    Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)),
-                    pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        ///     Gets a collection of  objects by Level
-        /// 
-        /// The level to retrieve Content from
-        /// An Enumerable list of  objects
-        /// Contrary to most methods, this method filters out trashed content items.
-        public IEnumerable GetByLevel(int level)
+        if (pageSize <= 0)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IQuery? query = Query().Where(x => x.Level == level && x.Trashed == false);
-                return _documentRepository.Get(query);
-            }
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        /// 
-        ///     Gets a specific version of an  item.
-        /// 
-        /// Id of the version to retrieve
-        /// An  item
-        public IContent? GetVersion(int versionId)
+        if (ordering == null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetVersion(versionId);
-            }
+            throw new ArgumentNullException(nameof(ordering));
         }
 
-        /// 
-        ///     Gets a collection of an  objects versions by Id
-        /// 
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetVersions(int id)
+        return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
+    }
+
+    /// 
+    ///     Gets the parent of the current content as an  item.
+    /// 
+    /// Id of the  to retrieve the parent from
+    /// Parent  object
+    public IContent? GetParent(int id)
+    {
+        // intentionally not locking
+        IContent? content = GetById(id);
+        return GetParent(content);
+    }
+
+    /// 
+    ///     Gets the parent of the current content as an  item.
+    /// 
+    ///  to retrieve the parent from
+    /// Parent  object
+    public IContent? GetParent(IContent? content)
+    {
+        if (content?.ParentId == Constants.System.Root || content?.ParentId == Constants.System.RecycleBinContent ||
+            content is null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetAllVersions(id);
-            }
+            return null;
         }
 
-        /// 
-        ///     Gets a collection of an  objects versions by Id
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetVersionsSlim(int id, int skip, int take)
+        return GetById(content.ParentId);
+    }
+
+    /// 
+    ///     Gets a collection of  objects, which reside at the first level / root
+    /// 
+    /// An Enumerable list of  objects
+    public IEnumerable GetRootContent()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetAllVersionsSlim(id, skip, take);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery query = Query().Where(x => x.ParentId == Constants.System.Root);
+            return _documentRepository.Get(query);
         }
+    }
 
-        /// 
-        ///     Gets a list of all version Ids for the given content item ordered so latest is first
-        /// 
-        /// 
-        /// The maximum number of rows to return
-        /// 
-        public IEnumerable GetVersionIds(int id, int maxRows)
+    /// 
+    ///     Gets all published content items
+    /// 
+    /// 
+    internal IEnumerable GetAllPublished()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _documentRepository.GetVersionIds(id, maxRows);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.Get(QueryNotTrashed);
         }
+    }
 
-        /// 
-        ///     Gets a collection of  objects, which are ancestors of the current content.
-        /// 
-        /// Id of the  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        public IEnumerable GetAncestors(int id)
+    /// 
+    public IEnumerable GetContentForExpiration(DateTime date)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            // intentionally not locking
-            IContent? content = GetById(id);
-            if (content is null)
-            {
-                return Enumerable.Empty();
-            }
-
-            return GetAncestors(content);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetContentForExpiration(date);
         }
+    }
 
-        /// 
-        ///     Gets a collection of  objects, which are ancestors of the current content.
-        /// 
-        ///  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        public IEnumerable GetAncestors(IContent content)
+    /// 
+    public IEnumerable GetContentForRelease(DateTime date)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            //null check otherwise we get exceptions
-            if (content.Path.IsNullOrWhiteSpace())
-            {
-                return Enumerable.Empty();
-            }
-
-            var ids = content.GetAncestorIds()?.ToArray();
-            if (ids?.Any() == false)
-            {
-                return new List();
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetMany(ids!);
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.GetContentForRelease(date);
         }
+    }
 
-        /// 
-        ///     Gets a collection of published  objects by Parent Id
-        /// 
-        /// Id of the Parent to retrieve Children from
-        /// An Enumerable list of published  objects
-        public IEnumerable GetPublishedChildren(int id)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IQuery? query = Query().Where(x => x.ParentId == id && x.Published);
-                return _documentRepository.Get(query).OrderBy(x => x.SortOrder);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
-
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
-
-            if (ordering == null)
-            {
-                ordering = Ordering.By("sortOrder");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-
-                IQuery? query = Query()?.Where(x => x.ParentId == id);
-                return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
+    /// 
+    ///     Gets a collection of an  objects, which resides in the Recycle Bin
+    /// 
+    /// An Enumerable list of  objects
+    public IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
             if (ordering == null)
             {
                 ordering = Ordering.By("Path");
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IQuery? query = Query()?
+                .Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix));
+            return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
 
-                //if the id is System Root, then just get all
-                if (id != Constants.System.Root)
-                {
-                    TreeEntityPath[] contentPath =
-                        _entityRepository.GetAllPaths(Constants.ObjectTypes.Document, id).ToArray();
-                    if (contentPath.Length == 0)
-                    {
-                        totalChildren = 0;
-                        return Enumerable.Empty();
-                    }
+    /// 
+    ///     Checks whether an  item has any children
+    /// 
+    /// Id of the 
+    /// True if the content has any children otherwise False
+    public bool HasChildren(int id) => CountChildren(id) > 0;
 
-                    return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize,
-                        out totalChildren, filter, ordering);
-                }
-
-                return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
-            }
+    /// 
+    ///     Checks if the passed in  can be published based on the ancestors publish state.
+    /// 
+    ///  to check if ancestors are published
+    /// True if the Content can be published, otherwise False
+    public bool IsPathPublishable(IContent content)
+    {
+        // fast
+        if (content.ParentId == Constants.System.Root)
+        {
+            return true; // root content is always publishable
         }
 
-        private IQuery? GetPagedDescendantQuery(string contentPath)
+        if (content.Trashed)
         {
-            IQuery? query = Query();
-            if (!contentPath.IsNullOrWhiteSpace())
-            {
-                query?.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
-            }
-
-            return query;
+            return false; // trashed content is never publishable
         }
 
-        private IEnumerable GetPagedLocked(IQuery? query, long pageIndex, int pageSize,
-            out long totalChildren,
-            IQuery? filter, Ordering? ordering)
+        // not trashed and has a parent: publishable if the parent is path-published
+        IContent? parent = GetById(content.ParentId);
+        return parent == null || IsPathPublished(parent);
+    }
+
+    public bool IsPathPublished(IContent? content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.IsPathPublished(content);
+        }
+    }
 
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
+    #endregion
 
-            if (ordering == null)
-            {
-                throw new ArgumentNullException(nameof(ordering));
-            }
+    #region Save, Publish, Unpublish
 
-            return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
+    /// 
+    public OperationResult Save(IContent content, int? userId = null, ContentScheduleCollection? contentSchedule = null)
+    {
+        PublishedState publishedState = content.PublishedState;
+        if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
+        {
+            throw new InvalidOperationException(
+                $"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method.");
         }
 
-        /// 
-        ///     Gets the parent of the current content as an  item.
-        /// 
-        /// Id of the  to retrieve the parent from
-        /// Parent  object
-        public IContent? GetParent(int id)
+        if (content.Name != null && content.Name.Length > 255)
         {
-            // intentionally not locking
-            IContent? content = GetById(id);
-            return GetParent(content);
+            throw new InvalidOperationException(
+                $"Content with the name {content.Name} cannot be more than 255 characters in length.");
         }
 
-        /// 
-        ///     Gets the parent of the current content as an  item.
-        /// 
-        ///  to retrieve the parent from
-        /// Parent  object
-        public IContent? GetParent(IContent? content)
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            if (content?.ParentId == Constants.System.Root || content?.ParentId == Constants.System.RecycleBinContent ||
-                content is null)
+            var savingNotification = new ContentSavingNotification(content, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                return null;
-            }
-
-            return GetById(content.ParentId);
-        }
-
-        /// 
-        ///     Gets a collection of  objects, which reside at the first level / root
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetRootContent()
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IQuery query = Query().Where(x => x.ParentId == Constants.System.Root);
-                return _documentRepository.Get(query);
-            }
-        }
-
-        /// 
-        ///     Gets all published content items
-        /// 
-        /// 
-        internal IEnumerable GetAllPublished()
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.Get(QueryNotTrashed);
-            }
-        }
-
-        /// 
-        public IEnumerable GetContentForExpiration(DateTime date)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetContentForExpiration(date);
-            }
-        }
-
-        /// 
-        public IEnumerable GetContentForRelease(DateTime date)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.GetContentForRelease(date);
-            }
-        }
-
-        /// 
-        ///     Gets a collection of an  objects, which resides in the Recycle Bin
-        /// 
-        /// An Enumerable list of  objects
-        public IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                if (ordering == null)
-                {
-                    ordering = Ordering.By("Path");
-                }
-
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IQuery? query = Query()?
-                    .Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix));
-                return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        ///     Checks whether an  item has any children
-        /// 
-        /// Id of the 
-        /// True if the content has any children otherwise False
-        public bool HasChildren(int id) => CountChildren(id) > 0;
-
-        /// 
-        ///     Checks if the passed in  can be published based on the ancestors publish state.
-        /// 
-        ///  to check if ancestors are published
-        /// True if the Content can be published, otherwise False
-        public bool IsPathPublishable(IContent content)
-        {
-            // fast
-            if (content.ParentId == Constants.System.Root)
-            {
-                return true; // root content is always publishable
-            }
-
-            if (content.Trashed)
-            {
-                return false; // trashed content is never publishable
-            }
-
-            // not trashed and has a parent: publishable if the parent is path-published
-            IContent? parent = GetById(content.ParentId);
-            return parent == null || IsPathPublished(parent);
-        }
-
-        public bool IsPathPublished(IContent? content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.IsPathPublished(content);
-            }
-        }
-
-        #endregion
-
-        #region Save, Publish, Unpublish
-
-        /// 
-        public OperationResult Save(IContent content, int? userId = null,
-            ContentScheduleCollection? contentSchedule = null)
-        {
-            PublishedState publishedState = content.PublishedState;
-            if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
-            {
-                throw new InvalidOperationException(
-                    $"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method.");
-            }
-
-            if (content.Name != null && content.Name.Length > 255)
-            {
-                throw new InvalidOperationException(
-                    $"Content with the name {content.Name} cannot be more than 255 characters in length.");
-            }
-
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingNotification = new ContentSavingNotification(content, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-                userId ??= Constants.Security.SuperUserId;
-
-                if (content.HasIdentity == false)
-                {
-                    content.CreatorId = userId.Value;
-                }
-
-                content.WriterId = userId.Value;
-
-                //track the cultures that have changed
-                List? culturesChanging = content.ContentType.VariesByCulture()
-                    ? content.CultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
-                    : null;
-                // TODO: Currently there's no way to change track which variant properties have changed, we only have change
-                // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
-                // in this particular case, determining which cultures have changed works with the above with names since it will
-                // have always changed if it's been saved in the back office but that's not really fail safe.
-
-                _documentRepository.Save(content);
-
-                if (contentSchedule != null)
-                {
-                    _documentRepository.PersistContentSchedule(content, contentSchedule);
-                }
-
-                scope.Notifications.Publish(
-                    new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification));
-
-                // TODO: we had code here to FORCE that this event can never be suppressed. But that just doesn't make a ton of sense?!
-                // I understand that if its suppressed that the caches aren't updated, but that would be expected. If someone
-                // is supressing events then I think it's expected that nothing will happen. They are probably doing it for perf
-                // reasons like bulk import and in those cases we don't want this occuring.
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, eventMessages));
-
-                if (culturesChanging != null)
-                {
-                    var languages = _languageRepository.GetMany()?
-                        .Where(x => culturesChanging.InvariantContains(x.IsoCode))
-                        .Select(x => x.CultureName);
-                    if (languages is not null)
-                    {
-                        var langs = string.Join(", ", languages);
-                        Audit(AuditType.SaveVariant, userId.Value, content.Id, $"Saved languages: {langs}", langs);
-                    }
-                }
-                else
-                {
-                    Audit(AuditType.Save, userId.Value, content.Id);
-                }
-
                 scope.Complete();
+                return OperationResult.Cancel(eventMessages);
             }
 
-            return OperationResult.Succeed(eventMessages);
-        }
+            scope.WriteLock(Constants.Locks.ContentTree);
+            userId ??= Constants.Security.SuperUserId;
 
-        /// 
-        public OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            IContent[] contentsA = contents.ToArray();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            if (content.HasIdentity == false)
             {
-                var savingNotification = new ContentSavingNotification(contentsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-                foreach (IContent content in contentsA)
-                {
-                    if (content.HasIdentity == false)
-                    {
-                        content.CreatorId = userId;
-                    }
-
-                    content.WriterId = userId;
-
-                    _documentRepository.Save(content);
-                }
-
-                scope.Notifications.Publish(
-                    new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification));
-                // TODO: See note above about supressing events
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(contentsA, TreeChangeTypes.RefreshNode, eventMessages));
-
-                Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content");
-
-                scope.Complete();
+                content.CreatorId = userId.Value;
             }
 
-            return OperationResult.Succeed(eventMessages);
-        }
+            content.WriterId = userId.Value;
 
-        /// 
-        public PublishResult SaveAndPublish(IContent content, string culture = "*",
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            PublishedState publishedState = content.PublishedState;
-            if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
-            {
-                throw new InvalidOperationException(
-                    $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
-            }
-
-            // cannot accept invariant (null or empty) culture for variant content type
-            // cannot accept a specific culture for invariant content type (but '*' is ok)
-            if (content.ContentType.VariesByCulture())
-            {
-                if (culture.IsNullOrWhiteSpace())
-                {
-                    throw new NotSupportedException("Invariant culture is not supported by variant content types.");
-                }
-            }
-            else
-            {
-                if (!culture.IsNullOrWhiteSpace() && culture != "*")
-                {
-                    throw new NotSupportedException(
-                        $"Culture \"{culture}\" is not supported by invariant content types.");
-                }
-            }
-
-            if (content.Name != null && content.Name.Length > 255)
-            {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
-
-                // if culture is specific, first publish the invariant values, then publish the culture itself.
-                // if culture is '*', then publish them all (including variants)
-
-                //this will create the correct culture impact even if culture is * or null
-                var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
-
-                // publish the culture(s)
-                // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
-                content.PublishCulture(impact);
-
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
-        }
-
-        /// 
-        public PublishResult SaveAndPublish(IContent content, string[] cultures,
-            int userId = Constants.Security.SuperUserId)
-        {
-            if (content == null)
-            {
-                throw new ArgumentNullException(nameof(content));
-            }
-
-            if (cultures == null)
-            {
-                throw new ArgumentNullException(nameof(cultures));
-            }
-
-            if (content.Name != null && content.Name.Length > 255)
-            {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                EventMessages evtMsgs = EventMessagesFactory.Get();
-
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
-
-                var varies = content.ContentType.VariesByCulture();
-
-                if (cultures.Length == 0 && !varies)
-                {
-                    //no cultures specified and doesn't vary, so publish it, else nothing to publish
-                    return SaveAndPublish(content, userId: userId);
-                }
-
-                if (cultures.Any(x => x == null || x == "*"))
-                {
-                    throw new InvalidOperationException(
-                        "Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed");
-                }
-
-                IEnumerable impacts =
-                    cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x)));
-
-                // publish the culture(s)
-                // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
-                foreach (CultureImpact impact in impacts)
-                {
-                    content.PublishCulture(impact);
-                }
-
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
-        }
-
-        /// 
-        public PublishResult Unpublish(IContent content, string? culture = "*",
-            int userId = Constants.Security.SuperUserId)
-        {
-            if (content == null)
-            {
-                throw new ArgumentNullException(nameof(content));
-            }
-
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            culture = culture?.NullOrWhiteSpaceAsNull();
-
-            PublishedState publishedState = content.PublishedState;
-            if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
-            {
-                throw new InvalidOperationException(
-                    $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
-            }
-
-            // cannot accept invariant (null or empty) culture for variant content type
-            // cannot accept a specific culture for invariant content type (but '*' is ok)
-            if (content.ContentType.VariesByCulture())
-            {
-                if (culture == null)
-                {
-                    throw new NotSupportedException("Invariant culture is not supported by variant content types.");
-                }
-            }
-            else
-            {
-                if (culture != null && culture != "*")
-                {
-                    throw new NotSupportedException(
-                        $"Culture \"{culture}\" is not supported by invariant content types.");
-                }
-            }
-
-            // if the content is not published, nothing to do
-            if (!content.Published)
-            {
-                return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
-
-                // all cultures = unpublish whole
-                if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
-                {
-                    // It's important to understand that when the document varies by culture but the "*" is used,
-                    // we are just unpublishing the whole document but leaving all of the culture's as-is. This is expected
-                    // because we don't want to actually unpublish every culture and then the document, we just want everything
-                    // to be non-routable so that when it's re-published all variants were as they were.
-
-                    content.PublishedState = PublishedState.Unpublishing;
-                    PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                        savingNotification.State, userId);
-                    scope.Complete();
-                    return result;
-                }
-                else
-                {
-                    // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will
-                    // essentially be re-publishing the document with the requested culture removed.
-                    // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished
-                    // and will then unpublish the document accordingly.
-                    // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist)
-                    var removed = content.UnpublishCulture(culture);
-
-                    //save and publish any changes
-                    PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                        savingNotification.State, userId);
-
-                    scope.Complete();
-
-                    // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures
-                    // were specified to be published which will be the case when removed is false. In that case
-                    // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before).
-                    if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed)
-                    {
-                        return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
-                    }
-
-                    return result;
-                }
-            }
-        }
-
-        /// 
-        ///     Saves a document and publishes/unpublishes any pending publishing changes made to the document.
-        /// 
-        /// 
-        ///     
-        ///         This MUST NOT be called from within this service, this used to be a public API and must only be used outside of
-        ///         this service.
-        ///         Internally in this service, calls must be made to CommitDocumentChangesInternal
-        ///     
-        ///     This is the underlying logic for both publishing and unpublishing any document
-        ///     
-        ///         Pending publishing/unpublishing changes on a document are made with calls to
-        ///          and
-        ///         .
-        ///     
-        ///     
-        ///         When publishing or unpublishing a single culture, or all cultures, use 
-        ///         and . But if the flexibility to both publish and unpublish in a single operation is
-        ///         required
-        ///         then this method needs to be used in combination with 
-        ///         and 
-        ///         on the content itself - this prepares the content, but does not commit anything - and then, invoke
-        ///          to actually commit the changes to the database.
-        ///     
-        ///     The document is *always* saved, even when publishing fails.
-        /// 
-        internal PublishResult CommitDocumentChanges(IContent content,
-            int userId = Constants.Security.SuperUserId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages evtMsgs = EventMessagesFactory.Get();
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var savingNotification = new ContentSavingNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-                }
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs,
-                    savingNotification.State, userId);
-                scope.Complete();
-                return result;
-            }
-        }
-
-        /// 
-        ///     Handles a lot of business logic cases for how the document should be persisted
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        ///     
-        ///         Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for
-        ///         pending scheduled publishing, etc... is dealt with in this method.
-        ///         There is quite a lot of cases to take into account along with logic that needs to deal with scheduled
-        ///         saving/publishing, branch saving/publishing, etc...
-        ///     
-        /// 
-        private PublishResult CommitDocumentChangesInternal(ICoreScope scope, IContent content,
-            EventMessages eventMessages, IReadOnlyCollection allLangs,
-            IDictionary? notificationState,
-            int userId = Constants.Security.SuperUserId,
-            bool branchOne = false, bool branchRoot = false)
-        {
-            if (scope == null)
-            {
-                throw new ArgumentNullException(nameof(scope));
-            }
-
-            if (content == null)
-            {
-                throw new ArgumentNullException(nameof(content));
-            }
-
-            if (eventMessages == null)
-            {
-                throw new ArgumentNullException(nameof(eventMessages));
-            }
-
-            PublishResult? publishResult = null;
-            PublishResult? unpublishResult = null;
-
-            // nothing set = republish it all
-            if (content.PublishedState != PublishedState.Publishing &&
-                content.PublishedState != PublishedState.Unpublishing)
-            {
-                content.PublishedState = PublishedState.Publishing;
-            }
-
-            // State here is either Publishing or Unpublishing
-            // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later
-            var publishing = content.PublishedState == PublishedState.Publishing;
-            var unpublishing = content.PublishedState == PublishedState.Unpublishing;
-
-            var variesByCulture = content.ContentType.VariesByCulture();
-
-            //track cultures that are being published, changed, unpublished
-            IReadOnlyList? culturesPublishing = null;
-            IReadOnlyList? culturesUnpublishing = null;
-            IReadOnlyList? culturesChanging = variesByCulture
+            // track the cultures that have changed
+            List? culturesChanging = content.ContentType.VariesByCulture()
                 ? content.CultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
                 : null;
 
-            var isNew = !content.HasIdentity;
-            TreeChangeTypes changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
-            var previouslyPublished = content.HasIdentity && content.Published;
+            // TODO: Currently there's no way to change track which variant properties have changed, we only have change
+            // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
+            // in this particular case, determining which cultures have changed works with the above with names since it will
+            // have always changed if it's been saved in the back office but that's not really fail safe.
+            _documentRepository.Save(content);
 
-            //inline method to persist the document with the documentRepository since this logic could be called a couple times below
-            void SaveDocument(IContent c)
+            if (contentSchedule != null)
             {
-                // save, always
-                if (c.HasIdentity == false)
-                {
-                    c.CreatorId = userId;
-                }
-
-                c.WriterId = userId;
-
-                // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
-                _documentRepository.Save(c);
+                _documentRepository.PersistContentSchedule(content, contentSchedule);
             }
 
-            if (publishing)
+            scope.Notifications.Publish(
+                new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification));
+
+            // TODO: we had code here to FORCE that this event can never be suppressed. But that just doesn't make a ton of sense?!
+            // I understand that if its suppressed that the caches aren't updated, but that would be expected. If someone
+            // is supressing events then I think it's expected that nothing will happen. They are probably doing it for perf
+            // reasons like bulk import and in those cases we don't want this occuring.
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, eventMessages));
+
+            if (culturesChanging != null)
             {
-                //determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo
-                culturesUnpublishing = content.GetCulturesUnpublishing();
-                culturesPublishing = variesByCulture
-                    ? content.PublishCultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
-                    : null;
-
-                // ensure that the document can be published, and publish handling events, business rules, etc
-                publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ !branchOne || branchRoot,
-                    culturesPublishing, culturesUnpublishing, eventMessages, allLangs, notificationState);
-                if (publishResult.Success)
+                IEnumerable? languages = _languageRepository.GetMany()?
+                    .Where(x => culturesChanging.InvariantContains(x.IsoCode))
+                    .Select(x => x.CultureName);
+                if (languages is not null)
                 {
-                    // note: StrategyPublish flips the PublishedState to Publishing!
-                    publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, eventMessages);
+                    var langs = string.Join(", ", languages);
+                    Audit(AuditType.SaveVariant, userId.Value, content.Id, $"Saved languages: {langs}", langs);
+                }
+            }
+            else
+            {
+                Audit(AuditType.Save, userId.Value, content.Id);
+            }
 
-                    //check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole
-                    if (publishResult.Result == PublishResultType.SuccessUnpublishCulture &&
-                        content.PublishCultureInfos?.Count == 0)
-                    {
-                        // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures
-                        // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that
-                        // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to
-                        // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can
-                        // mark the document for Unpublishing.
-                        SaveDocument(content);
+            scope.Complete();
+        }
 
-                        //set the flag to unpublish and continue
-                        unpublishing = content.Published; // if not published yet, nothing to do
-                    }
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    /// 
+    public OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        IContent[] contentsA = contents.ToArray();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new ContentSavingNotification(contentsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+            foreach (IContent content in contentsA)
+            {
+                if (content.HasIdentity == false)
+                {
+                    content.CreatorId = userId;
+                }
+
+                content.WriterId = userId;
+
+                _documentRepository.Save(content);
+            }
+
+            scope.Notifications.Publish(
+                new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification));
+
+            // TODO: See note above about supressing events
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(contentsA, TreeChangeTypes.RefreshNode, eventMessages));
+
+            Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content");
+
+            scope.Complete();
+        }
+
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    /// 
+    public PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        PublishedState publishedState = content.PublishedState;
+        if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
+        {
+            throw new InvalidOperationException(
+                $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
+        }
+
+        // cannot accept invariant (null or empty) culture for variant content type
+        // cannot accept a specific culture for invariant content type (but '*' is ok)
+        if (content.ContentType.VariesByCulture())
+        {
+            if (culture.IsNullOrWhiteSpace())
+            {
+                throw new NotSupportedException("Invariant culture is not supported by variant content types.");
+            }
+        }
+        else
+        {
+            if (!culture.IsNullOrWhiteSpace() && culture != "*")
+            {
+                throw new NotSupportedException(
+                    $"Culture \"{culture}\" is not supported by invariant content types.");
+            }
+        }
+
+        if (content.Name != null && content.Name.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
+
+            // if culture is specific, first publish the invariant values, then publish the culture itself.
+            // if culture is '*', then publish them all (including variants)
+
+            // this will create the correct culture impact even if culture is * or null
+            var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
+
+            // publish the culture(s)
+            // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
+            content.PublishCulture(impact);
+
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
+
+    /// 
+    public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId)
+    {
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(content));
+        }
+
+        if (cultures == null)
+        {
+            throw new ArgumentNullException(nameof(cultures));
+        }
+
+        if (content.Name != null && content.Name.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
+
+            var varies = content.ContentType.VariesByCulture();
+
+            if (cultures.Length == 0 && !varies)
+            {
+                // No cultures specified and doesn't vary, so publish it, else nothing to publish
+                return SaveAndPublish(content, userId: userId);
+            }
+
+            if (cultures.Any(x => x == null || x == "*"))
+            {
+                throw new InvalidOperationException(
+                    "Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed");
+            }
+
+            IEnumerable impacts =
+                cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x)));
+
+            // publish the culture(s)
+            // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
+            foreach (CultureImpact impact in impacts)
+            {
+                content.PublishCulture(impact);
+            }
+
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
+
+    /// 
+    public PublishResult Unpublish(IContent content, string? culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(content));
+        }
+
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        culture = culture?.NullOrWhiteSpaceAsNull();
+
+        PublishedState publishedState = content.PublishedState;
+        if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
+        {
+            throw new InvalidOperationException(
+                $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
+        }
+
+        // cannot accept invariant (null or empty) culture for variant content type
+        // cannot accept a specific culture for invariant content type (but '*' is ok)
+        if (content.ContentType.VariesByCulture())
+        {
+            if (culture == null)
+            {
+                throw new NotSupportedException("Invariant culture is not supported by variant content types.");
+            }
+        }
+        else
+        {
+            if (culture != null && culture != "*")
+            {
+                throw new NotSupportedException(
+                    $"Culture \"{culture}\" is not supported by invariant content types.");
+            }
+        }
+
+        // if the content is not published, nothing to do
+        if (!content.Published)
+        {
+            return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
+
+            // all cultures = unpublish whole
+            if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
+            {
+                // It's important to understand that when the document varies by culture but the "*" is used,
+                // we are just unpublishing the whole document but leaving all of the culture's as-is. This is expected
+                // because we don't want to actually unpublish every culture and then the document, we just want everything
+                // to be non-routable so that when it's re-published all variants were as they were.
+                content.PublishedState = PublishedState.Unpublishing;
+                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+                scope.Complete();
+                return result;
+            }
+            else
+            {
+                // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will
+                // essentially be re-publishing the document with the requested culture removed.
+                // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished
+                // and will then unpublish the document accordingly.
+                // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist)
+                var removed = content.UnpublishCulture(culture);
+
+                // Save and publish any changes
+                PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+
+                scope.Complete();
+
+                // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures
+                // were specified to be published which will be the case when removed is false. In that case
+                // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before).
+                if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed)
+                {
+                    return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
+                }
+
+                return result;
+            }
+        }
+    }
+
+    /// 
+    ///     Saves a document and publishes/unpublishes any pending publishing changes made to the document.
+    /// 
+    /// 
+    ///     
+    ///         This MUST NOT be called from within this service, this used to be a public API and must only be used outside of
+    ///         this service.
+    ///         Internally in this service, calls must be made to CommitDocumentChangesInternal
+    ///     
+    ///     This is the underlying logic for both publishing and unpublishing any document
+    ///     
+    ///         Pending publishing/unpublishing changes on a document are made with calls to
+    ///          and
+    ///         .
+    ///     
+    ///     
+    ///         When publishing or unpublishing a single culture, or all cultures, use 
+    ///         and . But if the flexibility to both publish and unpublish in a single operation is
+    ///         required
+    ///         then this method needs to be used in combination with 
+    ///         and 
+    ///         on the content itself - this prepares the content, but does not commit anything - and then, invoke
+    ///          to actually commit the changes to the database.
+    ///     
+    ///     The document is *always* saved, even when publishing fails.
+    /// 
+    internal PublishResult CommitDocumentChanges(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var savingNotification = new ContentSavingNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+            }
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
+            scope.Complete();
+            return result;
+        }
+    }
+
+    /// 
+    ///     Handles a lot of business logic cases for how the document should be persisted
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     
+    ///         Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for
+    ///         pending scheduled publishing, etc... is dealt with in this method.
+    ///         There is quite a lot of cases to take into account along with logic that needs to deal with scheduled
+    ///         saving/publishing, branch saving/publishing, etc...
+    ///     
+    /// 
+    private PublishResult CommitDocumentChangesInternal(
+        ICoreScope scope,
+        IContent content,
+        EventMessages eventMessages,
+        IReadOnlyCollection allLangs,
+        IDictionary? notificationState,
+        int userId = Constants.Security.SuperUserId,
+        bool branchOne = false,
+        bool branchRoot = false)
+    {
+        if (scope == null)
+        {
+            throw new ArgumentNullException(nameof(scope));
+        }
+
+        if (content == null)
+        {
+            throw new ArgumentNullException(nameof(content));
+        }
+
+        if (eventMessages == null)
+        {
+            throw new ArgumentNullException(nameof(eventMessages));
+        }
+
+        PublishResult? publishResult = null;
+        PublishResult? unpublishResult = null;
+
+        // nothing set = republish it all
+        if (content.PublishedState != PublishedState.Publishing &&
+            content.PublishedState != PublishedState.Unpublishing)
+        {
+            content.PublishedState = PublishedState.Publishing;
+        }
+
+        // State here is either Publishing or Unpublishing
+        // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later
+        var publishing = content.PublishedState == PublishedState.Publishing;
+        var unpublishing = content.PublishedState == PublishedState.Unpublishing;
+
+        var variesByCulture = content.ContentType.VariesByCulture();
+
+        // Track cultures that are being published, changed, unpublished
+        IReadOnlyList? culturesPublishing = null;
+        IReadOnlyList? culturesUnpublishing = null;
+        IReadOnlyList? culturesChanging = variesByCulture
+            ? content.CultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
+            : null;
+
+        var isNew = !content.HasIdentity;
+        TreeChangeTypes changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
+        var previouslyPublished = content.HasIdentity && content.Published;
+
+        // Inline method to persist the document with the documentRepository since this logic could be called a couple times below
+        void SaveDocument(IContent c)
+        {
+            // save, always
+            if (c.HasIdentity == false)
+            {
+                c.CreatorId = userId;
+            }
+
+            c.WriterId = userId;
+
+            // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
+            _documentRepository.Save(c);
+        }
+
+        if (publishing)
+        {
+            // Determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo
+            culturesUnpublishing = content.GetCulturesUnpublishing();
+            culturesPublishing = variesByCulture
+                ? content.PublishCultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
+                : null;
+
+            // ensure that the document can be published, and publish handling events, business rules, etc
+            publishResult = StrategyCanPublish(
+                scope,
+                content, /*checkPath:*/
+                !branchOne || branchRoot,
+                culturesPublishing,
+                culturesUnpublishing,
+                eventMessages,
+                allLangs,
+                notificationState);
+            if (publishResult.Success)
+            {
+                // note: StrategyPublish flips the PublishedState to Publishing!
+                publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, eventMessages);
+
+                // Check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole
+                if (publishResult.Result == PublishResultType.SuccessUnpublishCulture &&
+                    content.PublishCultureInfos?.Count == 0)
+                {
+                    // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures
+                    // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that
+                    // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to
+                    // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can
+                    // mark the document for Unpublishing.
+                    SaveDocument(content);
+
+                    // Set the flag to unpublish and continue
+                    unpublishing = content.Published; // if not published yet, nothing to do
+                }
+            }
+            else
+            {
+                // in a branch, just give up
+                if (branchOne && !branchRoot)
+                {
+                    return publishResult;
+                }
+
+                // Check for mandatory culture missing, and then unpublish document as a whole
+                if (publishResult.Result == PublishResultType.FailedPublishMandatoryCultureMissing)
+                {
+                    publishing = false;
+                    unpublishing = content.Published; // if not published yet, nothing to do
+
+                    // we may end up in a state where we won't publish nor unpublish
+                    // keep going, though, as we want to save anyways
+                }
+
+                // reset published state from temp values (publishing, unpublishing) to original value
+                // (published, unpublished) in order to save the document, unchanged - yes, this is odd,
+                // but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
+                // PublishState to anything other than Publishing or Unpublishing - which is precisely
+                // what we want to do here - throws
+                content.Published = content.Published;
+            }
+        }
+
+        // won't happen in a branch
+        if (unpublishing)
+        {
+            IContent? newest = GetById(content.Id); // ensure we have the newest version - in scope
+            if (content.VersionId != newest?.VersionId)
+            {
+                return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, eventMessages, content);
+            }
+
+            if (content.Published)
+            {
+                // ensure that the document can be unpublished, and unpublish
+                // handling events, business rules, etc
+                // note: StrategyUnpublish flips the PublishedState to Unpublishing!
+                // note: This unpublishes the entire document (not different variants)
+                unpublishResult = StrategyCanUnpublish(scope, content, eventMessages);
+                if (unpublishResult.Success)
+                {
+                    unpublishResult = StrategyUnpublish(content, eventMessages);
                 }
                 else
                 {
-                    // in a branch, just give up
-                    if (branchOne && !branchRoot)
-                    {
-                        return publishResult;
-                    }
-
-                    //check for mandatory culture missing, and then unpublish document as a whole
-                    if (publishResult.Result == PublishResultType.FailedPublishMandatoryCultureMissing)
-                    {
-                        publishing = false;
-                        unpublishing = content.Published; // if not published yet, nothing to do
-
-                        // we may end up in a state where we won't publish nor unpublish
-                        // keep going, though, as we want to save anyways
-                    }
-
                     // reset published state from temp values (publishing, unpublishing) to original value
                     // (published, unpublished) in order to save the document, unchanged - yes, this is odd,
                     // but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
@@ -1501,733 +1521,1849 @@ namespace Umbraco.Cms.Core.Services
                     content.Published = content.Published;
                 }
             }
-
-            if (unpublishing) // won't happen in a branch
+            else
             {
-                IContent? newest = GetById(content.Id); // ensure we have the newest version - in scope
-                if (content.VersionId != newest?.VersionId)
-                {
-                    return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, eventMessages,
-                        content);
-                }
-
-                if (content.Published)
-                {
-                    // ensure that the document can be unpublished, and unpublish
-                    // handling events, business rules, etc
-                    // note: StrategyUnpublish flips the PublishedState to Unpublishing!
-                    // note: This unpublishes the entire document (not different variants)
-                    unpublishResult = StrategyCanUnpublish(scope, content, eventMessages);
-                    if (unpublishResult.Success)
-                    {
-                        unpublishResult = StrategyUnpublish(content, eventMessages);
-                    }
-                    else
-                    {
-                        // reset published state from temp values (publishing, unpublishing) to original value
-                        // (published, unpublished) in order to save the document, unchanged - yes, this is odd,
-                        // but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
-                        // PublishState to anything other than Publishing or Unpublishing - which is precisely
-                        // what we want to do here - throws
-                        content.Published = content.Published;
-                    }
-                }
-                else
-                {
-                    // already unpublished - optimistic concurrency collision, really,
-                    // and I am not sure at all what we should do, better die fast, else
-                    // we may end up corrupting the db
-                    throw new InvalidOperationException("Concurrency collision.");
-                }
+                // already unpublished - optimistic concurrency collision, really,
+                // and I am not sure at all what we should do, better die fast, else
+                // we may end up corrupting the db
+                throw new InvalidOperationException("Concurrency collision.");
             }
+        }
 
-            //Persist the document
-            SaveDocument(content);
+        // Persist the document
+        SaveDocument(content);
 
-            // raise the Saved event, always
-            scope.Notifications.Publish(
-                new ContentSavedNotification(content, eventMessages).WithState(notificationState));
+        // raise the Saved event, always
+        scope.Notifications.Publish(
+            new ContentSavedNotification(content, eventMessages).WithState(notificationState));
 
-            if (unpublishing) // we have tried to unpublish - won't happen in a branch
+        // we have tried to unpublish - won't happen in a branch
+        if (unpublishing)
+        {
+            // and succeeded, trigger events
+            if (unpublishResult?.Success ?? false)
             {
-                if (unpublishResult?.Success ?? false) // and succeeded, trigger events
+                // events and audit
+                scope.Notifications.Publish(
+                    new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
+                scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
+
+                if (culturesUnpublishing != null)
                 {
-                    // events and audit
-                    scope.Notifications.Publish(
-                        new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
-                    scope.Notifications.Publish(new ContentTreeChangeNotification(content,
-                        TreeChangeTypes.RefreshBranch, eventMessages));
+                    // This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
+                    var langs = string.Join(", ", allLangs
+                        .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
+                        .Select(x => x.CultureName));
+                    Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
 
-                    if (culturesUnpublishing != null)
+                    if (publishResult == null)
                     {
-                        // This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
-
-                        var langs = string.Join(", ", allLangs
-                            .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
-                            .Select(x => x.CultureName));
-                        Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
-
-                        if (publishResult == null)
-                        {
-                            throw new PanicException("publishResult == null - should not happen");
-                        }
-
-                        switch (publishResult.Result)
-                        {
-                            case PublishResultType.FailedPublishMandatoryCultureMissing:
-                                //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
-
-                                //log that the whole content item has been unpublished due to mandatory culture unpublished
-                                Audit(AuditType.Unpublish, userId, content.Id,
-                                    "Unpublished (mandatory language unpublished)");
-                                return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture,
-                                    eventMessages, content);
-                            case PublishResultType.SuccessUnpublishCulture:
-                                //occurs when the last culture is unpublished
-
-                                Audit(AuditType.Unpublish, userId, content.Id,
-                                    "Unpublished (last language unpublished)");
-                                return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, eventMessages,
-                                    content);
-                        }
-                    }
-
-                    Audit(AuditType.Unpublish, userId, content.Id);
-                    return new PublishResult(PublishResultType.SuccessUnpublish, eventMessages, content);
-                }
-
-                // or, failed
-                scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
-                return new PublishResult(PublishResultType.FailedUnpublish, eventMessages, content); // bah
-            }
-
-            if (publishing) // we have tried to publish
-            {
-                if (publishResult?.Success ?? false) // and succeeded, trigger events
-                {
-                    if (isNew == false && previouslyPublished == false)
-                    {
-                        changeType = TreeChangeTypes.RefreshBranch; // whole branch
-                    }
-                    else if (isNew == false && previouslyPublished)
-                    {
-                        changeType = TreeChangeTypes.RefreshNode; // single node
-                    }
-
-
-                    // invalidate the node/branch
-                    if (!branchOne) // for branches, handled by SaveAndPublishBranch
-                    {
-                        scope.Notifications.Publish(
-                            new ContentTreeChangeNotification(content, changeType, eventMessages));
-                        scope.Notifications.Publish(
-                            new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
-                    }
-
-                    // it was not published and now is... descendants that were 'published' (but
-                    // had an unpublished ancestor) are 're-published' ie not explicitly published
-                    // but back as 'published' nevertheless
-                    if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id))
-                    {
-                        IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray();
-                        scope.Notifications.Publish(
-                            new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState));
+                        throw new PanicException("publishResult == null - should not happen");
                     }
 
                     switch (publishResult.Result)
                     {
-                        case PublishResultType.SuccessPublish:
-                            Audit(AuditType.Publish, userId, content.Id);
-                            break;
-                        case PublishResultType.SuccessPublishCulture:
-                            if (culturesPublishing != null)
-                            {
-                                var langs = string.Join(", ", allLangs
-                                    .Where(x => culturesPublishing.InvariantContains(x.IsoCode))
-                                    .Select(x => x.CultureName));
-                                Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}",
-                                    langs);
-                            }
+                        case PublishResultType.FailedPublishMandatoryCultureMissing:
+                            // Occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
 
-                            break;
+                            // Log that the whole content item has been unpublished due to mandatory culture unpublished
+                            Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)");
+                            return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, eventMessages, content);
                         case PublishResultType.SuccessUnpublishCulture:
-                            if (culturesUnpublishing != null)
-                            {
-                                var langs = string.Join(", ", allLangs
-                                    .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
-                                    .Select(x => x.CultureName));
-                                Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}",
-                                    langs);
-                            }
-
-                            break;
+                            // Occurs when the last culture is unpublished
+                            Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)");
+                            return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, eventMessages, content);
                     }
-
-                    return publishResult;
                 }
-            }
 
-            // should not happen
-            if (branchOne && !branchRoot)
-            {
-                throw new PanicException("branchOne && !branchRoot - should not happen");
-            }
-
-            //if publishing didn't happen or if it has failed, we still need to log which cultures were saved
-            if (!branchOne && (publishResult == null || !publishResult.Success))
-            {
-                if (culturesChanging != null)
-                {
-                    var langs = string.Join(", ", allLangs
-                        .Where(x => culturesChanging.InvariantContains(x.IsoCode))
-                        .Select(x => x.CultureName));
-                    Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs);
-                }
-                else
-                {
-                    Audit(AuditType.Save, userId, content.Id);
-                }
+                Audit(AuditType.Unpublish, userId, content.Id);
+                return new PublishResult(PublishResultType.SuccessUnpublish, eventMessages, content);
             }
 
             // or, failed
             scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
-            return publishResult!;
+            return new PublishResult(PublishResultType.FailedUnpublish, eventMessages, content); // bah
         }
 
-        /// 
-        public IEnumerable PerformScheduledPublish(DateTime date)
+        // we have tried to publish
+        if (publishing)
         {
-            var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList());
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-            var results = new List();
-
-            PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
-            PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
-
-            return results;
-        }
-
-        private void PerformScheduledPublishingExpiration(DateTime date, List results,
-            EventMessages evtMsgs, Lazy> allLangs)
-        {
-            using ICoreScope scope = ScopeProvider.CreateCoreScope();
-
-            // do a fast read without any locks since this executes often to see if we even need to proceed
-            if (_documentRepository.HasContentForExpiration(date))
+            // and succeeded, trigger events
+            if (publishResult?.Success ?? false)
             {
-                // now take a write lock since we'll be updating
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                foreach (IContent d in _documentRepository.GetContentForExpiration(date))
+                if (isNew == false && previouslyPublished == false)
                 {
-                    ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
-                    if (d.ContentType.VariesByCulture())
-                    {
-                        //find which cultures have pending schedules
-                        var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Expire, date)
-                            .Select(x => x.Culture)
-                            .Distinct()
-                            .ToList();
-
-                        if (pendingCultures.Count == 0)
-                        {
-                            continue; //shouldn't happen but no point in processing this document if there's nothing there
-                        }
-
-                        var savingNotification = new ContentSavingNotification(d, evtMsgs);
-                        if (scope.Notifications.PublishCancelable(savingNotification))
-                        {
-                            results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
-                            continue;
-                        }
-
-                        foreach (var c in pendingCultures)
-                        {
-                            //Clear this schedule for this culture
-                            contentSchedule.Clear(c, ContentScheduleAction.Expire, date);
-                            //set the culture to be published
-                            d.UnpublishCulture(c);
-                        }
-
-                        _documentRepository.PersistContentSchedule(d, contentSchedule);
-                        PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value,
-                            savingNotification.State, d.WriterId);
-                        if (result.Success == false)
-                        {
-                            _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id,
-                                result.Result);
-                        }
-
-                        results.Add(result);
-                    }
-                    else
-                    {
-                        //Clear this schedule for this culture
-                        contentSchedule.Clear(ContentScheduleAction.Expire, date);
-                        _documentRepository.PersistContentSchedule(d, contentSchedule);
-                        PublishResult result = Unpublish(d, userId: d.WriterId);
-                        if (result.Success == false)
-                        {
-                            _logger.LogError(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.",
-                                d.Id, result.Result);
-                        }
-
-                        results.Add(result);
-                    }
+                    changeType = TreeChangeTypes.RefreshBranch; // whole branch
+                }
+                else if (isNew == false && previouslyPublished)
+                {
+                    changeType = TreeChangeTypes.RefreshNode; // single node
                 }
 
-                _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
-            }
+                // invalidate the node/branch
+                // for branches, handled by SaveAndPublishBranch
+                if (!branchOne)
+                {
+                    scope.Notifications.Publish(
+                        new ContentTreeChangeNotification(content, changeType, eventMessages));
+                    scope.Notifications.Publish(
+                        new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
+                }
 
-            scope.Complete();
+                // it was not published and now is... descendants that were 'published' (but
+                // had an unpublished ancestor) are 're-published' ie not explicitly published
+                // but back as 'published' nevertheless
+                if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id))
+                {
+                    IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray();
+                    scope.Notifications.Publish(
+                        new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState));
+                }
+
+                switch (publishResult.Result)
+                {
+                    case PublishResultType.SuccessPublish:
+                        Audit(AuditType.Publish, userId, content.Id);
+                        break;
+                    case PublishResultType.SuccessPublishCulture:
+                        if (culturesPublishing != null)
+                        {
+                            var langs = string.Join(", ", allLangs
+                                .Where(x => culturesPublishing.InvariantContains(x.IsoCode))
+                                .Select(x => x.CultureName));
+                            Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs);
+                        }
+
+                        break;
+                    case PublishResultType.SuccessUnpublishCulture:
+                        if (culturesUnpublishing != null)
+                        {
+                            var langs = string.Join(", ", allLangs
+                                .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
+                                .Select(x => x.CultureName));
+                            Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
+                        }
+
+                        break;
+                }
+
+                return publishResult;
+            }
         }
 
-        private void PerformScheduledPublishingRelease(DateTime date, List results,
-            EventMessages evtMsgs, Lazy> allLangs)
+        // should not happen
+        if (branchOne && !branchRoot)
         {
-            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            throw new PanicException("branchOne && !branchRoot - should not happen");
+        }
 
-            // do a fast read without any locks since this executes often to see if we even need to proceed
-            if (_documentRepository.HasContentForRelease(date))
+        // if publishing didn't happen or if it has failed, we still need to log which cultures were saved
+        if (!branchOne && (publishResult == null || !publishResult.Success))
+        {
+            if (culturesChanging != null)
             {
-                // now take a write lock since we'll be updating
-                scope.WriteLock(Constants.Locks.ContentTree);
+                var langs = string.Join(", ", allLangs
+                    .Where(x => culturesChanging.InvariantContains(x.IsoCode))
+                    .Select(x => x.CultureName));
+                Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs);
+            }
+            else
+            {
+                Audit(AuditType.Save, userId, content.Id);
+            }
+        }
 
-                foreach (IContent d in _documentRepository.GetContentForRelease(date))
+        // or, failed
+        scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
+        return publishResult!;
+    }
+
+    /// 
+    public IEnumerable PerformScheduledPublish(DateTime date)
+    {
+        var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList());
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+        var results = new List();
+
+        PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
+        PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
+
+        return results;
+    }
+
+    private void PerformScheduledPublishingExpiration(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+
+        // do a fast read without any locks since this executes often to see if we even need to proceed
+        if (_documentRepository.HasContentForExpiration(date))
+        {
+            // now take a write lock since we'll be updating
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            foreach (IContent d in _documentRepository.GetContentForExpiration(date))
+            {
+                ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
+                if (d.ContentType.VariesByCulture())
                 {
-                    ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
-                    if (d.ContentType.VariesByCulture())
+                    // find which cultures have pending schedules
+                    var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Expire, date)
+                        .Select(x => x.Culture)
+                        .Distinct()
+                        .ToList();
+
+                    if (pendingCultures.Count == 0)
                     {
-                        //find which cultures have pending schedules
-                        var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Release, date)
-                            .Select(x => x.Culture)
-                            .Distinct()
-                            .ToList();
+                        continue; // shouldn't happen but no point in processing this document if there's nothing there
+                    }
 
-                        if (pendingCultures.Count == 0)
-                        {
-                            continue; //shouldn't happen but no point in processing this document if there's nothing there
-                        }
+                    var savingNotification = new ContentSavingNotification(d, evtMsgs);
+                    if (scope.Notifications.PublishCancelable(savingNotification))
+                    {
+                        results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
+                        continue;
+                    }
 
-                        var savingNotification = new ContentSavingNotification(d, evtMsgs);
-                        if (scope.Notifications.PublishCancelable(savingNotification))
-                        {
-                            results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
-                            continue;
-                        }
+                    foreach (var c in pendingCultures)
+                    {
+                        // Clear this schedule for this culture
+                        contentSchedule.Clear(c, ContentScheduleAction.Expire, date);
 
-                        var publishing = true;
-                        foreach (var culture in pendingCultures)
-                        {
-                            //Clear this schedule for this culture
-                            contentSchedule.Clear(culture, ContentScheduleAction.Release, date);
+                        // set the culture to be published
+                        d.UnpublishCulture(c);
+                    }
 
-                            if (d.Trashed)
-                            {
-                                continue; // won't publish
-                            }
+                    _documentRepository.PersistContentSchedule(d, contentSchedule);
+                    PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
+                    }
 
-                            //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
-                            IProperty[]? invalidProperties = null;
-                            var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture));
-                            var tryPublish = d.PublishCulture(impact) &&
-                                             _propertyValidationService.Value.IsPropertyDataValid(d,
-                                                 out invalidProperties, impact);
-                            if (invalidProperties != null && invalidProperties.Length > 0)
-                            {
-                                _logger.LogWarning(
-                                    "Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
-                                    d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias)));
-                            }
+                    results.Add(result);
+                }
+                else
+                {
+                    // Clear this schedule for this culture
+                    contentSchedule.Clear(ContentScheduleAction.Expire, date);
+                    _documentRepository.PersistContentSchedule(d, contentSchedule);
+                    PublishResult result = Unpublish(d, userId: d.WriterId);
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
+                    }
 
-                            publishing &= tryPublish; //set the culture to be published
-                            if (!publishing)
-                            {
-                                continue;
-                            }
-                        }
+                    results.Add(result);
+                }
+            }
 
-                        PublishResult result;
+            _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
+        }
+
+        scope.Complete();
+    }
+
+    private void PerformScheduledPublishingRelease(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+
+        // do a fast read without any locks since this executes often to see if we even need to proceed
+        if (_documentRepository.HasContentForRelease(date))
+        {
+            // now take a write lock since we'll be updating
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            foreach (IContent d in _documentRepository.GetContentForRelease(date))
+            {
+                ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
+                if (d.ContentType.VariesByCulture())
+                {
+                    // find which cultures have pending schedules
+                    var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Release, date)
+                        .Select(x => x.Culture)
+                        .Distinct()
+                        .ToList();
+
+                    if (pendingCultures.Count == 0)
+                    {
+                        continue; // shouldn't happen but no point in processing this document if there's nothing there
+                    }
+
+                    var savingNotification = new ContentSavingNotification(d, evtMsgs);
+                    if (scope.Notifications.PublishCancelable(savingNotification))
+                    {
+                        results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
+                        continue;
+                    }
+
+                    var publishing = true;
+                    foreach (var culture in pendingCultures)
+                    {
+                        // Clear this schedule for this culture
+                        contentSchedule.Clear(culture, ContentScheduleAction.Release, date);
 
                         if (d.Trashed)
                         {
-                            result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
-                        }
-                        else if (!publishing)
-                        {
-                            result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
-                        }
-                        else
-                        {
-                            _documentRepository.PersistContentSchedule(d, contentSchedule);
-                            result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value,
-                                savingNotification.State, d.WriterId);
+                            continue; // won't publish
                         }
 
-                        if (result.Success == false)
+                        // publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
+                        IProperty[]? invalidProperties = null;
+                        var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture));
+                        var tryPublish = d.PublishCulture(impact) &&
+                                         _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
+                        if (invalidProperties != null && invalidProperties.Length > 0)
                         {
-                            _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id,
-                                result.Result);
+                            _logger.LogWarning(
+                                "Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
+                                d.Id,
+                                culture,
+                                string.Join(",", invalidProperties.Select(x => x.Alias)));
                         }
 
-                        results.Add(result);
+                        publishing &= tryPublish; // set the culture to be published
+                        if (!publishing)
+                        {
+                        }
+                    }
+
+                    PublishResult result;
+
+                    if (d.Trashed)
+                    {
+                        result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
+                    }
+                    else if (!publishing)
+                    {
+                        result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
                     }
                     else
                     {
-                        //Clear this schedule
-                        contentSchedule.Clear(ContentScheduleAction.Release, date);
-
-                        PublishResult? result = null;
-
-                        if (d.Trashed)
-                        {
-                            result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
-                        }
-                        else
-                        {
-                            _documentRepository.PersistContentSchedule(d, contentSchedule);
-                            result = SaveAndPublish(d, userId: d.WriterId);
-                        }
-
-                        if (result.Success == false)
-                        {
-                            _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id,
-                                result.Result);
-                        }
-
-                        results.Add(result);
+                        _documentRepository.PersistContentSchedule(d, contentSchedule);
+                        result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
                     }
-                }
 
-                _documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
+                    }
+
+                    results.Add(result);
+                }
+                else
+                {
+                    // Clear this schedule
+                    contentSchedule.Clear(ContentScheduleAction.Release, date);
+
+                    PublishResult? result = null;
+
+                    if (d.Trashed)
+                    {
+                        result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
+                    }
+                    else
+                    {
+                        _documentRepository.PersistContentSchedule(d, contentSchedule);
+                        result = SaveAndPublish(d, userId: d.WriterId);
+                    }
+
+                    if (result.Success == false)
+                    {
+                        _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
+                    }
+
+                    results.Add(result);
+                }
             }
 
-            scope.Complete();
+            _documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
         }
 
-        // utility 'PublishCultures' func used by SaveAndPublishBranch
-        private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish,
-            IReadOnlyCollection allLangs)
+        scope.Complete();
+    }
+
+    // utility 'PublishCultures' func used by SaveAndPublishBranch
+    private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs)
+    {
+        // TODO: Th is does not support being able to return invalid property details to bubble up to the UI
+
+        // variant content type - publish specified cultures
+        // invariant content type - publish only the invariant culture
+        if (content.ContentType.VariesByCulture())
         {
-            //TODO: This does not support being able to return invalid property details to bubble up to the UI
-
-            // variant content type - publish specified cultures
-            // invariant content type - publish only the invariant culture
-            if (content.ContentType.VariesByCulture())
+            return culturesToPublish.All(culture =>
             {
-                return culturesToPublish.All(culture =>
-                {
-                    var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
-                    return content.PublishCulture(impact) &&
-                           _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
-                });
-            }
-
-            return content.PublishCulture(CultureImpact.Invariant)
-                   && _propertyValidationService.Value.IsPropertyDataValid(content, out _, CultureImpact.Invariant);
+                var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content);
+                return content.PublishCulture(impact) &&
+                       _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
+            });
         }
 
-        // utility 'ShouldPublish' func used by SaveAndPublishBranch
-        private HashSet? SaveAndPublishBranch_ShouldPublish(ref HashSet? cultures, string c,
-            bool published, bool edited, bool isRoot, bool force)
+        return content.PublishCulture(CultureImpact.Invariant)
+               && _propertyValidationService.Value.IsPropertyDataValid(content, out _, CultureImpact.Invariant);
+    }
+
+    // utility 'ShouldPublish' func used by SaveAndPublishBranch
+    private HashSet? SaveAndPublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, bool force)
+    {
+        // if published, republish
+        if (published)
         {
-            // if published, republish
-            if (published)
-            {
-                if (cultures == null)
-                {
-                    cultures = new HashSet(); // empty means 'already published'
-                }
-
-                if (edited)
-                {
-                    cultures.Add(c); //  means 'republish this culture'
-                }
-
-                return cultures;
-            }
-
-            // if not published, publish if force/root else do nothing
-            if (!force && !isRoot)
-            {
-                return cultures; // null means 'nothing to do'
-            }
-
             if (cultures == null)
             {
-                cultures = new HashSet();
+                cultures = new HashSet(); // empty means 'already published'
+            }
+
+            if (edited)
+            {
+                cultures.Add(c); //  means 'republish this culture'
             }
 
-            cultures.Add(c); //  means 'publish this culture'
             return cultures;
         }
 
-        /// 
-        public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*",
-            int userId = Constants.Security.SuperUserId)
+        // if not published, publish if force/root else do nothing
+        if (!force && !isRoot)
         {
-            // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
-            // and not to == them, else we would be comparing references, and that is a bad thing
-
-            // determines whether the document is edited, and thus needs to be published,
-            // for the specified culture (it may be edited for other cultures and that
-            // should not trigger a publish).
-
-            // determines cultures to be published
-            // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
-            HashSet? ShouldPublish(IContent c)
-            {
-                var isRoot = c.Id == content.Id;
-                HashSet? culturesToPublish = null;
-
-                if (!c.ContentType.VariesByCulture()) // invariant content type
-                {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot,
-                        force);
-                }
-
-                if (culture != "*") // variant content type, specific culture
-                {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture,
-                        c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
-                }
-
-                // variant content type, all cultures
-                if (c.Published)
-                {
-                    // then some (and maybe all) cultures will be 'already published' (unless forcing),
-                    // others will have to 'republish this culture'
-                    foreach (var x in c.AvailableCultures)
-                    {
-                        SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x),
-                            c.IsCultureEdited(x), isRoot, force);
-                    }
-
-                    return culturesToPublish;
-                }
-
-                // if not published, publish if force/root else do nothing
-                return force || isRoot
-                    ? new HashSet {"*"} // "*" means 'publish all'
-                    : null; // null means 'nothing to do'
-            }
-
-            return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+            return cultures; // null means 'nothing to do'
         }
 
-        /// 
-        public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures,
-            int userId = Constants.Security.SuperUserId)
+        if (cultures == null)
         {
-            // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
-            // and not to == them, else we would be comparing references, and that is a bad thing
-
-            cultures = cultures ?? Array.Empty();
-
-            // determines cultures to be published
-            // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
-            HashSet? ShouldPublish(IContent c)
-            {
-                var isRoot = c.Id == content.Id;
-                HashSet? culturesToPublish = null;
-
-                if (!c.ContentType.VariesByCulture()) // invariant content type
-                {
-                    return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot,
-                        force);
-                }
-
-                // variant content type, specific cultures
-                if (c.Published)
-                {
-                    // then some (and maybe all) cultures will be 'already published' (unless forcing),
-                    // others will have to 'republish this culture'
-                    foreach (var x in cultures)
-                    {
-                        SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x),
-                            c.IsCultureEdited(x), isRoot, force);
-                    }
-
-                    return culturesToPublish;
-                }
-
-                // if not published, publish if force/root else do nothing
-                return force || isRoot
-                    ? new HashSet(cultures) // means 'publish specified cultures'
-                    : null; // null means 'nothing to do'
-            }
-
-            return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+            cultures = new HashSet();
         }
 
-        internal IEnumerable SaveAndPublishBranch(IContent document, bool force,
-            Func?> shouldPublish,
-            Func, IReadOnlyCollection, bool> publishCultures,
-            int userId = Constants.Security.SuperUserId)
+        cultures.Add(c); //  means 'publish this culture'
+        return cultures;
+    }
+
+    /// 
+    public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId)
+    {
+        // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
+        // and not to == them, else we would be comparing references, and that is a bad thing
+
+        // determines whether the document is edited, and thus needs to be published,
+        // for the specified culture (it may be edited for other cultures and that
+        // should not trigger a publish).
+
+        // determines cultures to be published
+        // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
+        HashSet? ShouldPublish(IContent c)
         {
-            if (shouldPublish == null)
+            var isRoot = c.Id == content.Id;
+            HashSet? culturesToPublish = null;
+
+            // invariant content type
+            if (!c.ContentType.VariesByCulture())
             {
-                throw new ArgumentNullException(nameof(shouldPublish));
+                return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
             }
 
-            if (publishCultures == null)
+            // variant content type, specific culture
+            if (culture != "*")
             {
-                throw new ArgumentNullException(nameof(publishCultures));
+                return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
             }
 
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            var results = new List();
-            var publishedDocuments = new List();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            // variant content type, all cultures
+            if (c.Published)
             {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var allLangs = _languageRepository.GetMany().ToList();
-
-                if (!document.HasIdentity)
+                // then some (and maybe all) cultures will be 'already published' (unless forcing),
+                // others will have to 'republish this culture'
+                foreach (var x in c.AvailableCultures)
                 {
-                    throw new InvalidOperationException("Cannot not branch-publish a new document.");
+                    SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
                 }
 
-                PublishedState publishedState = document.PublishedState;
-                if (publishedState == PublishedState.Publishing)
+                return culturesToPublish;
+            }
+
+            // if not published, publish if force/root else do nothing
+            return force || isRoot
+                ? new HashSet { "*" } // "*" means 'publish all'
+                : null; // null means 'nothing to do'
+        }
+
+        return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+    }
+
+    /// 
+    public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId)
+    {
+        // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
+        // and not to == them, else we would be comparing references, and that is a bad thing
+        cultures = cultures ?? Array.Empty();
+
+        // determines cultures to be published
+        // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
+        HashSet? ShouldPublish(IContent c)
+        {
+            var isRoot = c.Id == content.Id;
+            HashSet? culturesToPublish = null;
+
+            // invariant content type
+            if (!c.ContentType.VariesByCulture())
+            {
+                return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
+            }
+
+            // variant content type, specific cultures
+            if (c.Published)
+            {
+                // then some (and maybe all) cultures will be 'already published' (unless forcing),
+                // others will have to 'republish this culture'
+                foreach (var x in cultures)
                 {
-                    throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
+                    SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
                 }
 
-                // deal with the branch root - if it fails, abort
-                PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true,
-                    publishedDocuments, eventMessages, userId, allLangs);
-                if (result != null)
+                return culturesToPublish;
+            }
+
+            // if not published, publish if force/root else do nothing
+            return force || isRoot
+                ? new HashSet(cultures) // means 'publish specified cultures'
+                : null; // null means 'nothing to do'
+        }
+
+        return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
+    }
+
+    internal IEnumerable SaveAndPublishBranch(
+        IContent document,
+        bool force,
+        Func?> shouldPublish,
+        Func, IReadOnlyCollection, bool> publishCultures,
+        int userId = Constants.Security.SuperUserId)
+    {
+        if (shouldPublish == null)
+        {
+            throw new ArgumentNullException(nameof(shouldPublish));
+        }
+
+        if (publishCultures == null)
+        {
+            throw new ArgumentNullException(nameof(publishCultures));
+        }
+
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        var results = new List();
+        var publishedDocuments = new List();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var allLangs = _languageRepository.GetMany().ToList();
+
+            if (!document.HasIdentity)
+            {
+                throw new InvalidOperationException("Cannot not branch-publish a new document.");
+            }
+
+            PublishedState publishedState = document.PublishedState;
+            if (publishedState == PublishedState.Publishing)
+            {
+                throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
+            }
+
+            // deal with the branch root - if it fails, abort
+            PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs);
+            if (result != null)
+            {
+                results.Add(result);
+                if (!result.Success)
                 {
-                    results.Add(result);
-                    if (!result.Success)
+                    return results;
+                }
+            }
+
+            // deal with descendants
+            // if one fails, abort its branch
+            var exclude = new HashSet();
+
+            int count;
+            var page = 0;
+            const int pageSize = 100;
+            do
+            {
+                count = 0;
+
+                // important to order by Path ASC so make it explicit in case defaults change
+                // ReSharper disable once RedundantArgumentDefaultValue
+                foreach (IContent d in GetPagedDescendants(document.Id, page, pageSize, out _, ordering: Ordering.By("Path", Direction.Ascending)))
+                {
+                    count++;
+
+                    // if parent is excluded, exclude child too
+                    if (exclude.Contains(d.ParentId))
                     {
-                        return results;
+                        exclude.Add(d.Id);
+                        continue;
                     }
-                }
 
-                // deal with descendants
-                // if one fails, abort its branch
-                var exclude = new HashSet();
-
-                int count;
-                var page = 0;
-                const int pageSize = 100;
-                do
-                {
-                    count = 0;
-                    // important to order by Path ASC so make it explicit in case defaults change
-                    // ReSharper disable once RedundantArgumentDefaultValue
-                    foreach (IContent d in GetPagedDescendants(document.Id, page, pageSize, out _,
-                                 ordering: Ordering.By("Path", Direction.Ascending)))
+                    // no need to check path here, parent has to be published here
+                    result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs);
+                    if (result != null)
                     {
-                        count++;
-
-                        // if parent is excluded, exclude child too
-                        if (exclude.Contains(d.ParentId))
+                        results.Add(result);
+                        if (result.Success)
                         {
-                            exclude.Add(d.Id);
                             continue;
                         }
-
-                        // no need to check path here, parent has to be published here
-                        result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false,
-                            publishedDocuments, eventMessages, userId, allLangs);
-                        if (result != null)
-                        {
-                            results.Add(result);
-                            if (result.Success)
-                            {
-                                continue;
-                            }
-                        }
-
-                        // if we could not publish the document, cut its branch
-                        exclude.Add(d.Id);
                     }
 
-                    page++;
-                } while (count > 0);
+                    // if we could not publish the document, cut its branch
+                    exclude.Add(d.Id);
+                }
 
-                Audit(AuditType.Publish, userId, document.Id, "Branch published");
-
-                // trigger events for the entire branch
-                // (SaveAndPublishBranchOne does *not* do it)
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages));
-                scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages));
-
-                scope.Complete();
+                page++;
             }
+            while (count > 0);
 
-            return results;
+            Audit(AuditType.Publish, userId, document.Id, "Branch published");
+
+            // trigger events for the entire branch
+            // (SaveAndPublishBranchOne does *not* do it)
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages));
+            scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages));
+
+            scope.Complete();
         }
 
-        // shouldPublish: a function determining whether the document has changes that need to be published
-        //  note - 'force' is handled by 'editing'
-        // publishValues: a function publishing values (using the appropriate PublishCulture calls)
-        private PublishResult? SaveAndPublishBranchItem(ICoreScope scope, IContent document,
-            Func?> shouldPublish,
-            Func, IReadOnlyCollection, bool> publishCultures,
-            bool isRoot,
-            ICollection publishedDocuments,
-            EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs)
+        return results;
+    }
+
+    // shouldPublish: a function determining whether the document has changes that need to be published
+    //  note - 'force' is handled by 'editing'
+    // publishValues: a function publishing values (using the appropriate PublishCulture calls)
+    private PublishResult? SaveAndPublishBranchItem(
+        ICoreScope scope,
+        IContent document,
+        Func?> shouldPublish,
+        Func, IReadOnlyCollection,
+            bool> publishCultures,
+        bool isRoot,
+        ICollection publishedDocuments,
+        EventMessages evtMsgs,
+        int userId,
+        IReadOnlyCollection allLangs)
+    {
+        HashSet? culturesToPublish = shouldPublish(document);
+
+        // null = do not include
+        if (culturesToPublish == null)
         {
-            HashSet? culturesToPublish = shouldPublish(document);
-            if (culturesToPublish == null) // null = do not include
+            return null;
+        }
+
+        // empty = already published
+        if (culturesToPublish.Count == 0)
+        {
+            return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
+        }
+
+        var savingNotification = new ContentSavingNotification(document, evtMsgs);
+        if (scope.Notifications.PublishCancelable(savingNotification))
+        {
+            return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document);
+        }
+
+        // publish & check if values are valid
+        if (!publishCultures(document, culturesToPublish, allLangs))
+        {
+            // TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid
+            return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
+        }
+
+        PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, savingNotification.State, userId, true, isRoot);
+        if (result.Success)
+        {
+            publishedDocuments.Add(document);
+        }
+
+        return result;
+    }
+
+    #endregion
+
+    #region Delete
+
+    /// 
+    public OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(content, eventMessages)))
             {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            // if it's not trashed yet, and published, we should unpublish
+            // but... Unpublishing event makes no sense (not going to cancel?) and no need to save
+            // just raise the event
+            if (content.Trashed == false && content.Published)
+            {
+                scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages));
+            }
+
+            DeleteLocked(scope, content, eventMessages);
+
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, eventMessages));
+            Audit(AuditType.Delete, userId, content.Id);
+
+            scope.Complete();
+        }
+
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
+    {
+        void DoDelete(IContent c)
+        {
+            _documentRepository.Delete(c);
+            scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
+
+            // media files deleted by QueuingEventDispatcher
+        }
+
+        const int pageSize = 500;
+        var total = long.MaxValue;
+        while (total > 0)
+        {
+            // get descendants - ordered from deepest to shallowest
+            IEnumerable descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
+            foreach (IContent c in descendants)
+            {
+                DoDelete(c);
+            }
+        }
+
+        DoDelete(content);
+    }
+
+    // TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
+    // Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT,
+    // if that's not the case, then the file will never be deleted, because when we delete the content,
+    // the version referencing the file will not be there anymore. SO, we can leak files.
+
+    /// 
+    ///     Permanently deletes versions from an  object prior to a specific date.
+    ///     This method will never delete the latest version of a content item.
+    /// 
+    /// Id of the  object to delete versions from
+    /// Latest version date
+    /// Optional Id of the User deleting versions of a Content object
+    public void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var deletingVersionsNotification =
+                new ContentDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
+            if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentRepository.DeleteVersions(id, versionDate);
+
+            scope.Notifications.Publish(
+                new ContentDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(
+                    deletingVersionsNotification));
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version date)");
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Permanently deletes specific version(s) from an  object.
+    ///     This method will never delete the latest version of a content item.
+    /// 
+    /// Id of the  object to delete a version from
+    /// Id of the version to delete
+    /// Boolean indicating whether to delete versions prior to the versionId
+    /// Optional Id of the User deleting versions of a Content object
+    public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, versionId);
+            if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            if (deletePriorVersions)
+            {
+                IContent? content = GetVersion(versionId);
+                DeleteVersions(id, content?.UpdateDate ?? DateTime.Now, userId);
+            }
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+            IContent? c = _documentRepository.Get(id);
+
+            // don't delete the current or published version
+            if (c?.VersionId != versionId &&
+                c?.PublishedVersionId != versionId)
+            {
+                _documentRepository.DeleteVersion(versionId);
+            }
+
+            scope.Notifications.Publish(
+                new ContentDeletedVersionsNotification(id, evtMsgs, versionId).WithStateFrom(
+                    deletingVersionsNotification));
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version)");
+
+            scope.Complete();
+        }
+    }
+
+    #endregion
+
+    #region Move, RecycleBin
+
+    /// 
+    public OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        var moves = new List<(IContent, string)>();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var originalPath = content.Path;
+            var moveEventInfo =
+                new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent);
+
+            var movingToRecycleBinNotification =
+                new ContentMovingToRecycleBinNotification(moveEventInfo, eventMessages);
+            if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification))
+            {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages); // causes rollback
+            }
+
+            // if it's published we may want to force-unpublish it - that would be backward-compatible... but...
+            // making a radical decision here: trashing is equivalent to moving under an unpublished node so
+            // it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted
+            // if (content.HasPublishedVersion)
+            // { }
+            PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true);
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
+
+            MoveEventInfo[] moveInfo = moves
+                .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
+                .ToArray();
+
+            scope.Notifications.Publish(
+                new ContentMovedToRecycleBinNotification(moveInfo, eventMessages).WithStateFrom(
+                    movingToRecycleBinNotification));
+            Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin");
+
+            scope.Complete();
+        }
+
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    /// 
+    ///     Moves an  object to a new location by changing its parent id.
+    /// 
+    /// 
+    ///     If the  object is already published it will be
+    ///     published after being moved to its new location. Otherwise it'll just
+    ///     be saved with a new parent id.
+    /// 
+    /// The  to move
+    /// Id of the Content's new Parent
+    /// Optional Id of the User moving the Content
+    public void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId)
+    {
+        // if moving to the recycle bin then use the proper method
+        if (parentId == Constants.System.RecycleBinContent)
+        {
+            MoveToRecycleBin(content, userId);
+            return;
+        }
+
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        var moves = new List<(IContent, string)>();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            IContent? parent = parentId == Constants.System.Root ? null : GetById(parentId);
+            if (parentId != Constants.System.Root && (parent == null || parent.Trashed))
+            {
+                throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
+            }
+
+            var moveEventInfo = new MoveEventInfo(content, content.Path, parentId);
+
+            var movingNotification = new ContentMovingNotification(moveEventInfo, eventMessages);
+            if (scope.Notifications.PublishCancelable(movingNotification))
+            {
+                scope.Complete();
+                return; // causes rollback
+            }
+
+            // if content was trashed, and since we're not moving to the recycle bin,
+            // indicate that the trashed status should be changed to false, else just
+            // leave it unchanged
+            var trashed = content.Trashed ? false : (bool?)null;
+
+            // if the content was trashed under another content, and so has a published version,
+            // it cannot move back as published but has to be unpublished first - that's for the
+            // root content, everything underneath will retain its published status
+            if (content.Trashed && content.Published)
+            {
+                // however, it had been masked when being trashed, so there's no need for
+                // any special event here - just change its state
+                content.PublishedState = PublishedState.Unpublishing;
+            }
+
+            PerformMoveLocked(content, parentId, parent, userId, moves, trashed);
+
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
+
+            // changes
+            MoveEventInfo[] moveInfo = moves
+                .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
+                .ToArray();
+
+            scope.Notifications.Publish(
+                new ContentMovedNotification(moveInfo, eventMessages).WithStateFrom(movingNotification));
+
+            Audit(AuditType.Move, userId, content.Id);
+
+            scope.Complete();
+        }
+    }
+
+    // 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<(IContent, string)> moves, bool? trash)
+    {
+        content.WriterId = userId;
+        content.ParentId = parentId;
+
+        // get the level delta (old pos to new pos)
+        // note that recycle bin (id:-20) level is 0!
+        var levelDelta = 1 - content.Level + (parent?.Level ?? 0);
+
+        var paths = new Dictionary();
+
+        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;
+
+        // these will be updated by the repo because we changed parentId
+        // content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id;
+        // content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId);
+        // content.Level += levelDelta;
+        PerformMoveContentLocked(content, userId, trash);
+
+        // if uow is not immediate, content.Path will be updated only when the UOW commits,
+        // and because we want it now, we have to calculate it by ourselves
+        // paths[content.Id] = content.Path;
+        paths[content.Id] =
+            (parent == null
+                ? parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString
+                : parent.Path) + "," + content.Id;
+
+        const int pageSize = 500;
+        IQuery? query = GetPagedDescendantQuery(originalPath);
+        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
+            IEnumerable descendants =
+                GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path"));
+
+            foreach (IContent descendant in descendants)
+            {
+                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);
+    }
+
+    private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
+    {
+        if (trash.HasValue)
+        {
+            ((ContentBase)content).Trashed = trash.Value;
+        }
+
+        content.WriterId = userId;
+        _documentRepository.Save(content);
+    }
+
+    /// 
+    ///     Empties the Recycle Bin by deleting all  that resides in the bin
+    /// 
+    public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId)
+    {
+        var deleted = new List();
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            // emptying the recycle bin means deleting whatever is in there - do it properly!
+            IQuery? query = Query().Where(x => x.ParentId == Constants.System.RecycleBinContent);
+            IContent[] contents = _documentRepository.Get(query).ToArray();
+
+            var emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(contents, eventMessages);
+            if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
+            {
+                scope.Complete();
+                return OperationResult.Cancel(eventMessages);
+            }
+
+            if (contents is not null)
+            {
+                foreach (IContent content in contents)
+                {
+                    DeleteLocked(scope, content, eventMessages);
+                    deleted.Add(content);
+                }
+            }
+
+            scope.Notifications.Publish(
+                new ContentEmptiedRecycleBinNotification(deleted, eventMessages).WithStateFrom(
+                    emptyingRecycleBinNotification));
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(deleted, TreeChangeTypes.Remove, eventMessages));
+            Audit(AuditType.Delete, userId, Constants.System.RecycleBinContent, "Recycle bin emptied");
+
+            scope.Complete();
+        }
+
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    public bool RecycleBinSmells()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentRepository.RecycleBinSmells();
+        }
+    }
+
+    #endregion
+
+    #region Others
+
+    /// 
+    ///     Copies an  object by creating a new Content object of the same type and copies all data from
+    ///     the current
+    ///     to the new copy which is returned. Recursively copies all children.
+    /// 
+    /// The  to copy
+    /// Id of the Content's new Parent
+    /// Boolean indicating whether the copy should be related to the original
+    /// Optional Id of the User copying the Content
+    /// The newly created  object
+    public IContent? Copy(IContent content, int parentId, bool relateToOriginal, int userId = Constants.Security.SuperUserId) => Copy(content, parentId, relateToOriginal, true, userId);
+
+    /// 
+    ///     Copies an  object by creating a new Content object of the same type and copies all data from
+    ///     the current
+    ///     to the new copy which is returned.
+    /// 
+    /// The  to copy
+    /// Id of the Content's new Parent
+    /// Boolean indicating whether the copy should be related to the original
+    /// A value indicating whether to recursively copy children.
+    /// Optional Id of the User copying the Content
+    /// The newly created  object
+    public IContent? Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        IContent copy = content.DeepCloneWithResetIdentities();
+        copy.ParentId = parentId;
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            if (scope.Notifications.PublishCancelable(
+                    new ContentCopyingNotification(content, copy, parentId, eventMessages)))
+            {
+                scope.Complete();
                 return null;
             }
 
-            if (culturesToPublish.Count == 0) // empty = already published
+            // note - relateToOriginal is not managed here,
+            // it's just part of the Copied event args so the RelateOnCopyHandler knows what to do
+            // meaning that the event has to trigger for every copied content including descendants
+            var copies = new List>();
+
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            // a copy is not published (but not really unpublishing either)
+            // update the create author and last edit author
+            if (copy.Published)
             {
-                return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
+                copy.Published = false;
             }
 
-            var savingNotification = new ContentSavingNotification(document, evtMsgs);
-            if (scope.Notifications.PublishCancelable(savingNotification))
+            copy.CreatorId = userId;
+            copy.WriterId = userId;
+
+            // get the current permissions, if there are any explicit ones they need to be copied
+            EntityPermissionCollection currentPermissions = GetPermissions(content);
+            currentPermissions.RemoveWhere(p => p.IsDefaultPermissions);
+
+            // save and flush because we need the ID for the recursive Copying events
+            _documentRepository.Save(copy);
+
+            // add permissions
+            if (currentPermissions.Count > 0)
             {
-                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document);
+                var permissionSet = new ContentPermissionSet(copy, currentPermissions);
+                _documentRepository.AddOrUpdatePermissions(permissionSet);
             }
 
-            // publish & check if values are valid
-            if (!publishCultures(document, culturesToPublish, allLangs))
+            // keep track of copies
+            copies.Add(Tuple.Create(content, copy));
+            var idmap = new Dictionary { [content.Id] = copy.Id };
+
+            // process descendants
+            if (recursive)
             {
-                //TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid
-                return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
+                const int pageSize = 500;
+                var page = 0;
+                var total = long.MaxValue;
+                while (page * pageSize < total)
+                {
+                    IEnumerable descendants =
+                        GetPagedDescendants(content.Id, page++, pageSize, out total);
+                    foreach (IContent descendant in descendants)
+                    {
+                        // if parent has not been copied, skip, else gets its copy id
+                        if (idmap.TryGetValue(descendant.ParentId, out parentId) == false)
+                        {
+                            continue;
+                        }
+
+                        IContent descendantCopy = descendant.DeepCloneWithResetIdentities();
+                        descendantCopy.ParentId = parentId;
+
+                        if (scope.Notifications.PublishCancelable(
+                                new ContentCopyingNotification(descendant, descendantCopy, parentId, eventMessages)))
+                        {
+                            continue;
+                        }
+
+                        // a copy is not published (but not really unpublishing either)
+                        // update the create author and last edit author
+                        if (descendantCopy.Published)
+                        {
+                            descendantCopy.Published = false;
+                        }
+
+                        descendantCopy.CreatorId = userId;
+                        descendantCopy.WriterId = userId;
+
+                        // save and flush (see above)
+                        _documentRepository.Save(descendantCopy);
+
+                        copies.Add(Tuple.Create(descendant, descendantCopy));
+                        idmap[descendant.Id] = descendantCopy.Id;
+                    }
+                }
             }
 
-            PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs,
-                savingNotification.State, userId, true, isRoot);
-            if (result.Success)
+            // not handling tags here, because
+            // - tags should be handled by the content repository
+            // - a copy is unpublished and therefore has no impact on tags in DB
+            scope.Notifications.Publish(
+                new ContentTreeChangeNotification(copy, TreeChangeTypes.RefreshBranch, eventMessages));
+            foreach (Tuple x in copies)
             {
-                publishedDocuments.Add(document);
+                scope.Notifications.Publish(new ContentCopiedNotification(x.Item1, x.Item2, parentId, relateToOriginal, eventMessages));
             }
 
-            return result;
+            Audit(AuditType.Copy, userId, content.Id);
+
+            scope.Complete();
         }
 
-        #endregion
+        return copy;
+    }
 
-        #region Delete
-
-        /// 
-        public OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId)
+    /// 
+    ///     Sends an  to Publication, which executes handlers and events for the 'Send to Publication'
+    ///     action.
+    /// 
+    /// The  to send to publication
+    /// Optional Id of the User issuing the send to publication
+    /// True if sending publication was successful otherwise false
+    public bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId)
+    {
+        if (content is null)
         {
-            EventMessages eventMessages = EventMessagesFactory.Get();
+            return false;
+        }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var sendingToPublishNotification = new ContentSendingToPublishNotification(content, evtMsgs);
+            if (scope.Notifications.PublishCancelable(sendingToPublishNotification))
             {
-                if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(content, eventMessages)))
+                scope.Complete();
+                return false;
+            }
+
+            // track the cultures changing for auditing
+            var culturesChanging = content.ContentType.VariesByCulture()
+                ? string.Join(",", content.CultureInfos!.Values.Where(x => x.IsDirty()).Select(x => x.Culture))
+                : null;
+
+            // TODO: Currently there's no way to change track which variant properties have changed, we only have change
+            // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
+            // in this particular case, determining which cultures have changed works with the above with names since it will
+            // have always changed if it's been saved in the back office but that's not really fail safe.
+
+            // Save before raising event
+            OperationResult saveResult = Save(content, userId);
+
+            // always complete (but maybe return a failed status)
+            scope.Complete();
+
+            if (!saveResult.Success)
+            {
+                return saveResult.Success;
+            }
+
+            scope.Notifications.Publish(
+                new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification));
+
+            if (culturesChanging != null)
+            {
+                Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
+            }
+            else
+            {
+                Audit(AuditType.SendToPublish, content.WriterId, content.Id);
+            }
+
+            return saveResult.Success;
+        }
+    }
+
+    /// 
+    ///     Sorts a collection of  objects by updating the SortOrder according
+    ///     to the ordering of items in the passed in .
+    /// 
+    /// 
+    ///     Using this method will ensure that the Published-state is maintained upon sorting
+    ///     so the cache is updated accordingly - as needed.
+    /// 
+    /// 
+    /// 
+    /// Result indicating what action was taken when handling the command.
+    public OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        IContent[] itemsA = items.ToArray();
+        if (itemsA.Length == 0)
+        {
+            return new OperationResult(OperationResultType.NoOperation, evtMsgs);
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
+            scope.Complete();
+            return ret;
+        }
+    }
+
+    /// 
+    ///     Sorts a collection of  objects by updating the SortOrder according
+    ///     to the ordering of items identified by the .
+    /// 
+    /// 
+    ///     Using this method will ensure that the Published-state is maintained upon sorting
+    ///     so the cache is updated accordingly - as needed.
+    /// 
+    /// 
+    /// 
+    /// Result indicating what action was taken when handling the command.
+    public OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        var idsA = ids?.ToArray();
+        if (idsA is null || idsA.Length == 0)
+        {
+            return new OperationResult(OperationResultType.NoOperation, evtMsgs);
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            IContent[] itemsA = GetByIds(idsA).ToArray();
+
+            OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
+            scope.Complete();
+            return ret;
+        }
+    }
+
+    private OperationResult Sort(ICoreScope scope, IContent[] itemsA, int userId, EventMessages eventMessages)
+    {
+        var sortingNotification = new ContentSortingNotification(itemsA, eventMessages);
+        var savingNotification = new ContentSavingNotification(itemsA, eventMessages);
+
+        // raise cancelable sorting event
+        if (scope.Notifications.PublishCancelable(sortingNotification))
+        {
+            return OperationResult.Cancel(eventMessages);
+        }
+
+        // raise cancelable saving event
+        if (scope.Notifications.PublishCancelable(savingNotification))
+        {
+            return OperationResult.Cancel(eventMessages);
+        }
+
+        var published = new List();
+        var saved = new List();
+        var sortOrder = 0;
+
+        foreach (IContent content in itemsA)
+        {
+            // if the current sort order equals that of the content we don't
+            // need to update it, so just increment the sort order and continue.
+            if (content.SortOrder == sortOrder)
+            {
+                sortOrder++;
+                continue;
+            }
+
+            // else update
+            content.SortOrder = sortOrder++;
+            content.WriterId = userId;
+
+            // if it's published, register it, no point running StrategyPublish
+            // since we're not really publishing it and it cannot be cancelled etc
+            if (content.Published)
+            {
+                published.Add(content);
+            }
+
+            // save
+            saved.Add(content);
+            _documentRepository.Save(content);
+        }
+
+        // first saved, then sorted
+        scope.Notifications.Publish(
+            new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification));
+        scope.Notifications.Publish(
+            new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification));
+
+        scope.Notifications.Publish(
+            new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages));
+
+        if (published.Any())
+        {
+            scope.Notifications.Publish(new ContentPublishedNotification(published, eventMessages));
+        }
+
+        Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
+        return OperationResult.Succeed(eventMessages);
+    }
+
+    public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            ContentDataIntegrityReport 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(_shortStringHelper, -1)) { Id = -1, Key = Guid.Empty };
+                scope.Notifications.Publish(new ContentTreeChangeNotification(root, TreeChangeTypes.RefreshAll, EventMessagesFactory.Get()));
+            }
+
+            return report;
+        }
+    }
+
+    #endregion
+
+    #region Internal Methods
+
+    /// 
+    ///     Gets a collection of  descendants by the first Parent.
+    /// 
+    ///  item to retrieve Descendants from
+    /// An Enumerable list of  objects
+    internal IEnumerable GetPublishedDescendants(IContent content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow!
+        }
+    }
+
+    internal IEnumerable GetPublishedDescendantsLocked(IContent content)
+    {
+        var pathMatch = content.Path + ",";
+        IQuery query = Query()
+            .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/);
+        IEnumerable contents = _documentRepository.Get(query);
+
+        // beware! contents contains all published version below content
+        // including those that are not directly published because below an unpublished content
+        // these must be filtered out here
+        var parents = new List { content.Id };
+        if (contents is not null)
+        {
+            foreach (IContent c in contents)
+            {
+                if (parents.Contains(c.ParentId))
                 {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
+                    yield return c;
+                    parents.Add(c.Id);
                 }
+            }
+        }
+    }
 
-                scope.WriteLock(Constants.Locks.ContentTree);
+    #endregion
 
+    #region Private Methods
+
+    private void Audit(AuditType type, int userId, int objectId, string? message = null, string? parameters = null) =>
+        _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.Document.GetName(), message, parameters));
+
+    private bool IsDefaultCulture(IReadOnlyCollection? langs, string culture) =>
+        langs?.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)) ?? false;
+
+    private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) =>
+        langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture));
+
+    #endregion
+
+    #region Publishing Strategies
+
+    /// 
+    ///     Ensures that a document can be published
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    private PublishResult StrategyCanPublish(
+        ICoreScope scope,
+        IContent content,
+        bool checkPath,
+        IReadOnlyList? culturesPublishing,
+        IReadOnlyCollection? culturesUnpublishing,
+        EventMessages evtMsgs,
+        IReadOnlyCollection allLangs,
+        IDictionary? notificationState)
+    {
+        // raise Publishing notification
+        if (scope.Notifications.PublishCancelable(
+                new ContentPublishingNotification(content, evtMsgs).WithState(notificationState)))
+        {
+            _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled");
+            return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
+        }
+
+        var variesByCulture = content.ContentType.VariesByCulture();
+
+        // If it's null it's invariant
+        CultureImpact[] impactsToPublish = culturesPublishing == null
+            ? new[] { CultureImpact.Invariant }
+            : culturesPublishing.Select(x =>
+                CultureImpact.Explicit(x, allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray();
+
+        // publish the culture(s)
+        if (!impactsToPublish.All(content.PublishCulture))
+        {
+            return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
+        }
+
+        // Validate the property values
+        IProperty[]? invalidProperties = null;
+        if (!impactsToPublish.All(x =>
+                _propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties, x)))
+        {
+            return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
+            {
+                InvalidProperties = invalidProperties,
+            };
+        }
+
+        // Check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will
+        // be changed to Unpublished and any culture currently published will not be visible.
+        if (variesByCulture)
+        {
+            if (culturesPublishing == null)
+            {
+                throw new InvalidOperationException(
+                    "Internal error, variesByCulture but culturesPublishing is null.");
+            }
+
+            if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing?.Count == 0)
+            {
+                // no published cultures = cannot be published
+                // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case
+                // there will be nothing to publish/unpublish.
+                return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
+            }
+
+            // missing mandatory culture = cannot be published
+            IEnumerable mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode);
+            var mandatoryMissing = mandatoryCultures.Any(x =>
+                !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase));
+            if (mandatoryMissing)
+            {
+                return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content);
+            }
+
+            if (culturesPublishing.Count == 0 && culturesUnpublishing?.Count > 0)
+            {
+                return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
+            }
+        }
+
+        // ensure that the document has published values
+        // either because it is 'publishing' or because it already has a published version
+        if (content.PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0)
+        {
+            _logger.LogInformation(
+                "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
+                content.Name,
+                content.Id,
+                "document does not have published values");
+            return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
+        }
+
+        ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
+
+        // loop over each culture publishing - or string.Empty for invariant
+        foreach (var culture in culturesPublishing ?? new[] { string.Empty })
+        {
+            // ensure that the document status is correct
+            // note: culture will be string.Empty for invariant
+            switch (content.GetStatus(contentSchedule, culture))
+            {
+                case ContentStatus.Expired:
+                    if (!variesByCulture)
+                    {
+                        _logger.LogInformation(
+                            "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired");
+                    }
+                    else
+                    {
+                        _logger.LogInformation(
+                            "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document culture has expired");
+                    }
+
+                    return new PublishResult(
+                        !variesByCulture
+                            ? PublishResultType.FailedPublishHasExpired : PublishResultType.FailedPublishCultureHasExpired,
+                        evtMsgs,
+                        content);
+
+                case ContentStatus.AwaitingRelease:
+                    if (!variesByCulture)
+                    {
+                        _logger.LogInformation(
+                            "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
+                            content.Name,
+                            content.Id,
+                            "document is awaiting release");
+                    }
+                    else
+                    {
+                        _logger.LogInformation(
+                            "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}",
+                            content.Name,
+                            content.Id,
+                            culture,
+                            "document is culture awaiting release");
+                    }
+
+                    return new PublishResult(
+                        !variesByCulture
+                            ? PublishResultType.FailedPublishAwaitingRelease
+                            : PublishResultType.FailedPublishCultureAwaitingRelease,
+                        evtMsgs,
+                        content);
+
+                case ContentStatus.Trashed:
+                    _logger.LogInformation(
+                        "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
+                        content.Name,
+                        content.Id,
+                        "document is trashed");
+                    return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content);
+            }
+        }
+
+        if (checkPath)
+        {
+            // check if the content can be path-published
+            // root content can be published
+            // else check ancestors - we know we are not trashed
+            var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content));
+            if (!pathIsOk)
+            {
+                _logger.LogInformation(
+                    "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
+                    content.Name,
+                    content.Id,
+                    "parent is not published");
+                return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content);
+            }
+        }
+
+        // If we are both publishing and unpublishing cultures, then return a mixed status
+        if (variesByCulture && culturesPublishing?.Count > 0 && culturesUnpublishing?.Count > 0)
+        {
+            return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
+        }
+
+        return new PublishResult(evtMsgs, content);
+    }
+
+    /// 
+    ///     Publishes a document
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     It is assumed that all publishing checks have passed before calling this method like
+    ///     
+    /// 
+    private PublishResult StrategyPublish(
+        IContent content,
+        IReadOnlyCollection? culturesPublishing,
+        IReadOnlyCollection? culturesUnpublishing,
+        EventMessages evtMsgs)
+    {
+        // change state to publishing
+        content.PublishedState = PublishedState.Publishing;
+
+        // if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result
+        if (content.ContentType.VariesByCulture())
+        {
+            if (content.Published && culturesUnpublishing?.Count == 0 && culturesPublishing?.Count == 0)
+            {
+                return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
+            }
+
+            if (culturesUnpublishing?.Count > 0)
+            {
+                _logger.LogInformation(
+                    "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.",
+                    content.Name,
+                    content.Id,
+                    string.Join(",", culturesUnpublishing));
+            }
+
+            if (culturesPublishing?.Count > 0)
+            {
+                _logger.LogInformation(
+                    "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.",
+                    content.Name,
+                    content.Id,
+                    string.Join(",", culturesPublishing));
+            }
+
+            if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count > 0)
+            {
+                return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
+            }
+
+            if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count == 0)
+            {
+                return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
+            }
+
+            return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content);
+        }
+
+        _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id);
+        return new PublishResult(evtMsgs, content);
+    }
+
+    /// 
+    ///     Ensures that a document can be unpublished
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    private PublishResult StrategyCanUnpublish(ICoreScope scope, IContent content, EventMessages evtMsgs)
+    {
+        // raise Unpublishing notification
+        if (scope.Notifications.PublishCancelable(new ContentUnpublishingNotification(content, evtMsgs)))
+        {
+            _logger.LogInformation(
+                "Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id);
+            return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content);
+        }
+
+        return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
+    }
+
+    /// 
+    ///     Unpublishes a document
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     It is assumed that all unpublishing checks have passed before calling this method like
+    ///     
+    /// 
+    private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs)
+    {
+        var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
+
+        // TODO: What is this check?? we just created this attempt and of course it is Success?!
+        if (attempt.Success == false)
+        {
+            return attempt;
+        }
+
+        // if the document has any release dates set to before now,
+        // they should be removed so they don't interrupt an unpublish
+        // otherwise it would remain released == published
+        ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
+        IReadOnlyList pastReleases =
+            contentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now);
+        foreach (ContentSchedule p in pastReleases)
+        {
+            contentSchedule.Remove(p);
+        }
+
+        if (pastReleases.Count > 0)
+        {
+            _logger.LogInformation(
+                "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id);
+        }
+
+        _documentRepository.PersistContentSchedule(content, contentSchedule);
+
+        // change state to unpublishing
+        content.PublishedState = PublishedState.Unpublishing;
+
+        _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id);
+        return attempt;
+    }
+
+    #endregion
+
+    #region Content Types
+
+    /// 
+    ///     Deletes all content of specified type. All children of deleted content is moved to Recycle Bin.
+    /// 
+    /// 
+    ///     This needs extra care and attention as its potentially a dangerous and extensive operation.
+    ///     
+    ///         Deletes content items of the specified type, and only that type. Does *not* handle content types
+    ///         inheritance and compositions, which need to be managed outside of this method.
+    ///     
+    /// 
+    /// Id of the 
+    /// Optional Id of the user issuing the delete operation
+    public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId)
+    {
+        // TODO: This currently this is called from the ContentTypeService but that needs to change,
+        // if we are deleting a content type, we should just delete the data and do this operation slightly differently.
+        // This method will recursively go lookup every content item, check if any of it's descendants are
+        // of a different type, move them to the recycle bin, then permanently delete the content items.
+        // The main problem with this is that for every content item being deleted, events are raised...
+        // which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
+        var changes = new List>();
+        var moves = new List<(IContent, string)>();
+        var contentTypeIdsA = contentTypeIds.ToArray();
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        // using an immediate uow here because we keep making changes with
+        // PerformMoveLocked and DeleteLocked that must be applied immediately,
+        // no point queuing operations
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            IQuery query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA);
+            IContent[] contents = _documentRepository.Get(query).ToArray();
+
+            if (contents is null)
+            {
+                return;
+            }
+
+            if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(contents, eventMessages)))
+            {
+                scope.Complete();
+                return;
+            }
+
+            // order by level, descending, so deepest first - that way, we cannot move
+            // a content of the deleted type, to the recycle bin (and then delete it...)
+            foreach (IContent content in contents.OrderByDescending(x => x.ParentId))
+            {
                 // if it's not trashed yet, and published, we should unpublish
                 // but... Unpublishing event makes no sense (not going to cancel?) and no need to save
                 // just raise the event
@@ -2236,1432 +3372,275 @@ namespace Umbraco.Cms.Core.Services
                     scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages));
                 }
 
+                // if current content has children, move them to trash
+                IContent c = content;
+                IQuery childQuery = Query().Where(x => x.ParentId == c.Id);
+                IEnumerable children = _documentRepository.Get(childQuery);
+                foreach (IContent child in children)
+                {
+                    // see MoveToRecycleBin
+                    PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true);
+                    changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch));
+                }
+
+                // delete content
+                // triggers the deleted event (and handles the files)
                 DeleteLocked(scope, content, eventMessages);
-
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, eventMessages));
-                Audit(AuditType.Delete, userId, content.Id);
-
-                scope.Complete();
+                changes.Add(new TreeChange(content, TreeChangeTypes.Remove));
             }
 
-            return OperationResult.Succeed(eventMessages);
+            MoveEventInfo[] moveInfos = moves
+                .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
+                .ToArray();
+            if (moveInfos.Length > 0)
+            {
+                scope.Notifications.Publish(new ContentMovedToRecycleBinNotification(moveInfos, eventMessages));
+            }
+
+            scope.Notifications.Publish(new ContentTreeChangeNotification(changes, eventMessages));
+
+            Audit(AuditType.Delete, userId, Constants.System.Root, $"Delete content of type {string.Join(",", contentTypeIdsA)}");
+
+            scope.Complete();
         }
-
-        private void DeleteLocked(ICoreScope scope, IContent content, EventMessages evtMsgs)
-        {
-            void DoDelete(IContent c)
-            {
-                _documentRepository.Delete(c);
-                scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
-
-                // media files deleted by QueuingEventDispatcher
-            }
-
-            const int pageSize = 500;
-            var total = long.MaxValue;
-            while (total > 0)
-            {
-                //get descendants - ordered from deepest to shallowest
-                IEnumerable descendants = GetPagedDescendants(content.Id, 0, pageSize, out total,
-                    ordering: Ordering.By("Path", Direction.Descending));
-                foreach (IContent c in descendants)
-                {
-                    DoDelete(c);
-                }
-            }
-
-            DoDelete(content);
-        }
-
-        //TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
-        // Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT,
-        // if that's not the case, then the file will never be deleted, because when we delete the content,
-        // the version referencing the file will not be there anymore. SO, we can leak files.
-
-        /// 
-        ///     Permanently deletes versions from an  object prior to a specific date.
-        ///     This method will never delete the latest version of a content item.
-        /// 
-        /// Id of the  object to delete versions from
-        /// Latest version date
-        /// Optional Id of the User deleting versions of a Content object
-        public void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var deletingVersionsNotification =
-                    new ContentDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
-                if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentRepository.DeleteVersions(id, versionDate);
-
-                scope.Notifications.Publish(
-                    new ContentDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(
-                        deletingVersionsNotification));
-                Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version date)");
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        ///     Permanently deletes specific version(s) from an  object.
-        ///     This method will never delete the latest version of a content item.
-        /// 
-        /// Id of the  object to delete a version from
-        /// Id of the version to delete
-        /// Boolean indicating whether to delete versions prior to the versionId
-        /// Optional Id of the User deleting versions of a Content object
-        public void DeleteVersion(int id, int versionId, bool deletePriorVersions,
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, versionId);
-                if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (deletePriorVersions)
-                {
-                    IContent? content = GetVersion(versionId);
-                    DeleteVersions(id, content?.UpdateDate ?? DateTime.Now, userId);
-                }
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-                IContent? c = _documentRepository.Get(id);
-                if (c?.VersionId != versionId &&
-                    c?.PublishedVersionId != versionId) // don't delete the current or published version
-                {
-                    _documentRepository.DeleteVersion(versionId);
-                }
-
-                scope.Notifications.Publish(
-                    new ContentDeletedVersionsNotification(id, evtMsgs, versionId).WithStateFrom(
-                        deletingVersionsNotification));
-                Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version)");
-
-                scope.Complete();
-            }
-        }
-
-        #endregion
-
-        #region Move, RecycleBin
-
-        /// 
-        public OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            var moves = new List<(IContent, string)>();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var originalPath = content.Path;
-                var moveEventInfo =
-                    new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent);
-
-                var movingToRecycleBinNotification =
-                    new ContentMovingToRecycleBinNotification(moveEventInfo, eventMessages);
-                if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages); // causes rollback
-                }
-
-                // if it's published we may want to force-unpublish it - that would be backward-compatible... but...
-                // making a radical decision here: trashing is equivalent to moving under an unpublished node so
-                // it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted
-                //if (content.HasPublishedVersion)
-                //{ }
-
-                PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true);
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
-
-                MoveEventInfo[] moveInfo = moves
-                    .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
-                    .ToArray();
-
-                scope.Notifications.Publish(
-                    new ContentMovedToRecycleBinNotification(moveInfo, eventMessages).WithStateFrom(
-                        movingToRecycleBinNotification));
-                Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin");
-
-                scope.Complete();
-            }
-
-            return OperationResult.Succeed(eventMessages);
-        }
-
-        /// 
-        ///     Moves an  object to a new location by changing its parent id.
-        /// 
-        /// 
-        ///     If the  object is already published it will be
-        ///     published after being moved to its new location. Otherwise it'll just
-        ///     be saved with a new parent id.
-        /// 
-        /// The  to move
-        /// Id of the Content's new Parent
-        /// Optional Id of the User moving the Content
-        public void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId)
-        {
-            // if moving to the recycle bin then use the proper method
-            if (parentId == Constants.System.RecycleBinContent)
-            {
-                MoveToRecycleBin(content, userId);
-                return;
-            }
-
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            var moves = new List<(IContent, string)>();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                IContent? parent = parentId == Constants.System.Root ? null : GetById(parentId);
-                if (parentId != Constants.System.Root && (parent == null || parent.Trashed))
-                {
-                    throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
-                }
-
-                var moveEventInfo = new MoveEventInfo(content, content.Path, parentId);
-
-                var movingNotification = new ContentMovingNotification(moveEventInfo, eventMessages);
-                if (scope.Notifications.PublishCancelable(movingNotification))
-                {
-                    scope.Complete();
-                    return; // causes rollback
-                }
-
-                // if content was trashed, and since we're not moving to the recycle bin,
-                // indicate that the trashed status should be changed to false, else just
-                // leave it unchanged
-                var trashed = content.Trashed ? false : (bool?)null;
-
-                // if the content was trashed under another content, and so has a published version,
-                // it cannot move back as published but has to be unpublished first - that's for the
-                // root content, everything underneath will retain its published status
-                if (content.Trashed && content.Published)
-                {
-                    // however, it had been masked when being trashed, so there's no need for
-                    // any special event here - just change its state
-                    content.PublishedState = PublishedState.Unpublishing;
-                }
-
-                PerformMoveLocked(content, parentId, parent, userId, moves, trashed);
-
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
-
-                MoveEventInfo[] moveInfo = moves //changes
-                    .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
-                    .ToArray();
-
-                scope.Notifications.Publish(
-                    new ContentMovedNotification(moveInfo, eventMessages).WithStateFrom(movingNotification));
-
-                Audit(AuditType.Move, userId, content.Id);
-
-                scope.Complete();
-            }
-        }
-
-        // 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<(IContent, string)> moves,
-            bool? trash)
-        {
-            content.WriterId = userId;
-            content.ParentId = parentId;
-
-            // get the level delta (old pos to new pos)
-            // note that recycle bin (id:-20) level is 0!
-            var levelDelta = 1 - content.Level + (parent?.Level ?? 0);
-
-            var paths = new Dictionary();
-
-            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;
-
-            // these will be updated by the repo because we changed parentId
-            //content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id;
-            //content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId);
-            //content.Level += levelDelta;
-            PerformMoveContentLocked(content, userId, trash);
-
-            // if uow is not immediate, content.Path will be updated only when the UOW commits,
-            // and because we want it now, we have to calculate it by ourselves
-            //paths[content.Id] = content.Path;
-            paths[content.Id] =
-                (parent == null
-                    ? parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString
-                    : parent.Path) + "," + content.Id;
-
-            const int pageSize = 500;
-            IQuery? query = GetPagedDescendantQuery(originalPath);
-            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
-                IEnumerable descendants =
-                    GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path"));
-
-                foreach (IContent descendant in descendants)
-                {
-                    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);
-        }
-
-        private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
-        {
-            if (trash.HasValue)
-            {
-                ((ContentBase)content).Trashed = trash.Value;
-            }
-
-            content.WriterId = userId;
-            _documentRepository.Save(content);
-        }
-
-        /// 
-        ///     Empties the Recycle Bin by deleting all  that resides in the bin
-        /// 
-        public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId)
-        {
-            var deleted = new List();
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                // emptying the recycle bin means deleting whatever is in there - do it properly!
-                IQuery? query = Query().Where(x => x.ParentId == Constants.System.RecycleBinContent);
-                IContent[] contents = _documentRepository.Get(query).ToArray();
-
-                var emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(contents, eventMessages);
-                if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Cancel(eventMessages);
-                }
-
-                if (contents is not null)
-                {
-                    foreach (IContent content in contents)
-                    {
-                        DeleteLocked(scope, content, eventMessages);
-                        deleted.Add(content);
-                    }
-                }
-
-                scope.Notifications.Publish(
-                    new ContentEmptiedRecycleBinNotification(deleted, eventMessages).WithStateFrom(
-                        emptyingRecycleBinNotification));
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(deleted, TreeChangeTypes.Remove, eventMessages));
-                Audit(AuditType.Delete, userId, Constants.System.RecycleBinContent, "Recycle bin emptied");
-
-                scope.Complete();
-            }
-
-            return OperationResult.Succeed(eventMessages);
-        }
-
-        public bool RecycleBinSmells()
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentRepository.RecycleBinSmells();
-            }
-        }
-
-        #endregion
-
-        #region Others
-
-        /// 
-        ///     Copies an  object by creating a new Content object of the same type and copies all data from
-        ///     the current
-        ///     to the new copy which is returned. Recursively copies all children.
-        /// 
-        /// The  to copy
-        /// Id of the Content's new Parent
-        /// Boolean indicating whether the copy should be related to the original
-        /// Optional Id of the User copying the Content
-        /// The newly created  object
-        public IContent? Copy(IContent content, int parentId, bool relateToOriginal,
-            int userId = Constants.Security.SuperUserId) => Copy(content, parentId, relateToOriginal, true, userId);
-
-        /// 
-        ///     Copies an  object by creating a new Content object of the same type and copies all data from
-        ///     the current
-        ///     to the new copy which is returned.
-        /// 
-        /// The  to copy
-        /// Id of the Content's new Parent
-        /// Boolean indicating whether the copy should be related to the original
-        /// A value indicating whether to recursively copy children.
-        /// Optional Id of the User copying the Content
-        /// The newly created  object
-        public IContent? Copy(IContent content, int parentId, bool relateToOriginal, bool recursive,
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            IContent copy = content.DeepCloneWithResetIdentities();
-            copy.ParentId = parentId;
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                if (scope.Notifications.PublishCancelable(
-                        new ContentCopyingNotification(content, copy, parentId, eventMessages)))
-                {
-                    scope.Complete();
-                    return null;
-                }
-
-                // note - relateToOriginal is not managed here,
-                // it's just part of the Copied event args so the RelateOnCopyHandler knows what to do
-                // meaning that the event has to trigger for every copied content including descendants
-
-                var copies = new List>();
-
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                // a copy is not published (but not really unpublishing either)
-                // update the create author and last edit author
-                if (copy.Published)
-                {
-                    copy.Published = false;
-                }
-
-                copy.CreatorId = userId;
-                copy.WriterId = userId;
-
-                //get the current permissions, if there are any explicit ones they need to be copied
-                EntityPermissionCollection currentPermissions = GetPermissions(content);
-                currentPermissions.RemoveWhere(p => p.IsDefaultPermissions);
-
-                // save and flush because we need the ID for the recursive Copying events
-                _documentRepository.Save(copy);
-
-                //add permissions
-                if (currentPermissions.Count > 0)
-                {
-                    var permissionSet = new ContentPermissionSet(copy, currentPermissions);
-                    _documentRepository.AddOrUpdatePermissions(permissionSet);
-                }
-
-                // keep track of copies
-                copies.Add(Tuple.Create(content, copy));
-                var idmap = new Dictionary {[content.Id] = copy.Id};
-
-                if (recursive) // process descendants
-                {
-                    const int pageSize = 500;
-                    var page = 0;
-                    var total = long.MaxValue;
-                    while (page * pageSize < total)
-                    {
-                        IEnumerable descendants =
-                            GetPagedDescendants(content.Id, page++, pageSize, out total);
-                        foreach (IContent descendant in descendants)
-                        {
-                            // if parent has not been copied, skip, else gets its copy id
-                            if (idmap.TryGetValue(descendant.ParentId, out parentId) == false)
-                            {
-                                continue;
-                            }
-
-                            IContent descendantCopy = descendant.DeepCloneWithResetIdentities();
-                            descendantCopy.ParentId = parentId;
-
-                            if (scope.Notifications.PublishCancelable(
-                                    new ContentCopyingNotification(descendant, descendantCopy, parentId,
-                                        eventMessages)))
-                            {
-                                continue;
-                            }
-
-                            // a copy is not published (but not really unpublishing either)
-                            // update the create author and last edit author
-                            if (descendantCopy.Published)
-                            {
-                                descendantCopy.Published = false;
-                            }
-
-                            descendantCopy.CreatorId = userId;
-                            descendantCopy.WriterId = userId;
-
-                            // save and flush (see above)
-                            _documentRepository.Save(descendantCopy);
-
-                            copies.Add(Tuple.Create(descendant, descendantCopy));
-                            idmap[descendant.Id] = descendantCopy.Id;
-                        }
-                    }
-                }
-
-                // not handling tags here, because
-                // - tags should be handled by the content repository
-                // - a copy is unpublished and therefore has no impact on tags in DB
-
-                scope.Notifications.Publish(
-                    new ContentTreeChangeNotification(copy, TreeChangeTypes.RefreshBranch, eventMessages));
-                foreach (Tuple x in copies)
-                {
-                    scope.Notifications.Publish(new ContentCopiedNotification(x.Item1, x.Item2, parentId,
-                        relateToOriginal, eventMessages));
-                }
-
-                Audit(AuditType.Copy, userId, content.Id);
-
-                scope.Complete();
-            }
-
-            return copy;
-        }
-
-        /// 
-        ///     Sends an  to Publication, which executes handlers and events for the 'Send to Publication'
-        ///     action.
-        /// 
-        /// The  to send to publication
-        /// Optional Id of the User issuing the send to publication
-        /// True if sending publication was successful otherwise false
-        public bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId)
-        {
-            if (content is null)
-            {
-                return false;
-            }
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var sendingToPublishNotification = new ContentSendingToPublishNotification(content, evtMsgs);
-                if (scope.Notifications.PublishCancelable(sendingToPublishNotification))
-                {
-                    scope.Complete();
-                    return false;
-                }
-
-                //track the cultures changing for auditing
-                var culturesChanging = content.ContentType.VariesByCulture()
-                    ? string.Join(",", content.CultureInfos!.Values.Where(x => x.IsDirty()).Select(x => x.Culture))
-                    : null;
-
-                // TODO: Currently there's no way to change track which variant properties have changed, we only have change
-                // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
-                // in this particular case, determining which cultures have changed works with the above with names since it will
-                // have always changed if it's been saved in the back office but that's not really fail safe.
-
-                //Save before raising event
-                OperationResult saveResult = Save(content, userId);
-
-                // always complete (but maybe return a failed status)
-                scope.Complete();
-
-                if (!saveResult.Success)
-                {
-                    return saveResult.Success;
-                }
-
-                scope.Notifications.Publish(
-                    new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification));
-
-                if (culturesChanging != null)
-                {
-                    Audit(AuditType.SendToPublishVariant, userId, content.Id,
-                        $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
-                }
-                else
-                {
-                    Audit(AuditType.SendToPublish, content.WriterId, content.Id);
-                }
-
-                return saveResult.Success;
-            }
-        }
-
-        /// 
-        ///     Sorts a collection of  objects by updating the SortOrder according
-        ///     to the ordering of items in the passed in .
-        /// 
-        /// 
-        ///     Using this method will ensure that the Published-state is maintained upon sorting
-        ///     so the cache is updated accordingly - as needed.
-        /// 
-        /// 
-        /// 
-        /// Result indicating what action was taken when handling the command.
-        public OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            IContent[] itemsA = items.ToArray();
-            if (itemsA.Length == 0)
-            {
-                return new OperationResult(OperationResultType.NoOperation, evtMsgs);
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
-                scope.Complete();
-                return ret;
-            }
-        }
-
-        /// 
-        ///     Sorts a collection of  objects by updating the SortOrder according
-        ///     to the ordering of items identified by the .
-        /// 
-        /// 
-        ///     Using this method will ensure that the Published-state is maintained upon sorting
-        ///     so the cache is updated accordingly - as needed.
-        /// 
-        /// 
-        /// 
-        /// Result indicating what action was taken when handling the command.
-        public OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            var idsA = ids?.ToArray();
-            if (idsA is null || idsA.Length == 0)
-            {
-                return new OperationResult(OperationResultType.NoOperation, evtMsgs);
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                IContent[] itemsA = GetByIds(idsA).ToArray();
-
-                OperationResult ret = Sort(scope, itemsA, userId, evtMsgs);
-                scope.Complete();
-                return ret;
-            }
-        }
-
-        private OperationResult Sort(ICoreScope scope, IContent[] itemsA, int userId, EventMessages eventMessages)
-        {
-            var sortingNotification = new ContentSortingNotification(itemsA, eventMessages);
-            var savingNotification = new ContentSavingNotification(itemsA, eventMessages);
-
-            // raise cancelable sorting event
-            if (scope.Notifications.PublishCancelable(sortingNotification))
-            {
-                return OperationResult.Cancel(eventMessages);
-            }
-
-            // raise cancelable saving event
-            if (scope.Notifications.PublishCancelable(savingNotification))
-            {
-                return OperationResult.Cancel(eventMessages);
-            }
-
-            var published = new List();
-            var saved = new List();
-            var sortOrder = 0;
-
-            foreach (IContent content in itemsA)
-            {
-                // if the current sort order equals that of the content we don't
-                // need to update it, so just increment the sort order and continue.
-                if (content.SortOrder == sortOrder)
-                {
-                    sortOrder++;
-                    continue;
-                }
-
-                // else update
-                content.SortOrder = sortOrder++;
-                content.WriterId = userId;
-
-                // if it's published, register it, no point running StrategyPublish
-                // since we're not really publishing it and it cannot be cancelled etc
-                if (content.Published)
-                {
-                    published.Add(content);
-                }
-
-                // save
-                saved.Add(content);
-                _documentRepository.Save(content);
-            }
-
-            //first saved, then sorted
-            scope.Notifications.Publish(
-                new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification));
-            scope.Notifications.Publish(
-                new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification));
-
-            scope.Notifications.Publish(
-                new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages));
-
-            if (published.Any())
-            {
-                scope.Notifications.Publish(new ContentPublishedNotification(published, eventMessages));
-            }
-
-            Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
-            return OperationResult.Succeed(eventMessages);
-        }
-
-        public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                ContentDataIntegrityReport 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(_shortStringHelper, -1))
-                    {
-                        Id = -1, Key = Guid.Empty
-                    };
-                    scope.Notifications.Publish(new ContentTreeChangeNotification(root, TreeChangeTypes.RefreshAll,
-                        EventMessagesFactory.Get()));
-                }
-
-                return report;
-            }
-        }
-
-        #endregion
-
-        #region Internal Methods
-
-        /// 
-        ///     Gets a collection of  descendants by the first Parent.
-        /// 
-        ///  item to retrieve Descendants from
-        /// An Enumerable list of  objects
-        internal IEnumerable GetPublishedDescendants(IContent content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow!
-            }
-        }
-
-        internal IEnumerable GetPublishedDescendantsLocked(IContent content)
-        {
-            var pathMatch = content.Path + ",";
-            IQuery query = Query()
-                .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/);
-            IEnumerable contents = _documentRepository.Get(query);
-
-            // beware! contents contains all published version below content
-            // including those that are not directly published because below an unpublished content
-            // these must be filtered out here
-
-            var parents = new List {content.Id};
-            if (contents is not null)
-            {
-                foreach (IContent c in contents)
-                {
-                    if (parents.Contains(c.ParentId))
-                    {
-                        yield return c;
-                        parents.Add(c.Id);
-                    }
-                }
-            }
-        }
-
-        #endregion
-
-        #region Private Methods
-
-        private void Audit(AuditType type, int userId, int objectId, string? message = null,
-            string? parameters = null) =>
-            _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.Document.GetName(), message,
-                parameters));
-
-        private bool IsDefaultCulture(IReadOnlyCollection? langs, string culture) =>
-            langs?.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)) ?? false;
-
-        private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) =>
-            langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture));
-
-        #endregion
-
-        #region Publishing Strategies
-
-        /// 
-        ///     Ensures that a document can be published
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        private PublishResult StrategyCanPublish(ICoreScope scope, IContent content, bool checkPath,
-            IReadOnlyList? culturesPublishing,
-            IReadOnlyCollection? culturesUnpublishing, EventMessages evtMsgs,
-            IReadOnlyCollection allLangs, IDictionary? notificationState)
-        {
-            // raise Publishing notification
-            if (scope.Notifications.PublishCancelable(
-                    new ContentPublishingNotification(content, evtMsgs).WithState(notificationState)))
-            {
-                _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                    content.Name, content.Id, "publishing was cancelled");
-                return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
-            }
-
-            var variesByCulture = content.ContentType.VariesByCulture();
-
-            CultureImpact[] impactsToPublish = culturesPublishing == null
-                ? new[] {CultureImpact.Invariant} //if it's null it's invariant
-                : culturesPublishing.Select(x =>
-                    CultureImpact.Explicit(x,
-                        allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray();
-
-            // publish the culture(s)
-            if (!impactsToPublish.All(content.PublishCulture))
-            {
-                return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
-            }
-
-            //validate the property values
-            IProperty[]? invalidProperties = null;
-            if (!impactsToPublish.All(x =>
-                    _propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties, x)))
-            {
-                return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
-                {
-                    InvalidProperties = invalidProperties
-                };
-            }
-
-            //Check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will
-            // be changed to Unpublished and any culture currently published will not be visible.
-            if (variesByCulture)
-            {
-                if (culturesPublishing == null)
-                {
-                    throw new InvalidOperationException(
-                        "Internal error, variesByCulture but culturesPublishing is null.");
-                }
-
-                if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing?.Count == 0)
-                {
-                    // no published cultures = cannot be published
-                    // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case
-                    // there will be nothing to publish/unpublish.
-                    return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
-                }
-
-
-                // missing mandatory culture = cannot be published
-                IEnumerable mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode);
-                var mandatoryMissing = mandatoryCultures.Any(x =>
-                    !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase));
-                if (mandatoryMissing)
-                {
-                    return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content);
-                }
-
-                if (culturesPublishing.Count == 0 && culturesUnpublishing?.Count > 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
-                }
-            }
-
-            // ensure that the document has published values
-            // either because it is 'publishing' or because it already has a published version
-            if (content.PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0)
-            {
-                _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                    content.Name, content.Id, "document does not have published values");
-                return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
-            }
-
-            ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
-            //loop over each culture publishing - or string.Empty for invariant
-            foreach (var culture in culturesPublishing ?? new[] {string.Empty})
-            {
-                // ensure that the document status is correct
-                // note: culture will be string.Empty for invariant
-                switch (content.GetStatus(contentSchedule, culture))
-                {
-                    case ContentStatus.Expired:
-                        if (!variesByCulture)
-                        {
-                            _logger.LogInformation(
-                                "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name,
-                                content.Id, "document has expired");
-                        }
-                        else
-                        {
-                            _logger.LogInformation(
-                                "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}",
-                                content.Name, content.Id, culture, "document culture has expired");
-                        }
-
-                        return new PublishResult(
-                            !variesByCulture
-                                ? PublishResultType.FailedPublishHasExpired
-                                : PublishResultType.FailedPublishCultureHasExpired, evtMsgs, content);
-
-                    case ContentStatus.AwaitingRelease:
-                        if (!variesByCulture)
-                        {
-                            _logger.LogInformation(
-                                "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name,
-                                content.Id, "document is awaiting release");
-                        }
-                        else
-                        {
-                            _logger.LogInformation(
-                                "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}",
-                                content.Name, content.Id, culture, "document is culture awaiting release");
-                        }
-
-                        return new PublishResult(
-                            !variesByCulture
-                                ? PublishResultType.FailedPublishAwaitingRelease
-                                : PublishResultType.FailedPublishCultureAwaitingRelease, evtMsgs, content);
-
-                    case ContentStatus.Trashed:
-                        _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                            content.Name, content.Id, "document is trashed");
-                        return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content);
-                }
-            }
-
-            if (checkPath)
-            {
-                // check if the content can be path-published
-                // root content can be published
-                // else check ancestors - we know we are not trashed
-                var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content));
-                if (!pathIsOk)
-                {
-                    _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
-                        content.Name, content.Id, "parent is not published");
-                    return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content);
-                }
-            }
-
-            //If we are both publishing and unpublishing cultures, then return a mixed status
-            if (variesByCulture && culturesPublishing?.Count > 0 && culturesUnpublishing?.Count > 0)
-            {
-                return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
-            }
-
-            return new PublishResult(evtMsgs, content);
-        }
-
-        /// 
-        ///     Publishes a document
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        ///     It is assumed that all publishing checks have passed before calling this method like
-        ///     
-        /// 
-        private PublishResult StrategyPublish(IContent content,
-            IReadOnlyCollection? culturesPublishing, IReadOnlyCollection? culturesUnpublishing,
-            EventMessages evtMsgs)
-        {
-            // change state to publishing
-            content.PublishedState = PublishedState.Publishing;
-
-            //if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result
-            if (content.ContentType.VariesByCulture())
-            {
-                if (content.Published && culturesUnpublishing?.Count == 0 && culturesPublishing?.Count == 0)
-                {
-                    return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
-                }
-
-                if (culturesUnpublishing?.Count > 0)
-                {
-                    _logger.LogInformation(
-                        "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.",
-                        content.Name, content.Id, string.Join(",", culturesUnpublishing));
-                }
-
-                if (culturesPublishing?.Count > 0)
-                {
-                    _logger.LogInformation(
-                        "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.",
-                        content.Name, content.Id, string.Join(",", culturesPublishing));
-                }
-
-                if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count > 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
-                }
-
-                if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count == 0)
-                {
-                    return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
-                }
-
-                return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content);
-            }
-
-            _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name,
-                content.Id);
-            return new PublishResult(evtMsgs, content);
-        }
-
-        /// 
-        ///     Ensures that a document can be unpublished
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        private PublishResult StrategyCanUnpublish(ICoreScope scope, IContent content, EventMessages evtMsgs)
-        {
-            // raise Unpublishing notification
-            if (scope.Notifications.PublishCancelable(new ContentUnpublishingNotification(content, evtMsgs)))
-            {
-                _logger.LogInformation(
-                    "Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.",
-                    content.Name, content.Id);
-                return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content);
-            }
-
-            return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
-        }
-
-        /// 
-        ///     Unpublishes a document
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        ///     It is assumed that all unpublishing checks have passed before calling this method like
-        ///     
-        /// 
-        private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs)
-        {
-            var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
-
-            //TODO: What is this check?? we just created this attempt and of course it is Success?!
-            if (attempt.Success == false)
-            {
-                return attempt;
-            }
-
-            // if the document has any release dates set to before now,
-            // they should be removed so they don't interrupt an unpublish
-            // otherwise it would remain released == published
-
-            var contentSchedule = _documentRepository.GetContentSchedule(content.Id);
-            var pastReleases = contentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now);
-            foreach (var p in pastReleases)
-                contentSchedule.Remove(p);
-
-            if (pastReleases.Count > 0)
-            {
-                _logger.LogInformation(
-                    "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.",
-                    content.Name, content.Id);
-            }
-
-            _documentRepository.PersistContentSchedule(content, contentSchedule);
-            // change state to unpublishing
-            content.PublishedState = PublishedState.Unpublishing;
-
-            _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name,
-                content.Id);
-            return attempt;
-        }
-
-        #endregion
-
-        #region Content Types
-
-        /// 
-        ///     Deletes all content of specified type. All children of deleted content is moved to Recycle Bin.
-        /// 
-        /// 
-        ///     This needs extra care and attention as its potentially a dangerous and extensive operation.
-        ///     
-        ///         Deletes content items of the specified type, and only that type. Does *not* handle content types
-        ///         inheritance and compositions, which need to be managed outside of this method.
-        ///     
-        /// 
-        /// Id of the 
-        /// Optional Id of the user issuing the delete operation
-        public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId)
-        {
-            // TODO: This currently this is called from the ContentTypeService but that needs to change,
-            // if we are deleting a content type, we should just delete the data and do this operation slightly differently.
-            // This method will recursively go lookup every content item, check if any of it's descendants are
-            // of a different type, move them to the recycle bin, then permanently delete the content items.
-            // The main problem with this is that for every content item being deleted, events are raised...
-            // which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
-
-            var changes = new List>();
-            var moves = new List<(IContent, string)>();
-            var contentTypeIdsA = contentTypeIds.ToArray();
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            // using an immediate uow here because we keep making changes with
-            // PerformMoveLocked and DeleteLocked that must be applied immediately,
-            // no point queuing operations
-            //
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                IQuery query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA);
-                IContent[] contents = _documentRepository.Get(query).ToArray();
-
-                if (contents is null)
-                {
-                    return;
-                }
-
-                if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(contents, eventMessages)))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                // order by level, descending, so deepest first - that way, we cannot move
-                // a content of the deleted type, to the recycle bin (and then delete it...)
-                foreach (IContent content in contents.OrderByDescending(x => x.ParentId))
-                {
-                    // if it's not trashed yet, and published, we should unpublish
-                    // but... Unpublishing event makes no sense (not going to cancel?) and no need to save
-                    // just raise the event
-                    if (content.Trashed == false && content.Published)
-                    {
-                        scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages));
-                    }
-
-                    // if current content has children, move them to trash
-                    IContent c = content;
-                    IQuery childQuery = Query().Where(x => x.ParentId == c.Id);
-                    IEnumerable children = _documentRepository.Get(childQuery);
-                    foreach (IContent child in children)
-                    {
-                        // see MoveToRecycleBin
-                        PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true);
-                        changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch));
-                    }
-
-                    // delete content
-                    // triggers the deleted event (and handles the files)
-                    DeleteLocked(scope, content, eventMessages);
-                    changes.Add(new TreeChange(content, TreeChangeTypes.Remove));
-                }
-
-                MoveEventInfo[] moveInfos = moves
-                    .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
-                    .ToArray();
-                if (moveInfos.Length > 0)
-                {
-                    scope.Notifications.Publish(new ContentMovedToRecycleBinNotification(moveInfos, eventMessages));
-                }
-
-                scope.Notifications.Publish(new ContentTreeChangeNotification(changes, eventMessages));
-
-                Audit(AuditType.Delete, userId, Constants.System.Root,
-                    $"Delete content of type {string.Join(",", contentTypeIdsA)}");
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        ///     Deletes all content items of specified type. All children of deleted content item is moved to Recycle Bin.
-        /// 
-        /// This needs extra care and attention as its potentially a dangerous and extensive operation
-        /// Id of the 
-        /// Optional id of the user deleting the media
-        public void DeleteOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
-            DeleteOfTypes(new[] {contentTypeId}, userId);
-
-        private IContentType GetContentType(ICoreScope scope, string contentTypeAlias)
-        {
-            if (contentTypeAlias == null)
-            {
-                throw new ArgumentNullException(nameof(contentTypeAlias));
-            }
-
-            if (string.IsNullOrWhiteSpace(contentTypeAlias))
-            {
-                throw new ArgumentException("Value can't be empty or consist only of white-space characters.",
-                    nameof(contentTypeAlias));
-            }
-
-            scope.ReadLock(Constants.Locks.ContentTypes);
-
-            IQuery query = Query().Where(x => x.Alias == contentTypeAlias);
-            IContentType? contentType = _contentTypeRepository.Get(query).FirstOrDefault();
-
-            if (contentType == null)
-            {
-                throw new Exception(
-                    $"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback
-            }
-
-            return contentType;
-        }
-
-        private IContentType GetContentType(string contentTypeAlias)
-        {
-            if (contentTypeAlias == null)
-            {
-                throw new ArgumentNullException(nameof(contentTypeAlias));
-            }
-
-            if (string.IsNullOrWhiteSpace(contentTypeAlias))
-            {
-                throw new ArgumentException("Value can't be empty or consist only of white-space characters.",
-                    nameof(contentTypeAlias));
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return GetContentType(scope, contentTypeAlias);
-            }
-        }
-
-        #endregion
-
-        #region Blueprints
-
-        public IContent? GetBlueprintById(int id)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IContent? blueprint = _documentBlueprintRepository.Get(id);
-                if (blueprint != null)
-                {
-                    blueprint.Blueprint = true;
-                }
-
-                return blueprint;
-            }
-        }
-
-        public IContent? GetBlueprintById(Guid id)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.ContentTree);
-                IContent? blueprint = _documentBlueprintRepository.Get(id);
-                if (blueprint != null)
-                {
-                    blueprint.Blueprint = true;
-                }
-
-                return blueprint;
-            }
-        }
-
-        public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            //always ensure the blueprint is at the root
-            if (content.ParentId != -1)
-            {
-                content.ParentId = -1;
-            }
-
-            content.Blueprint = true;
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                if (content.HasIdentity == false)
-                {
-                    content.CreatorId = userId;
-                }
-
-                content.WriterId = userId;
-
-                _documentBlueprintRepository.Save(content);
-
-                Audit(AuditType.Save, Constants.Security.SuperUserId, content.Id,
-                    $"Saved content template: {content.Name}");
-
-                scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
-
-                scope.Complete();
-            }
-        }
-
-        public void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-                _documentBlueprintRepository.Delete(content);
-                scope.Notifications.Publish(new ContentDeletedBlueprintNotification(content, evtMsgs));
-                scope.Complete();
-            }
-        }
-
-        private static readonly string?[] ArrayOfOneNullString = {null};
-
-        public IContent CreateContentFromBlueprint(IContent blueprint, string name,
-            int userId = Constants.Security.SuperUserId)
-        {
-            if (blueprint == null)
-            {
-                throw new ArgumentNullException(nameof(blueprint));
-            }
-
-            IContentType contentType = GetContentType(blueprint.ContentType.Alias);
-            var content = new Content(name, -1, contentType);
-            content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id);
-
-            content.CreatorId = userId;
-            content.WriterId = userId;
-
-            IEnumerable cultures = ArrayOfOneNullString;
-            if (blueprint.CultureInfos?.Count > 0)
-            {
-                cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
-                using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-                {
-                    if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(),
-                            out ContentCultureInfos defaultCulture))
-                    {
-                        defaultCulture.Name = name;
-                    }
-
-                    scope.Complete();
-                }
-            }
-
-            DateTime now = DateTime.Now;
-            foreach (var culture in cultures)
-            {
-                foreach (IProperty property in blueprint.Properties)
-                {
-                    var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null;
-                    content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture);
-                }
-
-                if (!string.IsNullOrEmpty(culture))
-                {
-                    content.SetCultureInfo(culture, blueprint.GetCultureName(culture), now);
-                }
-            }
-
-            return content;
-        }
-
-        public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                IQuery query = Query();
-                if (contentTypeId.Length > 0)
-                {
-                    query.Where(x => contentTypeId.Contains(x.ContentTypeId));
-                }
-
-                return _documentBlueprintRepository.Get(query).Select(x =>
-                {
-                    x.Blueprint = true;
-                    return x;
-                });
-            }
-        }
-
-        public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds,
-            int userId = Constants.Security.SuperUserId)
-        {
-            EventMessages evtMsgs = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.ContentTree);
-
-                var contentTypeIdsA = contentTypeIds.ToArray();
-                IQuery query = Query();
-                if (contentTypeIdsA.Length > 0)
-                {
-                    query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId));
-                }
-
-                IContent[]? blueprints = _documentBlueprintRepository.Get(query)?.Select(x =>
-                {
-                    x.Blueprint = true;
-                    return x;
-                }).ToArray();
-
-                if (blueprints is not null)
-                {
-                    foreach (IContent blueprint in blueprints)
-                    {
-                        _documentBlueprintRepository.Delete(blueprint);
-                    }
-
-                    scope.Notifications.Publish(new ContentDeletedBlueprintNotification(blueprints, evtMsgs));
-                    scope.Complete();
-                }
-            }
-        }
-
-        public void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
-            DeleteBlueprintsOfTypes(new[] {contentTypeId}, userId);
-
-        #endregion
     }
+
+    /// 
+    ///     Deletes all content items of specified type. All children of deleted content item is moved to Recycle Bin.
+    /// 
+    /// This needs extra care and attention as its potentially a dangerous and extensive operation
+    /// Id of the 
+    /// Optional id of the user deleting the media
+    public void DeleteOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
+        DeleteOfTypes(new[] { contentTypeId }, userId);
+
+    private IContentType GetContentType(ICoreScope scope, string contentTypeAlias)
+    {
+        if (contentTypeAlias == null)
+        {
+            throw new ArgumentNullException(nameof(contentTypeAlias));
+        }
+
+        if (string.IsNullOrWhiteSpace(contentTypeAlias))
+        {
+            throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias));
+        }
+
+        scope.ReadLock(Constants.Locks.ContentTypes);
+
+        IQuery query = Query().Where(x => x.Alias == contentTypeAlias);
+        IContentType? contentType = _contentTypeRepository.Get(query).FirstOrDefault();
+
+        if (contentType == null)
+        {
+            throw new Exception(
+                $"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback
+        }
+
+        return contentType;
+    }
+
+    private IContentType GetContentType(string contentTypeAlias)
+    {
+        if (contentTypeAlias == null)
+        {
+            throw new ArgumentNullException(nameof(contentTypeAlias));
+        }
+
+        if (string.IsNullOrWhiteSpace(contentTypeAlias))
+        {
+            throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias));
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return GetContentType(scope, contentTypeAlias);
+        }
+    }
+
+    #endregion
+
+    #region Blueprints
+
+    public IContent? GetBlueprintById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IContent? blueprint = _documentBlueprintRepository.Get(id);
+            if (blueprint != null)
+            {
+                blueprint.Blueprint = true;
+            }
+
+            return blueprint;
+        }
+    }
+
+    public IContent? GetBlueprintById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(Constants.Locks.ContentTree);
+            IContent? blueprint = _documentBlueprintRepository.Get(id);
+            if (blueprint != null)
+            {
+                blueprint.Blueprint = true;
+            }
+
+            return blueprint;
+        }
+    }
+
+    public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        // always ensure the blueprint is at the root
+        if (content.ParentId != -1)
+        {
+            content.ParentId = -1;
+        }
+
+        content.Blueprint = true;
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            if (content.HasIdentity == false)
+            {
+                content.CreatorId = userId;
+            }
+
+            content.WriterId = userId;
+
+            _documentBlueprintRepository.Save(content);
+
+            Audit(AuditType.Save, Constants.Security.SuperUserId, content.Id, $"Saved content template: {content.Name}");
+
+            scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
+
+            scope.Complete();
+        }
+    }
+
+    public void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentBlueprintRepository.Delete(content);
+            scope.Notifications.Publish(new ContentDeletedBlueprintNotification(content, evtMsgs));
+            scope.Complete();
+        }
+    }
+
+    private static readonly string?[] ArrayOfOneNullString = { null };
+
+    public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
+    {
+        if (blueprint == null)
+        {
+            throw new ArgumentNullException(nameof(blueprint));
+        }
+
+        IContentType contentType = GetContentType(blueprint.ContentType.Alias);
+        var content = new Content(name, -1, contentType);
+        content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id);
+
+        content.CreatorId = userId;
+        content.WriterId = userId;
+
+        IEnumerable cultures = ArrayOfOneNullString;
+        if (blueprint.CultureInfos?.Count > 0)
+        {
+            cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            {
+                if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
+                {
+                    defaultCulture.Name = name;
+                }
+
+                scope.Complete();
+            }
+        }
+
+        DateTime now = DateTime.Now;
+        foreach (var culture in cultures)
+        {
+            foreach (IProperty property in blueprint.Properties)
+            {
+                var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null;
+                content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture);
+            }
+
+            if (!string.IsNullOrEmpty(culture))
+            {
+                content.SetCultureInfo(culture, blueprint.GetCultureName(culture), now);
+            }
+        }
+
+        return content;
+    }
+
+    public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query();
+            if (contentTypeId.Length > 0)
+            {
+                query.Where(x => contentTypeId.Contains(x.ContentTypeId));
+            }
+
+            return _documentBlueprintRepository.Get(query).Select(x =>
+            {
+                x.Blueprint = true;
+                return x;
+            });
+        }
+    }
+
+    public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+
+            var contentTypeIdsA = contentTypeIds.ToArray();
+            IQuery query = Query();
+            if (contentTypeIdsA.Length > 0)
+            {
+                query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId));
+            }
+
+            IContent[]? blueprints = _documentBlueprintRepository.Get(query)?.Select(x =>
+            {
+                x.Blueprint = true;
+                return x;
+            }).ToArray();
+
+            if (blueprints is not null)
+            {
+                foreach (IContent blueprint in blueprints)
+                {
+                    _documentBlueprintRepository.Delete(blueprint);
+                }
+
+                scope.Notifications.Publish(new ContentDeletedBlueprintNotification(blueprints, evtMsgs));
+                scope.Complete();
+            }
+        }
+    }
+
+    public void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
+        DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
index 726c5b4435..b042612b1a 100644
--- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs
@@ -1,102 +1,106 @@
 // Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using System.Text.RegularExpressions;
 using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+/// 
+///     Content service extension methods
+/// 
+public static class ContentServiceExtensions
 {
-    /// 
-    /// Content service extension methods
-    /// 
-    public static class ContentServiceExtensions
+    #region RTE Anchor values
+
+    private static readonly Regex AnchorRegex = new("", RegexOptions.Compiled);
+
+    public static IEnumerable? GetByIds(this IContentService contentService, IEnumerable ids)
     {
-        #region RTE Anchor values
-
-        private static readonly Regex AnchorRegex = new Regex("", RegexOptions.Compiled);
-
-        public static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string? culture = "*")
+        var guids = new List();
+        foreach (Udi udi in ids)
         {
-            var result = new List();
-            var content = contentService.GetById(id);
-
-            if (content is not null)
+            if (udi is not GuidUdi guidUdi)
             {
-                foreach (var contentProperty in content.Properties)
+                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                    " which is required by content");
+            }
+
+            guids.Add(guidUdi);
+        }
+
+        return contentService.GetByIds(guids.Select(x => x.Guid));
+    }
+
+    /// 
+    ///     Method to create an IContent object based on the Udi of a parent
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
+    {
+        if (parentId is not GuidUdi guidUdi)
+        {
+            throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                " which is required by content");
+        }
+
+        IContent? parent = contentService.GetById(guidUdi.Guid);
+        return contentService.Create(name, parent, contentTypeAlias, userId);
+    }
+
+    /// 
+    ///     Remove all permissions for this user for all nodes
+    /// 
+    /// 
+    /// 
+    public static void RemoveContentPermissions(this IContentService contentService, int contentId) =>
+        contentService.SetPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection()));
+
+    public static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string? culture = "*")
+    {
+        var result = new List();
+        IContent? content = contentService.GetById(id);
+
+        if (content is not null)
+        {
+            foreach (IProperty contentProperty in content.Properties)
+            {
+                if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases
+                        .TinyMce))
                 {
-                    if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce))
+                    var value = contentProperty.GetValue(culture)?.ToString();
+                    if (!string.IsNullOrEmpty(value))
                     {
-                        var value = contentProperty.GetValue(culture)?.ToString();
-                        if (!string.IsNullOrEmpty(value))
-                        {
-                            result.AddRange(contentService.GetAnchorValuesFromRTEContent(value));
-                        }
+                        result.AddRange(contentService.GetAnchorValuesFromRTEContent(value));
                     }
                 }
             }
-
-            return result;
         }
 
-
-        public static IEnumerable GetAnchorValuesFromRTEContent(this IContentService contentService, string rteContent)
-        {
-            var result = new List();
-            var matches = AnchorRegex.Matches(rteContent);
-            foreach (Match match in matches)
-            {
-                result.Add(match.Value.Split(Constants.CharArrays.DoubleQuote)[1]);
-            }
-            return result;
-        }
-        #endregion
-
-        public static IEnumerable? GetByIds(this IContentService contentService, IEnumerable ids)
-        {
-            var guids = new List();
-            foreach (var udi in ids)
-            {
-                var guidUdi = udi as GuidUdi;
-                if (guidUdi is null)
-                    throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content");
-                guids.Add(guidUdi);
-            }
-
-            return contentService.GetByIds(guids.Select(x => x.Guid));
-        }
-
-        /// 
-        /// Method to create an IContent object based on the Udi of a parent
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
-        {
-            var guidUdi = parentId as GuidUdi;
-            if (guidUdi is null)
-                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content");
-            var parent = contentService.GetById(guidUdi.Guid);
-            return contentService.Create(name, parent, contentTypeAlias, userId);
-        }
-
-        /// 
-        /// Remove all permissions for this user for all nodes
-        /// 
-        /// 
-        /// 
-        public static void RemoveContentPermissions(this IContentService contentService, int contentId)
-        {
-            contentService.SetPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection()));
-        }
+        return result;
     }
+
+    public static IEnumerable GetAnchorValuesFromRTEContent(
+        this IContentService contentService,
+        string rteContent)
+    {
+        var result = new List();
+        MatchCollection matches = AnchorRegex.Matches(rteContent);
+        foreach (Match match in matches)
+        {
+            result.Add(match.Value.Split(Constants.CharArrays.DoubleQuote)[1]);
+        }
+
+        return result;
+    }
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs b/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs
index b493460876..36a790b9f6 100644
--- a/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs
+++ b/src/Umbraco.Core/Services/ContentTypeBaseServiceProvider.cs
@@ -1,42 +1,50 @@
-using System;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class ContentTypeBaseServiceProvider : IContentTypeBaseServiceProvider
 {
-    public class ContentTypeBaseServiceProvider : IContentTypeBaseServiceProvider
+    private readonly IContentTypeService _contentTypeService;
+    private readonly IMediaTypeService _mediaTypeService;
+    private readonly IMemberTypeService _memberTypeService;
+
+    public ContentTypeBaseServiceProvider(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService)
     {
-        private readonly IContentTypeService _contentTypeService;
-        private readonly IMediaTypeService _mediaTypeService;
-        private readonly IMemberTypeService _memberTypeService;
+        _contentTypeService = contentTypeService;
+        _mediaTypeService = mediaTypeService;
+        _memberTypeService = memberTypeService;
+    }
 
-        public ContentTypeBaseServiceProvider(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService)
+    public IContentTypeBaseService For(IContentBase contentBase)
+    {
+        if (contentBase == null)
         {
-            _contentTypeService = contentTypeService;
-            _mediaTypeService = mediaTypeService;
-            _memberTypeService = memberTypeService;
+            throw new ArgumentNullException(nameof(contentBase));
         }
 
-        public IContentTypeBaseService For(IContentBase contentBase)
+        switch (contentBase)
         {
-            if (contentBase == null) throw new ArgumentNullException(nameof(contentBase));
-            switch (contentBase)
-            {
-                case IContent _:
-                    return  _contentTypeService;
-                case IMedia _:
-                    return   _mediaTypeService;
-                case IMember _:
-                    return  _memberTypeService;
-                default:
-                    throw new ArgumentException($"Invalid contentBase type: {contentBase.GetType().FullName}" , nameof(contentBase));
-            }
-        }
-
-        // note: this should be a default interface method with C# 8
-        public IContentTypeComposition? GetContentTypeOf(IContentBase contentBase)
-        {
-            if (contentBase == null) throw new ArgumentNullException(nameof(contentBase));
-            return For(contentBase).Get(contentBase.ContentTypeId);
+            case IContent _:
+                return _contentTypeService;
+            case IMedia _:
+                return _mediaTypeService;
+            case IMember _:
+                return _memberTypeService;
+            default:
+                throw new ArgumentException(
+                    $"Invalid contentBase type: {contentBase.GetType().FullName}",
+                    nameof(contentBase));
         }
     }
+
+    // note: this should be a default interface method with C# 8
+    public IContentTypeComposition? GetContentTypeOf(IContentBase contentBase)
+    {
+        if (contentBase == null)
+        {
+            throw new ArgumentNullException(nameof(contentBase));
+        }
+
+        return For(contentBase).Get(contentBase.ContentTypeId);
+    }
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs
index 8f7316d913..39adcf0daf 100644
--- a/src/Umbraco.Core/Services/ContentTypeService.cs
+++ b/src/Umbraco.Core/Services/ContentTypeService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -9,125 +6,138 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents the ContentType Service, which is an easy access to operations involving 
+/// 
+public class ContentTypeService : ContentTypeServiceBase, IContentTypeService
 {
+    public ContentTypeService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IContentService contentService,
+        IContentTypeRepository repository,
+        IAuditRepository auditRepository,
+        IDocumentTypeContainerRepository entityContainerRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : base(provider, loggerFactory, eventMessagesFactory, repository, auditRepository, entityContainerRepository, entityRepository, eventAggregator) =>
+        ContentService = contentService;
+
+    // beware! order is important to avoid deadlocks
+    protected override int[] ReadLockIds { get; } = { Constants.Locks.ContentTypes };
+
+    protected override int[] WriteLockIds { get; } = { Constants.Locks.ContentTree, Constants.Locks.ContentTypes };
+
+    protected override Guid ContainedObjectType => Constants.ObjectTypes.DocumentType;
+
+    private IContentService ContentService { get; }
+
     /// 
-    /// Represents the ContentType Service, which is an easy access to operations involving 
+    ///     Gets all property type aliases across content, media and member types.
     /// 
-    public class ContentTypeService : ContentTypeServiceBase, IContentTypeService
+    /// All property type aliases.
+    /// Beware! Works across content, media and member types.
+    public IEnumerable GetAllPropertyTypeAliases()
     {
-        public ContentTypeService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IContentService contentService,
-            IContentTypeRepository repository, IAuditRepository auditRepository, IDocumentTypeContainerRepository entityContainerRepository, IEntityRepository entityRepository,
-            IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory, repository, auditRepository, entityContainerRepository, entityRepository, eventAggregator)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            ContentService = contentService;
-        }
-
-        // beware! order is important to avoid deadlocks
-        protected override int[] ReadLockIds { get; } = { Cms.Core.Constants.Locks.ContentTypes };
-        protected override int[] WriteLockIds { get; } = { Cms.Core.Constants.Locks.ContentTree, Cms.Core.Constants.Locks.ContentTypes };
-
-        private IContentService ContentService { get; }
-
-        protected override Guid ContainedObjectType => Cms.Core.Constants.ObjectTypes.DocumentType;
-
-        #region Notifications
-
-        protected override SavingNotification GetSavingNotification(IContentType item,
-            EventMessages eventMessages) => new ContentTypeSavingNotification(item, eventMessages);
-
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeSavingNotification(items, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IContentType item,
-            EventMessages eventMessages) => new ContentTypeSavedNotification(item, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeSavedNotification(items, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IContentType item,
-            EventMessages eventMessages) => new ContentTypeDeletingNotification(item, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeDeletingNotification(items, eventMessages);
-
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new ContentTypeDeletedNotification(items, eventMessages);
-
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new ContentTypeMovingNotification(moveInfo, eventMessages);
-
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new ContentTypeMovedNotification(moveInfo, eventMessages);
-
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new ContentTypeChangedNotification(changes, eventMessages);
-
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new ContentTypeRefreshedNotification(changes, eventMessages);
-
-        #endregion
-
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var typeIdsA = typeIds.ToArray();
-                ContentService.DeleteOfTypes(typeIdsA);
-                ContentService.DeleteBlueprintsOfTypes(typeIdsA);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Gets all property type aliases across content, media and member types.
-        /// 
-        /// All property type aliases.
-        /// Beware! Works across content, media and member types.
-        public IEnumerable GetAllPropertyTypeAliases()
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // that one is special because it works across content, media and member types
-                scope.ReadLock(new[] { Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes });
-                return Repository.GetAllPropertyTypeAliases();
-            }
-        }
-
-        /// 
-        /// Gets all content type aliases across content, media and member types.
-        /// 
-        /// Optional object types guid to restrict to content, and/or media, and/or member types.
-        /// All content type aliases.
-        /// Beware! Works across content, media and member types.
-        public IEnumerable GetAllContentTypeAliases(params Guid[] guids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // that one is special because it works across content, media and member types
-                scope.ReadLock(new[] { Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes });
-                return Repository.GetAllContentTypeAliases(guids);
-            }
-        }
-
-        /// 
-        /// Gets all content type id for aliases across content, media and member types.
-        /// 
-        /// Aliases to look for.
-        /// All content type ids.
-        /// Beware! Works across content, media and member types.
-        public IEnumerable GetAllContentTypeIds(string[] aliases)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // that one is special because it works across content, media and member types
-                scope.ReadLock(new[] { Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes });
-                return Repository.GetAllContentTypeIds(aliases);
-            }
+            // that one is special because it works across content, media and member types
+            scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes);
+            return Repository.GetAllPropertyTypeAliases();
         }
     }
+
+    /// 
+    ///     Gets all content type aliases across content, media and member types.
+    /// 
+    /// Optional object types guid to restrict to content, and/or media, and/or member types.
+    /// All content type aliases.
+    /// Beware! Works across content, media and member types.
+    public IEnumerable GetAllContentTypeAliases(params Guid[] guids)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            // that one is special because it works across content, media and member types
+            scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes);
+            return Repository.GetAllContentTypeAliases(guids);
+        }
+    }
+
+    /// 
+    ///     Gets all content type id for aliases across content, media and member types.
+    /// 
+    /// Aliases to look for.
+    /// All content type ids.
+    /// Beware! Works across content, media and member types.
+    public IEnumerable GetAllContentTypeIds(string[] aliases)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            // that one is special because it works across content, media and member types
+            scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes);
+            return Repository.GetAllContentTypeIds(aliases);
+        }
+    }
+
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var typeIdsA = typeIds.ToArray();
+            ContentService.DeleteOfTypes(typeIdsA);
+            ContentService.DeleteBlueprintsOfTypes(typeIdsA);
+            scope.Complete();
+        }
+    }
+
+    #region Notifications
+
+    protected override SavingNotification GetSavingNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeSavingNotification(item, eventMessages);
+
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeSavingNotification(items, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeSavedNotification(item, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeSavedNotification(items, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IContentType item,
+        EventMessages eventMessages) => new ContentTypeDeletingNotification(item, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeDeletingNotification(items, eventMessages);
+
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new ContentTypeDeletedNotification(items, eventMessages);
+
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new ContentTypeMovingNotification(moveInfo, eventMessages);
+
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new ContentTypeMovedNotification(moveInfo, eventMessages);
+
+    protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new ContentTypeChangedNotification(changes, eventMessages);
+
+    protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new ContentTypeRefreshedNotification(changes, eventMessages);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
index 1e97e02dca..7549cd849c 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs
@@ -2,12 +2,12 @@ using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public abstract class ContentTypeServiceBase : RepositoryService
 {
-    public abstract class ContentTypeServiceBase : RepositoryService
+    protected ContentTypeServiceBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        protected ContentTypeServiceBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory)
-            : base(provider, loggerFactory, eventMessagesFactory)
-        { }
     }
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
index ce49a4f9d3..98a7195fbf 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
@@ -5,1098 +5,1113 @@ using Umbraco.Cms.Core.Exceptions;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services.Changes;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeBaseService
+    where TRepository : IContentTypeRepositoryBase
+    where TItem : class, IContentTypeComposition
 {
-    public abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeBaseService
-        where TRepository : IContentTypeRepositoryBase
-        where TItem : class, IContentTypeComposition
+    private readonly IAuditRepository _auditRepository;
+    private readonly IEntityContainerRepository _containerRepository;
+    private readonly IEntityRepository _entityRepository;
+    private readonly IEventAggregator _eventAggregator;
+
+    protected ContentTypeServiceBase(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        TRepository repository,
+        IAuditRepository auditRepository,
+        IEntityContainerRepository containerRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IAuditRepository _auditRepository;
-        private readonly IEntityContainerRepository _containerRepository;
-        private readonly IEntityRepository _entityRepository;
-        private readonly IEventAggregator _eventAggregator;
+        Repository = repository;
+        _auditRepository = auditRepository;
+        _containerRepository = containerRepository;
+        _entityRepository = entityRepository;
+        _eventAggregator = eventAggregator;
+    }
 
-        protected ContentTypeServiceBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            TRepository repository, IAuditRepository auditRepository, IEntityContainerRepository containerRepository, IEntityRepository entityRepository,
-            IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    protected TRepository Repository { get; }
+    protected abstract int[] WriteLockIds { get; }
+    protected abstract int[] ReadLockIds { get; }
+
+    #region Notifications
+
+    protected abstract SavingNotification GetSavingNotification(TItem item, EventMessages eventMessages);
+    protected abstract SavingNotification GetSavingNotification(IEnumerable items, EventMessages eventMessages);
+
+    protected abstract SavedNotification GetSavedNotification(TItem item, EventMessages eventMessages);
+    protected abstract SavedNotification GetSavedNotification(IEnumerable items, EventMessages eventMessages);
+
+    protected abstract DeletingNotification GetDeletingNotification(TItem item, EventMessages eventMessages);
+    protected abstract DeletingNotification GetDeletingNotification(IEnumerable items, EventMessages eventMessages);
+
+    protected abstract DeletedNotification GetDeletedNotification(IEnumerable items, EventMessages eventMessages);
+
+    protected abstract MovingNotification GetMovingNotification(MoveEventInfo moveInfo, EventMessages eventMessages);
+
+    protected abstract MovedNotification GetMovedNotification(IEnumerable> moveInfo, EventMessages eventMessages);
+
+    protected abstract ContentTypeChangeNotification GetContentTypeChangedNotification(IEnumerable> changes, EventMessages eventMessages);
+
+    // This notification is identical to GetTypeChangeNotification, however it needs to be a different notification type because it's published within the transaction
+    /// The purpose of this notification being published within the transaction is so that listeners can perform database
+    /// operations from within the same transaction and guarantee data consistency so that if anything goes wrong
+    /// the entire transaction can be rolled back. This is used by Nucache.
+    protected abstract ContentTypeRefreshNotification GetContentTypeRefreshedNotification(IEnumerable> changes, EventMessages eventMessages);
+
+    #endregion
+
+    #region Validation
+
+    public Attempt ValidateComposition(TItem? compo)
+    {
+        try
         {
-            Repository = repository;
-            _auditRepository = auditRepository;
-            _containerRepository = containerRepository;
-            _entityRepository = entityRepository;
-            _eventAggregator = eventAggregator;
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            {
+                scope.ReadLock(ReadLockIds);
+                ValidateLocked(compo!);
+            }
+
+            return Attempt.Succeed();
+        }
+        catch (InvalidCompositionException ex)
+        {
+            return Attempt.Fail(ex.PropertyTypeAliases, ex);
+        }
+    }
+
+    protected void ValidateLocked(TItem compositionContentType)
+    {
+        // performs business-level validation of the composition
+        // should ensure that it is absolutely safe to save the composition
+
+        // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors)
+        // but that cannot be used (conflict with descendants)
+
+        IContentTypeComposition[] allContentTypes = Repository.GetMany(new int[0]).Cast().ToArray();
+
+        IEnumerable compositionAliases = compositionContentType.CompositionAliases();
+        IEnumerable compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y)));
+        var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias).ToArray();
+        var propertyGroupAliases = compositionContentType.PropertyGroups.ToDictionary(x => x.Alias, x => x.Type, StringComparer.InvariantCultureIgnoreCase);
+        IEnumerable indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id));
+        var comparer = new DelegateEqualityComparer((x, y) => x?.Id == y?.Id, x => x.Id);
+        var dependencies = new HashSet(compositions, comparer);
+
+        var stack = new Stack();
+        foreach (IContentTypeComposition indirectReference in indirectReferences)
+        {
+            stack.Push(indirectReference); // push indirect references to a stack, so we can add recursively
         }
 
-        protected TRepository Repository { get; }
-        protected abstract int[] WriteLockIds { get; }
-        protected abstract int[] ReadLockIds { get; }
-
-        #region Notifications
-
-        protected abstract SavingNotification GetSavingNotification(TItem item, EventMessages eventMessages);
-        protected abstract SavingNotification GetSavingNotification(IEnumerable items, EventMessages eventMessages);
-
-        protected abstract SavedNotification GetSavedNotification(TItem item, EventMessages eventMessages);
-        protected abstract SavedNotification GetSavedNotification(IEnumerable items, EventMessages eventMessages);
-
-        protected abstract DeletingNotification GetDeletingNotification(TItem item, EventMessages eventMessages);
-        protected abstract DeletingNotification GetDeletingNotification(IEnumerable items, EventMessages eventMessages);
-
-        protected abstract DeletedNotification GetDeletedNotification(IEnumerable items, EventMessages eventMessages);
-
-        protected abstract MovingNotification GetMovingNotification(MoveEventInfo moveInfo, EventMessages eventMessages);
-
-        protected abstract MovedNotification GetMovedNotification(IEnumerable> moveInfo, EventMessages eventMessages);
-
-        protected abstract ContentTypeChangeNotification GetContentTypeChangedNotification(IEnumerable> changes, EventMessages eventMessages);
-
-        // This notification is identical to GetTypeChangeNotification, however it needs to be a different notification type because it's published within the transaction
-        /// The purpose of this notification being published within the transaction is so that listeners can perform database
-        /// operations from within the same transaction and guarantee data consistency so that if anything goes wrong
-        /// the entire transaction can be rolled back. This is used by Nucache.
-        protected abstract ContentTypeRefreshNotification GetContentTypeRefreshedNotification(IEnumerable> changes, EventMessages eventMessages);
-
-        #endregion
-
-        #region Validation
-
-        public Attempt ValidateComposition(TItem? compo)
+        while (stack.Count > 0)
         {
-            try
+            IContentTypeComposition indirectReference = stack.Pop();
+            dependencies.Add(indirectReference);
+
+            // get all compositions for the current indirect reference
+            IEnumerable directReferences = indirectReference.ContentTypeComposition;
+            foreach (IContentTypeComposition directReference in directReferences)
             {
-                using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+                if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias))
                 {
-                    scope.ReadLock(ReadLockIds);
-                    ValidateLocked(compo!);
-                }
-                return Attempt.Succeed();
-            }
-            catch (InvalidCompositionException ex)
-            {
-                return Attempt.Fail(ex.PropertyTypeAliases, ex);
-            }
-        }
-
-        protected void ValidateLocked(TItem compositionContentType)
-        {
-            // performs business-level validation of the composition
-            // should ensure that it is absolutely safe to save the composition
-
-            // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors)
-            // but that cannot be used (conflict with descendants)
-
-            var allContentTypes = Repository.GetMany(new int[0]).Cast().ToArray();
-
-            var compositionAliases = compositionContentType.CompositionAliases();
-            var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y)));
-            var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias).ToArray();
-            var propertyGroupAliases = compositionContentType.PropertyGroups.ToDictionary(x => x.Alias, x => x.Type, StringComparer.InvariantCultureIgnoreCase);
-            var indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id));
-            var comparer = new DelegateEqualityComparer((x, y) => x?.Id == y?.Id, x => x.Id);
-            var dependencies = new HashSet(compositions, comparer);
-
-            var stack = new Stack();
-            foreach (var indirectReference in indirectReferences)
-                stack.Push(indirectReference); // push indirect references to a stack, so we can add recursively
-
-            while (stack.Count > 0)
-            {
-                var indirectReference = stack.Pop();
-                dependencies.Add(indirectReference);
-
-                // get all compositions for the current indirect reference
-                var directReferences = indirectReference.ContentTypeComposition;
-                foreach (var directReference in directReferences)
-                {
-                    if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias))
-                        continue;
-
-                    dependencies.Add(directReference);
-
-                    // a direct reference has compositions of its own - these also need to be taken into account
-                    var directReferenceGraph = directReference.CompositionAliases();
-                    foreach (var c in allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))))
-                        dependencies.Add(c);
+                    continue;
                 }
 
-                // recursive lookup of indirect references
-                foreach (var c in allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)))
-                    stack.Push(c);
+                dependencies.Add(directReference);
+
+                // a direct reference has compositions of its own - these also need to be taken into account
+                IEnumerable directReferenceGraph = directReference.CompositionAliases();
+                foreach (IContentTypeComposition c in allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))))
+                {
+                    dependencies.Add(c);
+                }
             }
 
-            var duplicatePropertyTypeAliases = new List();
-            var invalidPropertyGroupAliases = new List();
-
-            foreach (var dependency in dependencies)
+            // recursive lookup of indirect references
+            foreach (IContentTypeComposition c in allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)))
             {
-                if (dependency.Id == compositionContentType.Id)
-                    continue;
-
-                var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase));
-                if (contentTypeDependency == null)
-                    continue;
-
-                duplicatePropertyTypeAliases.AddRange(contentTypeDependency.PropertyTypes.Select(x => x.Alias).Intersect(propertyTypeAliases, StringComparer.InvariantCultureIgnoreCase));
-                invalidPropertyGroupAliases.AddRange(contentTypeDependency.PropertyGroups.Where(x => propertyGroupAliases.TryGetValue(x.Alias, out var type) && type != x.Type).Select(x => x.Alias));
-            }
-
-            if (duplicatePropertyTypeAliases.Count > 0 || invalidPropertyGroupAliases.Count > 0)
-
-            {
-                throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases.Distinct().ToArray(), invalidPropertyGroupAliases.Distinct().ToArray());
+                stack.Push(c);
             }
         }
 
-        #endregion
+        var duplicatePropertyTypeAliases = new List();
+        var invalidPropertyGroupAliases = new List();
 
-        #region Composition
-
-        internal IEnumerable> ComposeContentTypeChanges(params TItem[] contentTypes)
+        foreach (IContentTypeComposition dependency in dependencies)
         {
-            // find all content types impacted by the changes,
-            // - content type alias changed
-            // - content type property removed, or alias changed
-            // - content type composition removed (not testing if composition had properties...)
-            // - content type variation changed
-            // - property type variation changed
-            //
-            // because these are the changes that would impact the raw content data
-
-            // note
-            // this is meant to run *after* uow.Commit() so must use WasPropertyDirty() everywhere
-            // instead of IsPropertyDirty() since dirty properties have been reset already
-
-            var changes = new List>();
-
-            foreach (var contentType in contentTypes)
+            if (dependency.Id == compositionContentType.Id)
             {
-                var dirty = (IRememberBeingDirty)contentType;
+                continue;
+            }
 
-                // skip new content types
+            IContentTypeComposition? contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase));
+            if (contentTypeDependency == null)
+            {
+                continue;
+            }
+
+            duplicatePropertyTypeAliases.AddRange(contentTypeDependency.PropertyTypes.Select(x => x.Alias).Intersect(propertyTypeAliases, StringComparer.InvariantCultureIgnoreCase));
+            invalidPropertyGroupAliases.AddRange(contentTypeDependency.PropertyGroups.Where(x => propertyGroupAliases.TryGetValue(x.Alias, out PropertyGroupType type) && type != x.Type).Select(x => x.Alias));
+        }
+
+        if (duplicatePropertyTypeAliases.Count > 0 || invalidPropertyGroupAliases.Count > 0)
+
+        {
+            throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases.Distinct().ToArray(), invalidPropertyGroupAliases.Distinct().ToArray());
+        }
+    }
+
+    #endregion
+
+    #region Composition
+
+    internal IEnumerable> ComposeContentTypeChanges(params TItem[] contentTypes)
+    {
+        // find all content types impacted by the changes,
+        // - content type alias changed
+        // - content type property removed, or alias changed
+        // - content type composition removed (not testing if composition had properties...)
+        // - content type variation changed
+        // - property type variation changed
+        //
+        // because these are the changes that would impact the raw content data
+
+        // note
+        // this is meant to run *after* uow.Commit() so must use WasPropertyDirty() everywhere
+        // instead of IsPropertyDirty() since dirty properties have been reset already
+
+        var changes = new List>();
+
+        foreach (TItem contentType in contentTypes)
+        {
+            var dirty = (IRememberBeingDirty)contentType;
+
+            // skip new content types
+            // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly
+            var isNewContentType = dirty.WasPropertyDirty("Id");
+            if (isNewContentType)
+            {
+                AddChange(changes, contentType, ContentTypeChangeTypes.Create);
+                continue;
+            }
+
+            // alias change?
+            var hasAliasChanged = dirty.WasPropertyDirty("Alias");
+
+            // existing property alias change?
+            var hasAnyPropertyChangedAlias = contentType.PropertyTypes.Any(propertyType =>
+            {
+                // skip new properties
                 // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly
-                var isNewContentType = dirty.WasPropertyDirty("Id");
-                if (isNewContentType)
+                var isNewProperty = propertyType.WasPropertyDirty("Id");
+                if (isNewProperty)
                 {
-                    AddChange(changes, contentType, ContentTypeChangeTypes.Create);
-                    continue;
+                    return false;
                 }
 
                 // alias change?
-                var hasAliasChanged = dirty.WasPropertyDirty("Alias");
+                return propertyType.WasPropertyDirty("Alias");
+            });
 
-                // existing property alias change?
-                var hasAnyPropertyChangedAlias = contentType.PropertyTypes.Any(propertyType =>
+            // removed properties?
+            var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
+
+            // removed compositions?
+            var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
+
+            // variation changed?
+            var hasContentTypeVariationChanged = dirty.WasPropertyDirty("Variations");
+
+            // property variation change?
+            var hasAnyPropertyVariationChanged = contentType.WasPropertyTypeVariationChanged();
+
+            // main impact on properties?
+            var hasPropertyMainImpact = hasContentTypeVariationChanged || hasAnyPropertyVariationChanged
+                                                                       || hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias;
+
+            if (hasAliasChanged || hasPropertyMainImpact)
+            {
+                // add that one, as a main change
+                AddChange(changes, contentType, ContentTypeChangeTypes.RefreshMain);
+
+                if (hasPropertyMainImpact)
                 {
-                    // skip new properties
-                    // TODO: This used to be WasPropertyDirty("HasIdentity") but i don't think that actually worked for detecting new entities this does seem to work properly
-                    var isNewProperty = propertyType.WasPropertyDirty("Id");
-                    if (isNewProperty) return false;
-
-                    // alias change?
-                    return propertyType.WasPropertyDirty("Alias");
-                });
-
-                // removed properties?
-                var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
-
-                // removed compositions?
-                var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
-
-                // variation changed?
-                var hasContentTypeVariationChanged = dirty.WasPropertyDirty("Variations");
-
-                // property variation change?
-                var hasAnyPropertyVariationChanged = contentType.WasPropertyTypeVariationChanged();
-
-                // main impact on properties?
-                var hasPropertyMainImpact = hasContentTypeVariationChanged || hasAnyPropertyVariationChanged
-                    || hasAnyCompositionBeenRemoved || hasAnyPropertyBeenRemoved || hasAnyPropertyChangedAlias;
-
-                if (hasAliasChanged || hasPropertyMainImpact)
-                {
-                    // add that one, as a main change
-                    AddChange(changes, contentType, ContentTypeChangeTypes.RefreshMain);
-
-                    if (hasPropertyMainImpact)
-                        foreach (var c in GetComposedOf(contentType.Id))
-                            AddChange(changes, c, ContentTypeChangeTypes.RefreshMain);
-                }
-                else
-                {
-                    // add that one, as an other change
-                    AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther);
-                }
-            }
-
-            return changes;
-        }
-
-        // ensures changes contains no duplicates
-        private static void AddChange(ICollection> changes, TItem contentType, ContentTypeChangeTypes changeTypes)
-        {
-            var change = changes.FirstOrDefault(x => x.Item == contentType);
-            if (change == null)
-            {
-                changes.Add(new ContentTypeChange(contentType, changeTypes));
-                return;
-            }
-            change.ChangeTypes |= changeTypes;
-        }
-
-        #endregion
-
-        #region Get, Has, Is, Count
-
-        IContentTypeComposition? IContentTypeBaseService.Get(int id)
-        {
-            return Get(id);
-        }
-
-        public TItem? Get(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(id);
-            }
-        }
-
-        public TItem? Get(string alias)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(alias);
-            }
-        }
-
-        public TItem? Get(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Get(id);
-            }
-        }
-
-        public IEnumerable GetAll(params int[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.GetMany(ids);
-            }
-        }
-
-        public IEnumerable GetAll(IEnumerable? ids)
-        {
-            if (ids is null)
-            {
-                return Enumerable.Empty();
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.GetMany(ids.ToArray());
-            }
-        }
-
-        public IEnumerable GetChildren(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var query = Query().Where(x => x.ParentId == id);
-                return Repository.Get(query);
-            }
-        }
-
-        public IEnumerable GetChildren(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var found = Get(id);
-                if (found == null) return Enumerable.Empty();
-                var query = Query().Where(x => x.ParentId == found.Id);
-                return Repository.Get(query);
-            }
-        }
-
-        public bool HasChildren(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var query = Query().Where(x => x.ParentId == id);
-                var count = Repository.Count(query);
-                return count > 0;
-            }
-        }
-
-        public bool HasChildren(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                var found = Get(id);
-                if (found == null) return false;
-                var query = Query().Where(x => x.ParentId == found.Id);
-                var count = Repository.Count(query);
-                return count > 0;
-            }
-        }
-
-        /// 
-        /// Given the path of a content item, this will return true if the content item exists underneath a list view content item
-        /// 
-        /// 
-        /// 
-        public bool HasContainerInPath(string contentPath)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // can use same repo for both content and media
-                return Repository.HasContainerInPath(contentPath);
-            }
-        }
-
-        public bool HasContainerInPath(params int[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                // can use same repo for both content and media
-                return Repository.HasContainerInPath(ids);
-            }
-        }
-
-        public IEnumerable GetDescendants(int id, bool andSelf)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-
-                var descendants = new List();
-                if (andSelf)
-                {
-                    var self = Repository.Get(id);
-                    if (self is not null)
+                    foreach (TItem c in GetComposedOf(contentType.Id))
                     {
-                        descendants.Add(self);
+                        AddChange(changes, c, ContentTypeChangeTypes.RefreshMain);
                     }
                 }
-                var ids = new Stack();
-                ids.Push(id);
-
-                while (ids.Count > 0)
-                {
-                    var i = ids.Pop();
-                    var query = Query().Where(x => x.ParentId == i);
-                    var result = Repository.Get(query).ToArray();
-
-                    if (result is not null)
-                    {
-                        foreach (var c in result)
-                        {
-                            descendants.Add(c);
-                            ids.Push(c.Id);
-                        }
-                    }
-                }
-
-                return descendants.ToArray();
-            }
-        }
-
-        public IEnumerable GetComposedOf(int id, IEnumerable all)
-        {
-            return all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id));
-
-        }
-
-        public IEnumerable GetComposedOf(int id)
-        {
-            // GetAll is cheap, repository has a full dataset cache policy
-            // TODO: still, because it uses the cache, race conditions!
-            var allContentTypes = GetAll(Array.Empty());
-            return GetComposedOf(id, allContentTypes);
-        }
-
-        public int Count()
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.Count(Query());
-            }
-        }
-
-        public bool HasContentNodes(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds);
-                return Repository.HasContentNodes(id);
-            }
-        }
-
-        #endregion
-
-        #region Save
-
-        public void Save(TItem? item, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            if (item is null)
-            {
-                return;
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                SavingNotification savingNotification = GetSavingNotification(item, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (string.IsNullOrWhiteSpace(item.Name))
-                {
-                    throw new ArgumentException("Cannot save item with empty name.");
-                }
-
-                if (item.Name != null && item.Name.Length > 255)
-                {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-                }
-
-                scope.WriteLock(WriteLockIds);
-
-                // validate the DAG transform, within the lock
-                ValidateLocked(item); // throws if invalid
-
-                item.CreatorId = userId;
-                if (item.Description == string.Empty)
-                {
-                    item.Description = null;
-                }
-
-                Repository.Save(item); // also updates content/media/member items
-
-                // figure out impacted content types
-                ContentTypeChange[] changes = ComposeContentTypeChanges(item).ToArray();
-
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
-
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                SavedNotification savedNotification = GetSavedNotification(item, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
-
-                Audit(AuditType.Save, userId, item.Id);
-                scope.Complete();
-            }
-        }
-
-        public void Save(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            TItem[] itemsA = items.ToArray();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                SavingNotification savingNotification = GetSavingNotification(itemsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(WriteLockIds);
-
-                // all-or-nothing, validate them all first
-                foreach (TItem contentType in itemsA)
-                {
-                    ValidateLocked(contentType); // throws if invalid
-                }
-                foreach (TItem contentType in itemsA)
-                {
-                    contentType.CreatorId = userId;
-                    if (contentType.Description == string.Empty)
-                    {
-                        contentType.Description = null;
-                    }
-
-                    Repository.Save(contentType);
-                }
-
-                // figure out impacted content types
-                ContentTypeChange[] changes = ComposeContentTypeChanges(itemsA).ToArray();
-
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages)); ;
-
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                SavedNotification savedNotification = GetSavedNotification(itemsA, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
-
-                Audit(AuditType.Save, userId, -1);
-                scope.Complete();
-            }
-        }
-
-        #endregion
-
-        #region Delete
-
-        public void Delete(TItem item, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                DeletingNotification deletingNotification = GetDeletingNotification(item, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(WriteLockIds);
-
-                // all descendants are going to be deleted
-                TItem[] descendantsAndSelf = GetDescendants(item.Id, true)
-                    .ToArray();
-                TItem[] deleted = descendantsAndSelf;
-
-                // all impacted (through composition) probably lose some properties
-                // don't try to be too clever here, just report them all
-                // do this before anything is deleted
-                TItem[] changed = descendantsAndSelf.SelectMany(xx => GetComposedOf(xx.Id))
-                    .Distinct()
-                    .Except(descendantsAndSelf)
-                    .ToArray();
-
-                // delete content
-                DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id));
-
-                // Next find all other document types that have a reference to this content type
-                IEnumerable referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes?.Any(p=>p.Id.Value==item.Id) ?? false);
-                foreach (TItem reference in referenceToAllowedContentTypes)
-                {
-                    reference.AllowedContentTypes = reference.AllowedContentTypes?.Where(p => p.Id.Value != item.Id);
-                    var changedRef = new List>() { new ContentTypeChange(reference, ContentTypeChangeTypes.RefreshMain) };
-                    // Fire change event
-                    scope.Notifications.Publish(GetContentTypeChangedNotification(changedRef, eventMessages));
-                }
-
-                // finally delete the content type
-                // - recursively deletes all descendants
-                // - deletes all associated property data
-                //  (contents of any descendant type have been deleted but
-                //   contents of any composed (impacted) type remain but
-                //   need to have their property data cleared)
-                Repository.Delete(item);
-
-                ContentTypeChange[] changes = descendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove))
-                    .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther)))
-                    .ToArray();
-
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
-
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
-
-                Audit(AuditType.Delete, userId, item.Id);
-                scope.Complete();
-            }
-        }
-
-        public void Delete(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            TItem[] itemsA = items.ToArray();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                DeletingNotification deletingNotification = GetDeletingNotification(itemsA, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(WriteLockIds);
-
-                // all descendants are going to be deleted
-                TItem[] allDescendantsAndSelf = itemsA.SelectMany(xx => GetDescendants(xx.Id, true)).DistinctBy(x => x.Id).ToArray();
-                TItem[] deleted = allDescendantsAndSelf;
-
-                // all impacted (through composition) probably lose some properties
-                // don't try to be too clever here, just report them all
-                // do this before anything is deleted
-                TItem[] changed = allDescendantsAndSelf.SelectMany(x => GetComposedOf(x.Id))
-                    .Distinct()
-                    .Except(allDescendantsAndSelf)
-                    .ToArray();
-
-                // delete content
-                DeleteItemsOfTypes(allDescendantsAndSelf.Select(x => x.Id));
-
-                // finally delete the content types
-                // (see notes in overload)
-                foreach (TItem item in itemsA)
-                {
-                    Repository.Delete(item);
-                }
-
-                ContentTypeChange[] changes = allDescendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove))
-                    .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther)))
-                    .ToArray();
-
-                // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
-                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
-
-                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
-
-                Audit(AuditType.Delete, userId, -1);
-                scope.Complete();
-            }
-        }
-
-        protected abstract void DeleteItemsOfTypes(IEnumerable typeIds);
-
-        #endregion
-
-        #region Copy
-
-        public TItem Copy(TItem original, string alias, string name, int parentId = -1)
-        {
-            TItem? parent = null;
-            if (parentId > 0)
-            {
-                parent = Get(parentId);
-                if (parent == null)
-                {
-                    throw new InvalidOperationException("Could not find parent with id " + parentId);
-                }
-            }
-            return Copy(original, alias, name, parent);
-        }
-
-        public TItem Copy(TItem original, string alias, string name, TItem? parent)
-        {
-            if (original == null) throw new ArgumentNullException(nameof(original));
-            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));
-            if (parent != null && parent.HasIdentity == false) throw new InvalidOperationException("Parent must have an identity.");
-
-            // this is illegal
-            //var originalb = (ContentTypeCompositionBase)original;
-            // but we *know* it has to be a ContentTypeCompositionBase anyways
-            var originalb = (ContentTypeCompositionBase) (object) original;
-            var clone = (TItem) (object) originalb.DeepCloneWithResetIdentities(alias);
-
-            clone.Name = name;
-
-            //remove all composition that is not it's current alias
-            var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList();
-            foreach (var a in compositionAliases)
-            {
-                clone.RemoveContentType(a);
-            }
-
-            //if a parent is specified set it's composition and parent
-            if (parent != null)
-            {
-                //add a new parent composition
-                clone.AddContentType(parent);
-                clone.ParentId = parent.Id;
             }
             else
             {
-                //set to root
-                clone.ParentId = -1;
+                // add that one, as an other change
+                AddChange(changes, contentType, ContentTypeChangeTypes.RefreshOther);
             }
-
-            Save(clone);
-            return clone;
         }
 
-        public Attempt?> Copy(TItem copying, int containerId)
+        return changes;
+    }
+
+    // ensures changes contains no duplicates
+    private static void AddChange(ICollection> changes, TItem contentType, ContentTypeChangeTypes changeTypes)
+    {
+        ContentTypeChange? change = changes.FirstOrDefault(x => x.Item == contentType);
+        if (change == null)
         {
-            var eventMessages = EventMessagesFactory.Get();
-
-            TItem copy;
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(WriteLockIds);
-
-                try
-                {
-                    if (containerId > 0)
-                    {
-                        var container = _containerRepository?.Get(containerId);
-                        if (container == null)
-                            throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
-                    }
-                    var alias = Repository.GetUniqueAlias(copying.Alias);
-
-                    // this is illegal
-                    //var copyingb = (ContentTypeCompositionBase) copying;
-                    // but we *know* it has to be a ContentTypeCompositionBase anyways
-                    var copyingb = (ContentTypeCompositionBase) (object)copying;
-                    copy = (TItem) (object) copyingb.DeepCloneWithResetIdentities(alias);
-
-                    copy.Name = copy.Name + " (copy)"; // might not be unique
-
-                    // if it has a parent, and the parent is a content type, unplug composition
-                    // all other compositions remain in place in the copied content type
-                    if (copy.ParentId > 0)
-                    {
-                        var parent = Repository.Get(copy.ParentId);
-                        if (parent != null)
-                            copy.RemoveContentType(parent.Alias);
-                    }
-
-                    copy.ParentId = containerId;
-
-                    SavingNotification savingNotification = GetSavingNotification(copy, eventMessages);
-                    if (scope.Notifications.PublishCancelable(savingNotification))
-                    {
-                        scope.Complete();
-                        return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages, copy);
-                    }
-
-                    Repository.Save(copy);
-
-                    ContentTypeChange[] changes = ComposeContentTypeChanges(copy).ToArray();
-
-                    _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
-                    scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
-
-                    SavedNotification savedNotification = GetSavedNotification(copy, eventMessages);
-                    savedNotification.WithStateFrom(savingNotification);
-                    scope.Notifications.Publish(savedNotification);
-
-                    scope.Complete();
-                }
-                catch (DataOperationException ex)
-                {
-                    return OperationResult.Attempt.Fail(ex.Operation, eventMessages); // causes rollback
-                }
-            }
-
-            return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages, copy);
+            changes.Add(new ContentTypeChange(contentType, changeTypes));
+            return;
         }
 
-        #endregion
+        change.ChangeTypes |= changeTypes;
+    }
 
-        #region Move
+    #endregion
 
-        public Attempt?> Move(TItem moving, int containerId)
+    #region Get, Has, Is, Count
+
+    IContentTypeComposition? IContentTypeBaseService.Get(int id)
+    {
+        return Get(id);
+    }
+
+    public TItem? Get(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(id);
+    }
+
+    public TItem? Get(string alias)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(alias);
+    }
+
+    public TItem? Get(Guid id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Get(id);
+    }
+
+    public IEnumerable GetAll(params int[] ids)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.GetMany(ids);
+    }
+
+    public IEnumerable GetAll(IEnumerable? ids)
+    {
+        if (ids is null)
+        {
+            return Enumerable.Empty();
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+
+        {
+            scope.ReadLock(ReadLockIds);
+            return Repository.GetMany(ids.ToArray());
+        }
+    }
+
+    public IEnumerable GetChildren(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        IQuery query = Query().Where(x => x.ParentId == id);
+        return Repository.Get(query);
+    }
+
+    public IEnumerable GetChildren(Guid id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        TItem? found = Get(id);
+        if (found == null)
+        {
+            return Enumerable.Empty();
+        }
+
+        IQuery query = Query().Where(x => x.ParentId == found.Id);
+        return Repository.Get(query);
+    }
+
+    public bool HasChildren(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        IQuery query = Query().Where(x => x.ParentId == id);
+        var count = Repository.Count(query);
+        return count > 0;
+    }
+
+    public bool HasChildren(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.ReadLock(ReadLockIds);
+            TItem? found = Get(id);
+            if (found == null)
+            {
+                return false;
+            }
+
+            IQuery query = Query().Where(x => x.ParentId == found.Id);
+            var count = Repository.Count(query);
+            return count > 0;
+        }
+    }
+
+    /// 
+    /// Given the path of a content item, this will return true if the content item exists underneath a list view content item
+    /// 
+    /// 
+    /// 
+    public bool HasContainerInPath(string contentPath)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        // can use same repo for both content and media
+        return Repository.HasContainerInPath(contentPath);
+    }
+
+    public bool HasContainerInPath(params int[] ids)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        // can use same repo for both content and media
+        return Repository.HasContainerInPath(ids);
+    }
+
+    public IEnumerable GetDescendants(int id, bool andSelf)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+
+        var descendants = new List();
+        if (andSelf)
+        {
+            TItem? self = Repository.Get(id);
+            if (self is not null)
+            {
+                descendants.Add(self);
+            }
+        }
+
+        var ids = new Stack();
+        ids.Push(id);
+
+        while (ids.Count > 0)
+        {
+            var i = ids.Pop();
+            IQuery query = Query().Where(x => x.ParentId == i);
+            TItem[]? result = Repository.Get(query).ToArray();
+
+            if (result is not null)
+            {
+                foreach (TItem c in result)
+                {
+                    descendants.Add(c);
+                    ids.Push(c.Id);
+                }
+            }
+        }
+
+        return descendants.ToArray();
+    }
+
+    public IEnumerable GetComposedOf(int id, IEnumerable all) =>
+        all.Where(x => x.ContentTypeComposition.Any(y => y.Id == id));
+
+    public IEnumerable GetComposedOf(int id)
+    {
+        // GetAll is cheap, repository has a full dataset cache policy
+        // TODO: still, because it uses the cache, race conditions!
+        IEnumerable allContentTypes = GetAll(Array.Empty());
+        return GetComposedOf(id, allContentTypes);
+    }
+
+    public int Count()
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.Count(Query());
+    }
+
+    public bool HasContentNodes(int id)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds);
+        return Repository.HasContentNodes(id);
+    }
+
+    #endregion
+
+    #region Save
+
+    public void Save(TItem? item, int userId = Constants.Security.SuperUserId)
+    {
+        if (item is null)
+        {
+            return;
+        }
+
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        SavingNotification savingNotification = GetSavingNotification(item, eventMessages);
+        if (scope.Notifications.PublishCancelable(savingNotification))
+        {
+            scope.Complete();
+            return;
+        }
+
+        if (string.IsNullOrWhiteSpace(item.Name))
+        {
+            throw new ArgumentException("Cannot save item with empty name.");
+        }
+
+        if (item.Name != null && item.Name.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+        }
+
+        scope.WriteLock(WriteLockIds);
+
+        // validate the DAG transform, within the lock
+        ValidateLocked(item); // throws if invalid
+
+        item.CreatorId = userId;
+        if (item.Description == string.Empty)
+        {
+            item.Description = null;
+        }
+
+        Repository.Save(item); // also updates content/media/member items
+
+        // figure out impacted content types
+        ContentTypeChange[] changes = ComposeContentTypeChanges(item).ToArray();
+
+        // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+        _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+
+        scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+        SavedNotification savedNotification = GetSavedNotification(item, eventMessages);
+        savedNotification.WithStateFrom(savingNotification);
+        scope.Notifications.Publish(savedNotification);
+
+        Audit(AuditType.Save, userId, item.Id);
+        scope.Complete();
+    }
+
+    public void Save(IEnumerable items, int userId = Constants.Security.SuperUserId)
+    {
+        TItem[] itemsA = items.ToArray();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
             EventMessages eventMessages = EventMessagesFactory.Get();
-
-            var moveInfo = new List>();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            SavingNotification savingNotification = GetSavingNotification(itemsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                var moveEventInfo = new MoveEventInfo(moving, moving.Path, containerId);
-                MovingNotification movingNotification = GetMovingNotification(moveEventInfo, eventMessages);
-                if (scope.Notifications.PublishCancelable(movingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages);
-                }
-
-                scope.WriteLock(WriteLockIds); // also for containers
-
-                try
-                {
-                    EntityContainer? container = null;
-                    if (containerId > 0)
-                    {
-                        container = _containerRepository?.Get(containerId);
-                        if (container == null)
-                            throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
-                    }
-                    moveInfo.AddRange(Repository.Move(moving, container!));
-                    scope.Complete();
-                }
-                catch (DataOperationException ex)
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Fail(ex.Operation, eventMessages);
-                }
-
-                // note: not raising any Changed event here because moving a content type under another container
-                // has no impact on the published content types - would be entirely different if we were to support
-                // moving a content type under another content type.
-                MovedNotification movedNotification = GetMovedNotification(moveInfo, eventMessages);
-                movedNotification.WithStateFrom(movingNotification);
-                scope.Notifications.Publish(movedNotification);
+                scope.Complete();
+                return;
             }
 
-            return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages);
+            scope.WriteLock(WriteLockIds);
+
+            // all-or-nothing, validate them all first
+            foreach (TItem contentType in itemsA)
+            {
+                ValidateLocked(contentType); // throws if invalid
+            }
+            foreach (TItem contentType in itemsA)
+            {
+                contentType.CreatorId = userId;
+                if (contentType.Description == string.Empty)
+                {
+                    contentType.Description = null;
+                }
+
+                Repository.Save(contentType);
+            }
+
+            // figure out impacted content types
+            ContentTypeChange[] changes = ComposeContentTypeChanges(itemsA).ToArray();
+
+            // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+            _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+            SavedNotification savedNotification = GetSavedNotification(itemsA, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
+
+            Audit(AuditType.Save, userId, -1);
+            scope.Complete();
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Containers
+    #region Delete
 
-        protected abstract Guid ContainedObjectType { get; }
-
-        protected Guid ContainerObjectType => EntityContainer.GetContainerObjectType(ContainedObjectType);
-
-        public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
+    public void Delete(TItem item, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
             EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            DeletingNotification deletingNotification = GetDeletingNotification(item, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                scope.WriteLock(WriteLockIds); // also for containers
-
-                try
-                {
-                    var container = new EntityContainer(ContainedObjectType)
-                    {
-                        Name = name,
-                        ParentId = parentId,
-                        CreatorId = userId,
-                        Key = key
-                    };
-
-                    var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
-                    if (scope.Notifications.PublishCancelable(savingNotification))
-                    {
-                        scope.Complete();
-                        return OperationResult.Attempt.Cancel(eventMessages, container);
-                    }
-
-                    _containerRepository?.Save(container);
-                    scope.Complete();
-
-                    var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
-                    savedNotification.WithStateFrom(savingNotification);
-                    scope.Notifications.Publish(savedNotification);
-                    // TODO: Audit trail ?
-
-                    return OperationResult.Attempt.Succeed(eventMessages, container);
-                }
-                catch (Exception ex)
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Fail(OperationResultType.FailedCancelledByEvent, eventMessages, ex);
-                }
+                scope.Complete();
+                return;
             }
-        }
 
-        public Attempt SaveContainer(EntityContainer container, int userId = Cms.Core.Constants.Security.SuperUserId)
+            scope.WriteLock(WriteLockIds);
+
+            // all descendants are going to be deleted
+            TItem[] descendantsAndSelf = GetDescendants(item.Id, true)
+                .ToArray();
+            TItem[] deleted = descendantsAndSelf;
+
+            // all impacted (through composition) probably lose some properties
+            // don't try to be too clever here, just report them all
+            // do this before anything is deleted
+            TItem[] changed = descendantsAndSelf.SelectMany(xx => GetComposedOf(xx.Id))
+                .Distinct()
+                .Except(descendantsAndSelf)
+                .ToArray();
+
+            // delete content
+            DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id));
+
+            // Next find all other document types that have a reference to this content type
+            IEnumerable referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes?.Any(p=>p.Id.Value==item.Id) ?? false);
+            foreach (TItem reference in referenceToAllowedContentTypes)
+            {
+                reference.AllowedContentTypes = reference.AllowedContentTypes?.Where(p => p.Id.Value != item.Id);
+                var changedRef = new List>() { new ContentTypeChange(reference, ContentTypeChangeTypes.RefreshMain) };
+                // Fire change event
+                scope.Notifications.Publish(GetContentTypeChangedNotification(changedRef, eventMessages));
+            }
+
+            // finally delete the content type
+            // - recursively deletes all descendants
+            // - deletes all associated property data
+            //  (contents of any descendant type have been deleted but
+            //   contents of any composed (impacted) type remain but
+            //   need to have their property data cleared)
+            Repository.Delete(item);
+
+            ContentTypeChange[] changes = descendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove))
+                .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther)))
+                .ToArray();
+
+            // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+            _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+            DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
+            deletedNotification.WithStateFrom(deletingNotification);
+            scope.Notifications.Publish(deletedNotification);
+
+            Audit(AuditType.Delete, userId, item.Id);
+            scope.Complete();
+        }
+    }
+
+    public void Delete(IEnumerable items, int userId = Constants.Security.SuperUserId)
+    {
+        TItem[] itemsA = items.ToArray();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
             EventMessages eventMessages = EventMessagesFactory.Get();
-
-            Guid containerObjectType = ContainerObjectType;
-            if (container.ContainerObjectType != containerObjectType)
+            DeletingNotification deletingNotification = GetDeletingNotification(itemsA, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                var ex = new InvalidOperationException("Not a container of the proper type.");
-                return OperationResult.Attempt.Fail(eventMessages, ex);
+                scope.Complete();
+                return;
             }
 
-            if (container.HasIdentity && container.IsPropertyDirty("ParentId"))
+            scope.WriteLock(WriteLockIds);
+
+            // all descendants are going to be deleted
+            TItem[] allDescendantsAndSelf = itemsA.SelectMany(xx => GetDescendants(xx.Id, true)).DistinctBy(x => x.Id).ToArray();
+            TItem[] deleted = allDescendantsAndSelf;
+
+            // all impacted (through composition) probably lose some properties
+            // don't try to be too clever here, just report them all
+            // do this before anything is deleted
+            TItem[] changed = allDescendantsAndSelf.SelectMany(x => GetComposedOf(x.Id))
+                .Distinct()
+                .Except(allDescendantsAndSelf)
+                .ToArray();
+
+            // delete content
+            DeleteItemsOfTypes(allDescendantsAndSelf.Select(x => x.Id));
+
+            // finally delete the content types
+            // (see notes in overload)
+            foreach (TItem item in itemsA)
             {
-                var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead.");
-                return OperationResult.Attempt.Fail(eventMessages, ex);
+                Repository.Delete(item);
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            ContentTypeChange[] changes = allDescendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove))
+                .Concat(changed.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther)))
+                .ToArray();
+
+            // Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
+            _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+
+            scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+            DeletedNotification deletedNotification = GetDeletedNotification(deleted.DistinctBy(x => x.Id), eventMessages);
+            deletedNotification.WithStateFrom(deletingNotification);
+            scope.Notifications.Publish(deletedNotification);
+
+            Audit(AuditType.Delete, userId, -1);
+            scope.Complete();
+        }
+    }
+
+    protected abstract void DeleteItemsOfTypes(IEnumerable typeIds);
+
+    #endregion
+
+    #region Copy
+
+    public TItem Copy(TItem original, string alias, string name, int parentId = -1)
+    {
+        TItem? parent = null;
+        if (parentId > 0)
+        {
+            parent = Get(parentId);
+            if (parent == null)
             {
-                var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
+                throw new InvalidOperationException("Could not find parent with id " + parentId);
+            }
+        }
+        return Copy(original, alias, name, parent);
+    }
+
+    public TItem Copy(TItem original, string alias, string name, TItem? parent)
+    {
+        if (original == null)
+        {
+            throw new ArgumentNullException(nameof(original));
+        }
+
+        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));
+        }
+
+        if (parent != null && parent.HasIdentity == false)
+        {
+            throw new InvalidOperationException("Parent must have an identity.");
+        }
+
+        // this is illegal
+        //var originalb = (ContentTypeCompositionBase)original;
+        // but we *know* it has to be a ContentTypeCompositionBase anyways
+        var originalb = (ContentTypeCompositionBase) (object) original;
+        var clone = (TItem) (object) originalb.DeepCloneWithResetIdentities(alias);
+
+        clone.Name = name;
+
+        //remove all composition that is not it's current alias
+        var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList();
+        foreach (var a in compositionAliases)
+        {
+            clone.RemoveContentType(a);
+        }
+
+        //if a parent is specified set it's composition and parent
+        if (parent != null)
+        {
+            //add a new parent composition
+            clone.AddContentType(parent);
+            clone.ParentId = parent.Id;
+        }
+        else
+        {
+            //set to root
+            clone.ParentId = -1;
+        }
+
+        Save(clone);
+        return clone;
+    }
+
+    public Attempt?> Copy(TItem copying, int containerId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        TItem copy;
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(WriteLockIds);
+
+            try
+            {
+                if (containerId > 0)
+                {
+                    EntityContainer? container = _containerRepository?.Get(containerId);
+                    if (container == null)
+                    {
+                        throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                    }
+                }
+
+                var alias = Repository.GetUniqueAlias(copying.Alias);
+
+                // this is illegal
+                //var copyingb = (ContentTypeCompositionBase) copying;
+                // but we *know* it has to be a ContentTypeCompositionBase anyways
+                var copyingb = (ContentTypeCompositionBase) (object)copying;
+                copy = (TItem) (object) copyingb.DeepCloneWithResetIdentities(alias);
+
+                copy.Name = copy.Name + " (copy)"; // might not be unique
+
+                // if it has a parent, and the parent is a content type, unplug composition
+                // all other compositions remain in place in the copied content type
+                if (copy.ParentId > 0)
+                {
+                    TItem? parent = Repository.Get(copy.ParentId);
+                    if (parent != null)
+                    {
+                        copy.RemoveContentType(parent.Alias);
+                    }
+                }
+
+                copy.ParentId = containerId;
+
+                SavingNotification savingNotification = GetSavingNotification(copy, eventMessages);
                 if (scope.Notifications.PublishCancelable(savingNotification))
                 {
                     scope.Complete();
-                    return OperationResult.Attempt.Cancel(eventMessages);
+                    return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages, copy);
                 }
 
-                scope.WriteLock(WriteLockIds); // also for containers
+                Repository.Save(copy);
+
+                ContentTypeChange[] changes = ComposeContentTypeChanges(copy).ToArray();
+
+                _eventAggregator.Publish(GetContentTypeRefreshedNotification(changes, eventMessages));
+                scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
+
+                SavedNotification savedNotification = GetSavedNotification(copy, eventMessages);
+                savedNotification.WithStateFrom(savingNotification);
+                scope.Notifications.Publish(savedNotification);
+
+                scope.Complete();
+            }
+            catch (DataOperationException ex)
+            {
+                return OperationResult.Attempt.Fail(ex.Operation, eventMessages); // causes rollback
+            }
+        }
+
+        return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages, copy);
+    }
+
+    #endregion
+
+    #region Move
+
+    public Attempt?> Move(TItem moving, int containerId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        var moveInfo = new List>();
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var moveEventInfo = new MoveEventInfo(moving, moving.Path, containerId);
+            MovingNotification movingNotification = GetMovingNotification(moveEventInfo, eventMessages);
+            if (scope.Notifications.PublishCancelable(movingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, eventMessages);
+            }
+
+            scope.WriteLock(WriteLockIds); // also for containers
+
+            try
+            {
+                EntityContainer? container = null;
+                if (containerId > 0)
+                {
+                    container = _containerRepository?.Get(containerId);
+                    if (container == null)
+                    {
+                        throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                    }
+                }
+                moveInfo.AddRange(Repository.Move(moving, container!));
+                scope.Complete();
+            }
+            catch (DataOperationException ex)
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Fail(ex.Operation, eventMessages);
+            }
+
+            // note: not raising any Changed event here because moving a content type under another container
+            // has no impact on the published content types - would be entirely different if we were to support
+            // moving a content type under another content type.
+            MovedNotification movedNotification = GetMovedNotification(moveInfo, eventMessages);
+            movedNotification.WithStateFrom(movingNotification);
+            scope.Notifications.Publish(movedNotification);
+        }
+
+        return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages);
+    }
+
+    #endregion
+
+    #region Containers
+
+    protected abstract Guid ContainedObjectType { get; }
+
+    protected Guid ContainerObjectType => EntityContainer.GetContainerObjectType(ContainedObjectType);
+
+    public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+        scope.WriteLock(WriteLockIds); // also for containers
+
+        try
+        {
+            var container = new EntityContainer(ContainedObjectType)
+            {
+                Name = name,
+                ParentId = parentId,
+                CreatorId = userId,
+                Key = key
+            };
+
+            var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages, container);
+            }
+
+            _containerRepository?.Save(container);
+            scope.Complete();
+
+            var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
+            // TODO: Audit trail ?
+
+            return OperationResult.Attempt.Succeed(eventMessages, container);
+        }
+        catch (Exception ex)
+        {
+            scope.Complete();
+            return OperationResult.Attempt.Fail(OperationResultType.FailedCancelledByEvent, eventMessages, ex);
+        }
+    }
+
+    public Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        Guid containerObjectType = ContainerObjectType;
+        if (container.ContainerObjectType != containerObjectType)
+        {
+            var ex = new InvalidOperationException("Not a container of the proper type.");
+            return OperationResult.Attempt.Fail(eventMessages, ex);
+        }
+
+        if (container.HasIdentity && container.IsPropertyDirty("ParentId"))
+        {
+            var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead.");
+            return OperationResult.Attempt.Fail(eventMessages, ex);
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new EntityContainerSavingNotification(container, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages);
+            }
+
+            scope.WriteLock(WriteLockIds); // also for containers
+
+            _containerRepository?.Save(container);
+            scope.Complete();
+
+            var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
+            savedNotification.WithStateFrom(savingNotification);
+            scope.Notifications.Publish(savedNotification);
+        }
+
+        // TODO: Audit trail ?
+
+        return OperationResult.Attempt.Succeed(eventMessages);
+    }
+
+    public EntityContainer? GetContainer(int containerId)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
+
+        return _containerRepository.Get(containerId);
+    }
+
+    public EntityContainer? GetContainer(Guid containerId)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
+
+        return _containerRepository.Get(containerId);
+    }
+
+    public IEnumerable GetContainers(int[] containerIds)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
+
+        return _containerRepository.GetMany(containerIds);
+    }
+
+    public IEnumerable GetContainers(TItem item)
+    {
+        var ancestorIds = item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
+            .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt) ? asInt : int.MinValue)
+            .Where(x => x != int.MinValue && x != item.Id)
+            .ToArray();
+
+        return GetContainers(ancestorIds);
+    }
+
+    public IEnumerable GetContainers(string name, int level)
+    {
+        using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+        scope.ReadLock(ReadLockIds); // also for containers
+
+        return _containerRepository.Get(name, level);
+    }
+
+    public Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        using ICoreScope scope = ScopeProvider.CreateCoreScope();
+        scope.WriteLock(WriteLockIds); // also for containers
+
+        EntityContainer? container = _containerRepository?.Get(containerId);
+        if (container == null)
+        {
+            return OperationResult.Attempt.NoOperation(eventMessages);
+        }
+
+        // 'container' here does not know about its children, so we need
+        // to get it again from the entity repository, as a light entity
+        IEntitySlim? entity = _entityRepository.Get(container.Id);
+        if (entity?.HasChildren ?? false)
+        {
+            scope.Complete();
+            return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, eventMessages));
+        }
+
+        var deletingNotification = new EntityContainerDeletingNotification(container, eventMessages);
+        if (scope.Notifications.PublishCancelable(deletingNotification))
+        {
+            scope.Complete();
+            return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages));
+        }
+
+        _containerRepository?.Delete(container);
+        scope.Complete();
+
+        var deletedNotification = new EntityContainerDeletedNotification(container, eventMessages);
+        deletedNotification.WithStateFrom(deletingNotification);
+        scope.Notifications.Publish(deletedNotification);
+
+        return OperationResult.Attempt.Succeed(eventMessages);
+        // TODO: Audit trail ?
+    }
+
+    public Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(WriteLockIds); // also for containers
+
+            try
+            {
+                EntityContainer? container = _containerRepository?.Get(id);
+
+                //throw if null, this will be caught by the catch and a failed returned
+                if (container == null)
+                {
+                    throw new InvalidOperationException("No container found with id " + id);
+                }
+
+                container.Name = name;
+
+                var renamingNotification = new EntityContainerRenamingNotification(container, eventMessages);
+                if (scope.Notifications.PublishCancelable(renamingNotification))
+                {
+                    scope.Complete();
+                    return OperationResult.Attempt.Cancel(eventMessages);
+                }
 
                 _containerRepository?.Save(container);
                 scope.Complete();
 
-                var savedNotification = new EntityContainerSavedNotification(container, eventMessages);
-                savedNotification.WithStateFrom(savingNotification);
-                scope.Notifications.Publish(savedNotification);
+                var renamedNotification = new EntityContainerRenamedNotification(container, eventMessages);
+                renamedNotification.WithStateFrom(renamingNotification);
+                scope.Notifications.Publish(renamedNotification);
+
+                return OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, container);
             }
-
-            // TODO: Audit trail ?
-
-            return OperationResult.Attempt.Succeed(eventMessages);
-        }
-
-        public EntityContainer? GetContainer(int containerId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            catch (Exception ex)
             {
-                scope.ReadLock(ReadLockIds); // also for containers
-
-                return _containerRepository.Get(containerId);
+                return OperationResult.Attempt.Fail(eventMessages, ex);
             }
         }
-
-        public EntityContainer? GetContainer(Guid containerId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
-
-                return _containerRepository.Get(containerId);
-            }
-        }
-
-        public IEnumerable GetContainers(int[] containerIds)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
-
-                return _containerRepository.GetMany(containerIds);
-            }
-        }
-
-        public IEnumerable GetContainers(TItem item)
-        {
-            var ancestorIds = item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asInt) ? asInt : int.MinValue)
-                .Where(x => x != int.MinValue && x != item.Id)
-                .ToArray();
-
-            return GetContainers(ancestorIds);
-        }
-
-        public IEnumerable GetContainers(string name, int level)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(ReadLockIds); // also for containers
-
-                return _containerRepository.Get(name, level);
-            }
-        }
-
-        public Attempt DeleteContainer(int containerId, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(WriteLockIds); // also for containers
-
-                EntityContainer? container = _containerRepository?.Get(containerId);
-                if (container == null)
-                {
-                    return OperationResult.Attempt.NoOperation(eventMessages);
-                }
-
-                // 'container' here does not know about its children, so we need
-                // to get it again from the entity repository, as a light entity
-                IEntitySlim? entity = _entityRepository.Get(container.Id);
-                if (entity?.HasChildren ?? false)
-                {
-                    scope.Complete();
-                    return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, eventMessages));
-                }
-
-                var deletingNotification = new EntityContainerDeletingNotification(container, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages));
-                }
-
-                _containerRepository?.Delete(container);
-                scope.Complete();
-
-                var deletedNotification = new EntityContainerDeletedNotification(container, eventMessages);
-                deletedNotification.WithStateFrom(deletingNotification);
-                scope.Notifications.Publish(deletedNotification);
-
-                return OperationResult.Attempt.Succeed(eventMessages);
-                // TODO: Audit trail ?
-            }
-        }
-
-        public Attempt?> RenameContainer(int id, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(WriteLockIds); // also for containers
-
-                try
-                {
-                    EntityContainer? container = _containerRepository?.Get(id);
-
-                    //throw if null, this will be caught by the catch and a failed returned
-                    if (container == null)
-                    {
-                        throw new InvalidOperationException("No container found with id " + id);
-                    }
-
-                    container.Name = name;
-
-                    var renamingNotification = new EntityContainerRenamingNotification(container, eventMessages);
-                    if (scope.Notifications.PublishCancelable(renamingNotification))
-                    {
-                        scope.Complete();
-                        return OperationResult.Attempt.Cancel(eventMessages);
-                    }
-
-                    _containerRepository?.Save(container);
-                    scope.Complete();
-
-                    var renamedNotification = new EntityContainerRenamedNotification(container, eventMessages);
-                    renamedNotification.WithStateFrom(renamingNotification);
-                    scope.Notifications.Publish(renamedNotification);
-
-                    return OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, container);
-                }
-                catch (Exception ex)
-                {
-                    return OperationResult.Attempt.Fail(eventMessages, ex);
-                }
-            }
-        }
-
-        #endregion
-
-        #region Audit
-
-        private void Audit(AuditType type, int userId, int objectId)
-        {
-            _auditRepository.Save(new AuditItem(objectId, type, userId,
-                ObjectTypes.GetUmbracoObjectType(ContainedObjectType).GetName()));
-        }
-
-        #endregion
-
-
     }
+
+    #endregion
+
+    #region Audit
+
+    private void Audit(AuditType type, int userId, int objectId)
+    {
+        _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetUmbracoObjectType(ContainedObjectType).GetName()));
+    }
+
+    #endregion
+
+
 }
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
index 6c30b24b67..5ae8da3a12 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs
@@ -1,188 +1,207 @@
 // Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
-{
-    public static class ContentTypeServiceExtensions
-    {
-        /// 
-        /// Gets all of the element types (e.g. content types that have been marked as an element type).
-        /// 
-        /// The content type service.
-        /// Returns all the element types.
-        public static IEnumerable GetAllElementTypes(this IContentTypeService contentTypeService)
-        {
-            if (contentTypeService == null)
-            {
-                return Enumerable.Empty();
-            }
+namespace Umbraco.Extensions;
 
-            return contentTypeService.GetAll().Where(x => x.IsElement);
+public static class ContentTypeServiceExtensions
+{
+    /// 
+    ///     Gets all of the element types (e.g. content types that have been marked as an element type).
+    /// 
+    /// The content type service.
+    /// Returns all the element types.
+    public static IEnumerable GetAllElementTypes(this IContentTypeService contentTypeService)
+    {
+        if (contentTypeService == null)
+        {
+            return Enumerable.Empty();
         }
 
-        /// 
-        /// Returns the available composite content types for a given content type
-        /// 
-        /// 
-        /// 
-        /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out
-        /// along with any content types that have matching property types that are included in the filtered content types
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out.
-        /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot
-        /// be looked up via the db, they need to be passed in.
-        /// 
-        /// Whether the composite content types should be applicable for an element type
-        /// 
-        public static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService,
-            IContentTypeComposition? source,
-            IContentTypeComposition[] allContentTypes,
-            string[]? filterContentTypes = null,
-            string[]? filterPropertyTypes = null,
-            bool isElement = false)
+        return contentTypeService.GetAll().Where(x => x.IsElement);
+    }
+
+    /// 
+    ///     Returns the available composite content types for a given content type
+    /// 
+    /// 
+    /// 
+    ///     This is normally an empty list but if additional content type aliases are passed in, any content types containing
+    ///     those aliases will be filtered out
+    ///     along with any content types that have matching property types that are included in the filtered content types
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This is normally an empty list but if additional property type aliases are passed in, any content types that have
+    ///     these aliases will be filtered out.
+    ///     This is required because in the case of creating/modifying a content type because new property types being added to
+    ///     it are not yet persisted so cannot
+    ///     be looked up via the db, they need to be passed in.
+    /// 
+    /// Whether the composite content types should be applicable for an element type
+    /// 
+    public static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(
+        this IContentTypeService ctService,
+        IContentTypeComposition? source,
+        IContentTypeComposition[] allContentTypes,
+        string[]? filterContentTypes = null,
+        string[]? filterPropertyTypes = null,
+        bool isElement = false)
+    {
+        filterContentTypes = filterContentTypes == null
+            ? Array.Empty()
+            : filterContentTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
+
+        filterPropertyTypes = filterPropertyTypes == null
+            ? Array.Empty()
+            : filterPropertyTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
+
+        // create the full list of property types to use as the filter
+        // this is the combination of all property type aliases found in the content types passed in for the filter
+        // as well as the specific property types passed in for the filter
+        filterPropertyTypes = allContentTypes
+            .Where(c => filterContentTypes.InvariantContains(c.Alias))
+            .SelectMany(c => c.PropertyTypes)
+            .Select(c => c.Alias)
+            .Union(filterPropertyTypes)
+            .ToArray();
+
+        var sourceId = source?.Id ?? 0;
+
+        // find out if any content type uses this content type
+        IContentTypeComposition[] isUsing =
+            allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray();
+        if (isUsing.Length > 0)
         {
-            filterContentTypes = filterContentTypes == null
-                ? Array.Empty()
-                : filterContentTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
+            // if already in use a composition, do not allow any composited types
+            return new ContentTypeAvailableCompositionsResults();
+        }
 
-            filterPropertyTypes = filterPropertyTypes == null
-                ? Array.Empty()
-                : filterPropertyTypes.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
+        // if it is not used then composition is possible
+        // hashset guarantees uniqueness on Id
+        var list = new HashSet(new DelegateEqualityComparer(
+            (x, y) => x?.Id == y?.Id,
+            x => x.Id));
 
-            //create the full list of property types to use as the filter
-            //this is the combination of all property type aliases found in the content types passed in for the filter
-            //as well as the specific property types passed in for the filter
-            filterPropertyTypes = allContentTypes
-                    .Where(c => filterContentTypes.InvariantContains(c.Alias))
-                    .SelectMany(c => c.PropertyTypes)
-                    .Select(c => c.Alias)
-                    .Union(filterPropertyTypes)
-                    .ToArray();
+        // usable types are those that are top-level
+        // do not allow element types to be composed by non-element types as this will break the model generation in ModelsBuilder
+        IContentTypeComposition[] usableContentTypes = allContentTypes
+            .Where(x => x.ContentTypeComposition.Any() == false && (isElement == false || x.IsElement)).ToArray();
+        foreach (IContentTypeComposition x in usableContentTypes)
+        {
+            list.Add(x);
+        }
 
-            var sourceId = source?.Id ?? 0;
+        // indirect types are those that we use, directly or indirectly
+        IContentTypeComposition[] indirectContentTypes = GetDirectOrIndirect(source).ToArray();
+        foreach (IContentTypeComposition x in indirectContentTypes)
+        {
+            list.Add(x);
+        }
 
-            // find out if any content type uses this content type
-            var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray();
-            if (isUsing.Length > 0)
+        // At this point we have a list of content types that 'could' be compositions
+
+        // now we'll filter this list based on the filters requested
+        var filtered = list
+            .Where(x =>
             {
-                //if already in use a composition, do not allow any composited types
-                return new ContentTypeAvailableCompositionsResults();
-            }
+                // need to filter any content types that are included in this list
+                return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false;
+            })
+            .Where(x =>
+            {
+                // need to filter any content types that have matching property aliases that are included in this list
+                // ensure that we don't return if there's any overlapping property aliases from the filtered ones specified
+                return filterPropertyTypes.Intersect(
+                    x.PropertyTypes.Select(p => p.Alias),
+                    StringComparer.InvariantCultureIgnoreCase).Any() == false;
+            })
+            .OrderBy(x => x.Name)
+            .ToList();
 
-            // if it is not used then composition is possible
-            // hashset guarantees uniqueness on Id
-            var list = new HashSet(new DelegateEqualityComparer(
-                (x, y) => x?.Id == y?.Id,
-                x => x.Id));
+        // get ancestor ids - we will filter all ancestors
+        IContentTypeComposition[] ancestors = GetAncestors(source, allContentTypes);
+        var ancestorIds = ancestors.Select(x => x.Id).ToArray();
 
-            // usable types are those that are top-level
-            // do not allow element types to be composed by non-element types as this will break the model generation in ModelsBuilder
-            var usableContentTypes = allContentTypes
-                .Where(x => x.ContentTypeComposition.Any() == false && (isElement == false || x.IsElement)).ToArray();
-            foreach (var x in usableContentTypes)
-                list.Add(x);
+        // now we can create our result based on what is still available and the ancestors
+        var result = list
 
-            // indirect types are those that we use, directly or indirectly
-            var indirectContentTypes = GetDirectOrIndirect(source).ToArray();
-            foreach (var x in indirectContentTypes)
-                list.Add(x);
-
-            //At this point we have a list of content types that 'could' be compositions
-
-            //now we'll filter this list based on the filters requested
-            var filtered = list
-                .Where(x =>
-                {
-                    //need to filter any content types that are included in this list
-                    return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false;
-                })
-                .Where(x =>
-                {
-                    //need to filter any content types that have matching property aliases that are included in this list
-                    //ensure that we don't return if there's any overlapping property aliases from the filtered ones specified
-                    return filterPropertyTypes.Intersect(
-                        x.PropertyTypes.Select(p => p.Alias),
-                        StringComparer.InvariantCultureIgnoreCase).Any() == false;
-                })
-                .OrderBy(x => x.Name)
-                .ToList();
-
-            //get ancestor ids - we will filter all ancestors
-            var ancestors = GetAncestors(source, allContentTypes);
-            var ancestorIds = ancestors.Select(x => x.Id).ToArray();
-
-            //now we can create our result based on what is still available and the ancestors
-            var result = list
-                //not itself
-                .Where(x => x.Id != sourceId)
-                .OrderBy(x => x.Name)
-                .Select(composition => filtered.Contains(composition)
+            // not itself
+            .Where(x => x.Id != sourceId)
+            .OrderBy(x => x.Name)
+            .Select(composition => filtered.Contains(composition)
                 ? new ContentTypeAvailableCompositionsResult(composition, ancestorIds.Contains(composition.Id) == false)
                 : new ContentTypeAvailableCompositionsResult(composition, false)).ToList();
 
-            return new ContentTypeAvailableCompositionsResults(ancestors, result);
-        }
+        return new ContentTypeAvailableCompositionsResults(ancestors, result);
+    }
 
-
-        private static IContentTypeComposition[] GetAncestors(IContentTypeComposition? ctype, IContentTypeComposition[] allContentTypes)
+    private static IContentTypeComposition[] GetAncestors(
+        IContentTypeComposition? ctype,
+        IContentTypeComposition[] allContentTypes)
+    {
+        if (ctype == null)
         {
-            if (ctype == null) return new IContentTypeComposition[] {};
-            var ancestors = new List();
-            var parentId = ctype.ParentId;
-            while (parentId > 0)
-            {
-                var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId);
-                if (parent != null)
-                {
-                    ancestors.Add(parent);
-                    parentId = parent.ParentId;
-                }
-                else
-                {
-                    parentId = -1;
-                }
-            }
-            return ancestors.ToArray();
+            return new IContentTypeComposition[] { };
         }
 
-        /// 
-        /// Get those that we use directly
-        /// 
-        /// 
-        /// 
-        private static IEnumerable GetDirectOrIndirect(IContentTypeComposition? ctype)
+        var ancestors = new List();
+        var parentId = ctype.ParentId;
+        while (parentId > 0)
         {
-            if (ctype == null) return Enumerable.Empty();
-
-            // hashset guarantees uniqueness on Id
-            var all = new HashSet(new DelegateEqualityComparer(
-                (x, y) => x?.Id == y?.Id,
-                x => x.Id));
-
-            var stack = new Stack();
-
-            foreach (var x in ctype.ContentTypeComposition)
-                stack.Push(x);
-
-            while (stack.Count > 0)
+            IContentTypeComposition? parent = allContentTypes.FirstOrDefault(x => x.Id == parentId);
+            if (parent != null)
             {
-                var x = stack.Pop();
-                all.Add(x);
-                foreach (var y in x.ContentTypeComposition)
-                    stack.Push(y);
+                ancestors.Add(parent);
+                parentId = parent.ParentId;
+            }
+            else
+            {
+                parentId = -1;
             }
-
-            return all;
         }
+
+        return ancestors.ToArray();
+    }
+
+    /// 
+    ///     Get those that we use directly
+    /// 
+    /// 
+    /// 
+    private static IEnumerable GetDirectOrIndirect(IContentTypeComposition? ctype)
+    {
+        if (ctype == null)
+        {
+            return Enumerable.Empty();
+        }
+
+        // hashset guarantees uniqueness on Id
+        var all = new HashSet(new DelegateEqualityComparer(
+            (x, y) => x?.Id == y?.Id,
+            x => x.Id));
+
+        var stack = new Stack();
+
+        foreach (IContentTypeComposition x in ctype.ContentTypeComposition)
+        {
+            stack.Push(x);
+        }
+
+        while (stack.Count > 0)
+        {
+            IContentTypeComposition x = stack.Pop();
+            all.Add(x);
+            foreach (IContentTypeComposition y in x.ContentTypeComposition)
+            {
+                stack.Push(y);
+            }
+        }
+
+        return all;
     }
 }
diff --git a/src/Umbraco.Core/Services/ContentVersionService.cs b/src/Umbraco.Core/Services/ContentVersionService.cs
index 9e32bab762..24443a3957 100644
--- a/src/Umbraco.Core/Services/ContentVersionService.cs
+++ b/src/Umbraco.Core/Services/ContentVersionService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -10,191 +7,194 @@ using Umbraco.Cms.Core.Scoping;
 using Umbraco.Extensions;
 
 // ReSharper disable once CheckNamespace
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+internal class ContentVersionService : IContentVersionService
 {
-    internal class ContentVersionService : IContentVersionService
+    private readonly IAuditRepository _auditRepository;
+    private readonly IContentVersionCleanupPolicy _contentVersionCleanupPolicy;
+    private readonly IDocumentVersionRepository _documentVersionRepository;
+    private readonly IEventMessagesFactory _eventMessagesFactory;
+    private readonly ILanguageRepository _languageRepository;
+    private readonly ILogger _logger;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public ContentVersionService(
+        ILogger logger,
+        IDocumentVersionRepository documentVersionRepository,
+        IContentVersionCleanupPolicy contentVersionCleanupPolicy,
+        ICoreScopeProvider scopeProvider,
+        IEventMessagesFactory eventMessagesFactory,
+        IAuditRepository auditRepository,
+        ILanguageRepository languageRepository)
     {
-        private readonly ILogger _logger;
-        private readonly IDocumentVersionRepository _documentVersionRepository;
-        private readonly IContentVersionCleanupPolicy _contentVersionCleanupPolicy;
-        private readonly ICoreScopeProvider _scopeProvider;
-        private readonly IEventMessagesFactory _eventMessagesFactory;
-        private readonly IAuditRepository _auditRepository;
-        private readonly ILanguageRepository _languageRepository;
+        _logger = logger;
+        _documentVersionRepository = documentVersionRepository;
+        _contentVersionCleanupPolicy = contentVersionCleanupPolicy;
+        _scopeProvider = scopeProvider;
+        _eventMessagesFactory = eventMessagesFactory;
+        _auditRepository = auditRepository;
+        _languageRepository = languageRepository;
+    }
 
-        public ContentVersionService(
-            ILogger logger,
-            IDocumentVersionRepository documentVersionRepository,
-            IContentVersionCleanupPolicy contentVersionCleanupPolicy,
-            ICoreScopeProvider scopeProvider,
-            IEventMessagesFactory eventMessagesFactory,
-            IAuditRepository auditRepository,
-            ILanguageRepository languageRepository)
+    /// 
+    public IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate) =>
+
+        // Media - ignored
+        // Members - ignored
+        CleanupDocumentVersions(asAtDate);
+
+    /// 
+    public IEnumerable? GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string? culture = null)
+    {
+        if (pageIndex < 0)
         {
-            _logger = logger;
-            _documentVersionRepository = documentVersionRepository;
-            _contentVersionCleanupPolicy = contentVersionCleanupPolicy;
-            _scopeProvider = scopeProvider;
-            _eventMessagesFactory = eventMessagesFactory;
-            _auditRepository = auditRepository;
-            _languageRepository = languageRepository;
+            throw new ArgumentOutOfRangeException(nameof(pageIndex));
         }
 
-        /// 
-        public IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate)
+        if (pageSize <= 0)
         {
-            // Media - ignored
-            // Members - ignored
-            return CleanupDocumentVersions(asAtDate);
+            throw new ArgumentOutOfRangeException(nameof(pageSize));
         }
 
-        private IReadOnlyCollection CleanupDocumentVersions(DateTime asAtDate)
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            List versionsToDelete;
+            var languageId = _languageRepository.GetIdByIsoCode(culture, true);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _documentVersionRepository.GetPagedItemsByContentId(contentId, pageIndex, pageSize, out totalRecords, languageId);
+        }
+    }
 
-            /* Why so many scopes?
-             *
-             * We could just work out the set to delete at SQL infra level which was the original plan, however we agreed that really we should fire
-             * ContentService.DeletingVersions so people can hook & cancel if required.
-             *
-             * On first time run of cleanup on a site with a lot of history there may be a lot of historic ContentVersions to remove e.g. 200K for our.umbraco.com.
-             * If we weren't supporting SQL CE we could do TVP, or use temp tables to bulk delete with joins to our list of version ids to nuke.
-             * (much nicer, we can kill 100k in sub second time-frames).
-             *
-             * However we are supporting SQL CE, so the easiest thing to do is use the Umbraco InGroupsOf helper to create a query with 2K args of version
-             * ids to delete at a time.
-             *
-             * This is already done at the repository level, however if we only had a single scope at service level we're still locking
-             * the ContentVersions table (and other related tables) for a couple of minutes which makes the back office unusable.
-             *
-             * As a quick fix, we can also use InGroupsOf at service level, create a scope per group to give other connections a chance
-             * to grab the locks and execute their queries.
-             *
-             * This makes the back office a tiny bit sluggish during first run but it is usable for loading tree and publishing content.
-             *
-             * There are optimizations we can do, we could add a bulk delete for SqlServerSyntaxProvider which differs in implementation
-             * and fallback to this naive approach only for SQL CE, however we agreed it is not worth the effort as this is a one time pain,
-             * subsequent runs shouldn't have huge numbers of versions to cleanup.
-             *
-             * tl;dr lots of scopes to enable other connections to use the DB whilst we work.
-             */
-            using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+    /// 
+    public void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            scope.WriteLock(Constants.Locks.ContentTree);
+            _documentVersionRepository.SetPreventCleanup(versionId, preventCleanup);
+
+            ContentVersionMeta? version = _documentVersionRepository.Get(versionId);
+
+            if (version is null)
             {
-                IReadOnlyCollection? allHistoricVersions = _documentVersionRepository.GetDocumentVersionsEligibleForCleanup();
-
-                if (allHistoricVersions is null)
-                {
-                    return Array.Empty();
-                }
-                _logger.LogDebug("Discovered {count} candidate(s) for ContentVersion cleanup", allHistoricVersions.Count);
-                versionsToDelete = new List(allHistoricVersions.Count);
-
-                IEnumerable filteredContentVersions = _contentVersionCleanupPolicy.Apply(asAtDate, allHistoricVersions);
-
-                foreach (ContentVersionMeta version in filteredContentVersions)
-                {
-                    EventMessages messages = _eventMessagesFactory.Get();
-
-                    if (scope.Notifications.PublishCancelable(new ContentDeletingVersionsNotification(version.ContentId, messages, version.VersionId)))
-                    {
-                        _logger.LogDebug("Delete cancelled for ContentVersion [{versionId}]", version.VersionId);
-                        continue;
-                    }
-
-                    versionsToDelete.Add(version);
-                }
+                return;
             }
 
-            if (!versionsToDelete.Any())
+            AuditType auditType = preventCleanup
+                ? AuditType.ContentVersionPreventCleanup
+                : AuditType.ContentVersionEnableCleanup;
+
+            var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";
+
+            Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}");
+        }
+    }
+
+    private IReadOnlyCollection CleanupDocumentVersions(DateTime asAtDate)
+    {
+        List versionsToDelete;
+
+        /* Why so many scopes?
+         *
+         * We could just work out the set to delete at SQL infra level which was the original plan, however we agreed that really we should fire
+         * ContentService.DeletingVersions so people can hook & cancel if required.
+         *
+         * On first time run of cleanup on a site with a lot of history there may be a lot of historic ContentVersions to remove e.g. 200K for our.umbraco.com.
+         * If we weren't supporting SQL CE we could do TVP, or use temp tables to bulk delete with joins to our list of version ids to nuke.
+         * (much nicer, we can kill 100k in sub second time-frames).
+         *
+         * However we are supporting SQL CE, so the easiest thing to do is use the Umbraco InGroupsOf helper to create a query with 2K args of version
+         * ids to delete at a time.
+         *
+         * This is already done at the repository level, however if we only had a single scope at service level we're still locking
+         * the ContentVersions table (and other related tables) for a couple of minutes which makes the back office unusable.
+         *
+         * As a quick fix, we can also use InGroupsOf at service level, create a scope per group to give other connections a chance
+         * to grab the locks and execute their queries.
+         *
+         * This makes the back office a tiny bit sluggish during first run but it is usable for loading tree and publishing content.
+         *
+         * There are optimizations we can do, we could add a bulk delete for SqlServerSyntaxProvider which differs in implementation
+         * and fallback to this naive approach only for SQL CE, however we agreed it is not worth the effort as this is a one time pain,
+         * subsequent runs shouldn't have huge numbers of versions to cleanup.
+         *
+         * tl;dr lots of scopes to enable other connections to use the DB whilst we work.
+         */
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IReadOnlyCollection? allHistoricVersions =
+                _documentVersionRepository.GetDocumentVersionsEligibleForCleanup();
+
+            if (allHistoricVersions is null)
             {
-                _logger.LogDebug("No remaining ContentVersions for cleanup");
                 return Array.Empty();
             }
 
-            _logger.LogDebug("Removing {count} ContentVersion(s)", versionsToDelete.Count);
+            _logger.LogDebug("Discovered {count} candidate(s) for ContentVersion cleanup", allHistoricVersions.Count);
+            versionsToDelete = new List(allHistoricVersions.Count);
 
-            foreach (IEnumerable group in versionsToDelete.InGroupsOf(Constants.Sql.MaxParameterCount))
+            IEnumerable filteredContentVersions =
+                _contentVersionCleanupPolicy.Apply(asAtDate, allHistoricVersions);
+
+            foreach (ContentVersionMeta version in filteredContentVersions)
             {
-                using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+                EventMessages messages = _eventMessagesFactory.Get();
+
+                if (scope.Notifications.PublishCancelable(
+                        new ContentDeletingVersionsNotification(version.ContentId, messages, version.VersionId)))
                 {
-                    scope.WriteLock(Constants.Locks.ContentTree);
-                    var groupEnumerated = group.ToList();
-                    _documentVersionRepository.DeleteVersions(groupEnumerated.Select(x => x.VersionId));
-
-                    foreach (ContentVersionMeta version in groupEnumerated)
-                    {
-                        EventMessages messages = _eventMessagesFactory.Get();
-
-                        scope.Notifications.Publish(new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId));
-                    }
+                    _logger.LogDebug("Delete cancelled for ContentVersion [{versionId}]", version.VersionId);
+                    continue;
                 }
-            }
 
-            using (_scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                Audit(AuditType.Delete, Constants.Security.SuperUserId, -1, $"Removed {versionsToDelete.Count} ContentVersion(s) according to cleanup policy");
+                versionsToDelete.Add(version);
             }
-
-            return versionsToDelete;
         }
 
-        /// 
-        public IEnumerable? GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string? culture = null)
+        if (!versionsToDelete.Any())
         {
-            if (pageIndex < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            }
-
-            if (pageSize <= 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof(pageSize));
-            }
-
-            using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var languageId = _languageRepository.GetIdByIsoCode(culture, throwOnNotFound: true);
-                scope.ReadLock(Constants.Locks.ContentTree);
-                return _documentVersionRepository.GetPagedItemsByContentId(contentId, pageIndex, pageSize, out totalRecords, languageId);
-            }
+            _logger.LogDebug("No remaining ContentVersions for cleanup");
+            return Array.Empty();
         }
 
-        /// 
-        public void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1)
+        _logger.LogDebug("Removing {count} ContentVersion(s)", versionsToDelete.Count);
+
+        foreach (IEnumerable group in versionsToDelete.InGroupsOf(Constants.Sql.MaxParameterCount))
         {
             using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
             {
                 scope.WriteLock(Constants.Locks.ContentTree);
-                _documentVersionRepository.SetPreventCleanup(versionId, preventCleanup);
+                var groupEnumerated = group.ToList();
+                _documentVersionRepository.DeleteVersions(groupEnumerated.Select(x => x.VersionId));
 
-                ContentVersionMeta? version = _documentVersionRepository.Get(versionId);
-
-                if (version is null)
+                foreach (ContentVersionMeta version in groupEnumerated)
                 {
-                    return;
+                    EventMessages messages = _eventMessagesFactory.Get();
+
+                    scope.Notifications.Publish(
+                        new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId));
                 }
-
-                AuditType auditType = preventCleanup
-                    ? AuditType.ContentVersionPreventCleanup
-                    : AuditType.ContentVersionEnableCleanup;
-
-                var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";
-
-                Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}");
             }
         }
 
-        private void Audit(AuditType type, int userId, int objectId, string? message = null, string? parameters = null)
+        using (_scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            var entry = new AuditItem(
-                objectId,
-                type,
-                userId,
-                UmbracoObjectTypes.Document.GetName(),
-                message,
-                parameters);
-
-            _auditRepository.Save(entry);
+            Audit(AuditType.Delete, Constants.Security.SuperUserId, -1, $"Removed {versionsToDelete.Count} ContentVersion(s) according to cleanup policy");
         }
+
+        return versionsToDelete;
+    }
+
+    private void Audit(AuditType type, int userId, int objectId, string? message = null, string? parameters = null)
+    {
+        var entry = new AuditItem(
+            objectId,
+            type,
+            userId,
+            UmbracoObjectTypes.Document.GetName(),
+            message,
+            parameters);
+
+        _auditRepository.Save(entry);
     }
 }
diff --git a/src/Umbraco.Core/Services/DashboardService.cs b/src/Umbraco.Core/Services/DashboardService.cs
index 203ce64984..f5ddb30557 100644
--- a/src/Umbraco.Core/Services/DashboardService.cs
+++ b/src/Umbraco.Core/Services/DashboardService.cs
@@ -1,145 +1,161 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Umbraco.Cms.Core.Dashboards;
 using Umbraco.Cms.Core.Models.ContentEditing;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     A utility class for determine dashboard security
+/// 
+public class DashboardService : IDashboardService
 {
-    /// 
-    /// A utility class for determine dashboard security
-    /// 
-    public class DashboardService : IDashboardService
+    private readonly DashboardCollection _dashboardCollection;
+
+    private readonly ILocalizedTextService _localizedText;
+
+    // TODO: Unit test all this!!! :/
+    private readonly ISectionService _sectionService;
+
+    public DashboardService(ISectionService sectionService, DashboardCollection dashboardCollection, ILocalizedTextService localizedText)
     {
-        // TODO: Unit test all this!!! :/
+        _sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService));
+        _dashboardCollection = dashboardCollection ?? throw new ArgumentNullException(nameof(dashboardCollection));
+        _localizedText = localizedText ?? throw new ArgumentNullException(nameof(localizedText));
+    }
 
-        private readonly ISectionService _sectionService;
-        private readonly DashboardCollection _dashboardCollection;
-        private readonly ILocalizedTextService _localizedText;
+    /// 
+    public IEnumerable> GetDashboards(string section, IUser? currentUser)
+    {
+        var tabs = new List>();
+        var tabId = 0;
 
-        public DashboardService(ISectionService sectionService, DashboardCollection dashboardCollection, ILocalizedTextService localizedText)
+        foreach (IDashboard dashboard in _dashboardCollection.Where(x => x.Sections.InvariantContains(section)))
         {
-            _sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService));
-            _dashboardCollection = dashboardCollection ?? throw new ArgumentNullException(nameof(dashboardCollection));
-            _localizedText = localizedText ?? throw new ArgumentNullException(nameof(localizedText));
-        }
-
-
-        /// 
-        public IEnumerable> GetDashboards(string section, IUser? currentUser)
-        {
-            var tabs = new List>();
-            var tabId = 0;
-
-            foreach (var dashboard in _dashboardCollection.Where(x => x.Sections.InvariantContains(section)))
+            // validate access
+            if (currentUser is null || !CheckUserAccessByRules(currentUser, _sectionService, dashboard.AccessRules))
             {
-                // validate access
-                if (currentUser is null || !CheckUserAccessByRules(currentUser, _sectionService, dashboard.AccessRules))
-                    continue;
-
-                if (dashboard.View?.InvariantEndsWith(".ascx") ?? false)
-                    throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported.");
-
-                var dashboards = new List { dashboard };
-                tabs.Add(new Tab
-                {
-                    Id = tabId++,
-                    Label = _localizedText.Localize("dashboardTabs", dashboard.Alias),
-                    Alias = dashboard.Alias,
-                    Properties = dashboards
-                });
+                continue;
             }
 
-            return tabs;
-        }
-
-        /// 
-        public IDictionary>> GetDashboards(IUser? currentUser)
-        {
-            return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
-        }
-
-        private bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable rules)
-        {
-            if (user.Id == Constants.Security.SuperUserId)
-                return true;
-
-            var (denyRules, grantRules, grantBySectionRules) = GroupRules(rules);
-
-            var hasAccess = true;
-            string[]? assignedUserGroups = null;
-
-            // if there are no grant rules, then access is granted by default, unless denied
-            // otherwise, grant rules determine if access can be granted at all
-            if (grantBySectionRules.Length > 0 || grantRules.Length > 0)
+            if (dashboard.View?.InvariantEndsWith(".ascx") ?? false)
             {
-                hasAccess = false;
+                throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported.");
+            }
 
-                // check if this item has any grant-by-section arguments.
-                // if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far)
-                if (grantBySectionRules.Length > 0)
+            var dashboards = new List { dashboard };
+            tabs.Add(new Tab
+            {
+                Id = tabId++,
+                Label = _localizedText.Localize("dashboardTabs", dashboard.Alias),
+                Alias = dashboard.Alias,
+                Properties = dashboards,
+            });
+        }
+
+        return tabs;
+    }
+
+    /// 
+    public IDictionary>> GetDashboards(IUser? currentUser) => _sectionService
+        .GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
+
+    private static (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable rules)
+    {
+        IAccessRule[]? denyRules = null, grantRules = null, grantBySectionRules = null;
+
+        IEnumerable> groupedRules = rules.GroupBy(x => x.Type);
+        foreach (IGrouping group in groupedRules)
+        {
+            IAccessRule[] a = group.ToArray();
+            switch (group.Key)
+            {
+                case AccessRuleType.Deny:
+                    denyRules = a;
+                    break;
+                case AccessRuleType.Grant:
+                    grantRules = a;
+                    break;
+                case AccessRuleType.GrantBySection:
+                    grantBySectionRules = a;
+                    break;
+                default:
+                    throw new NotSupportedException($"The '{group.Key}'-AccessRuleType is not supported.");
+            }
+        }
+
+        return (denyRules ?? Array.Empty(), grantRules ?? Array.Empty(),
+            grantBySectionRules ?? Array.Empty());
+    }
+
+    private bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable rules)
+    {
+        if (user.Id == Constants.Security.SuperUserId)
+        {
+            return true;
+        }
+
+        (IAccessRule[] denyRules, IAccessRule[] grantRules, IAccessRule[] grantBySectionRules) = GroupRules(rules);
+
+        var hasAccess = true;
+        string[]? assignedUserGroups = null;
+
+        // if there are no grant rules, then access is granted by default, unless denied
+        // otherwise, grant rules determine if access can be granted at all
+        if (grantBySectionRules.Length > 0 || grantRules.Length > 0)
+        {
+            hasAccess = false;
+
+            // check if this item has any grant-by-section arguments.
+            // if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far)
+            if (grantBySectionRules.Length > 0)
+            {
+                var allowedSections = sectionService.GetAllowedSections(user.Id).Select(x => x.Alias).ToArray();
+                var wantedSections = grantBySectionRules.SelectMany(g =>
+                    g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ??
+                    Array.Empty()).ToArray();
+
+                if (wantedSections.Intersect(allowedSections).Any())
                 {
-                    var allowedSections = sectionService.GetAllowedSections(user.Id).Select(x => x.Alias).ToArray();
-                    var wantedSections = grantBySectionRules.SelectMany(g => g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()).ToArray();
-
-                    if (wantedSections.Intersect(allowedSections).Any())
-                        hasAccess = true;
-                }
-
-                // if not already granted access, check if this item as any grant arguments.
-                // if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far)
-                if (hasAccess == false && grantRules.Any())
-                {
-                    assignedUserGroups = user.Groups.Select(x => x.Alias).ToArray();
-                    var wantedUserGroups = grantRules.SelectMany(g => g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()).ToArray();
-
-                    if (wantedUserGroups.Intersect(assignedUserGroups).Any())
-                        hasAccess = true;
+                    hasAccess = true;
                 }
             }
 
-            // No need to check denyRules if there aren't any, just return current state
-            if (denyRules.Length == 0)
-                return hasAccess;
+            // if not already granted access, check if this item as any grant arguments.
+            // if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far)
+            if (hasAccess == false && grantRules.Any())
+            {
+                assignedUserGroups = user.Groups.Select(x => x.Alias).ToArray();
+                var wantedUserGroups = grantRules.SelectMany(g =>
+                    g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ??
+                    Array.Empty()).ToArray();
 
-            // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will
-            // be denied to see it no matter what
-            assignedUserGroups = assignedUserGroups ?? user.Groups.Select(x => x.Alias).ToArray();
-            var deniedUserGroups = denyRules.SelectMany(g => g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()).ToArray();
-
-            if (deniedUserGroups.Intersect(assignedUserGroups).Any())
-                hasAccess = false;
+                if (wantedUserGroups.Intersect(assignedUserGroups).Any())
+                {
+                    hasAccess = true;
+                }
+            }
+        }
 
+        // No need to check denyRules if there aren't any, just return current state
+        if (denyRules.Length == 0)
+        {
             return hasAccess;
         }
 
-        private static (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable rules)
+        // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will
+        // be denied to see it no matter what
+        assignedUserGroups ??= user.Groups.Select(x => x.Alias).ToArray();
+        var deniedUserGroups = denyRules.SelectMany(g =>
+                g.Value?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) ??
+                Array.Empty())
+            .ToArray();
+
+        if (deniedUserGroups.Intersect(assignedUserGroups).Any())
         {
-            IAccessRule[]? denyRules = null, grantRules = null, grantBySectionRules = null;
-
-            var groupedRules = rules.GroupBy(x => x.Type);
-            foreach (var group in groupedRules)
-            {
-                var a = group.ToArray();
-                switch (group.Key)
-                {
-                    case AccessRuleType.Deny:
-                        denyRules = a;
-                        break;
-                    case AccessRuleType.Grant:
-                        grantRules = a;
-                        break;
-                    case AccessRuleType.GrantBySection:
-                        grantBySectionRules = a;
-                        break;
-                    default:
-                        throw new NotSupportedException($"The '{group.Key.ToString()}'-AccessRuleType is not supported.");
-                }
-            }
-
-            return (denyRules ?? Array.Empty(), grantRules ?? Array.Empty(), grantBySectionRules ?? Array.Empty());
+            hasAccess = false;
         }
+
+        return hasAccess;
     }
 }
diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs
index 5c9d5847ed..1fdbb4a79b 100644
--- a/src/Umbraco.Core/Services/DataTypeService.cs
+++ b/src/Umbraco.Core/Services/DataTypeService.cs
@@ -1,13 +1,12 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Exceptions;
 using Umbraco.Cms.Core.IO;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.PropertyEditors;
 using Umbraco.Cms.Core.Scoping;
@@ -39,10 +38,17 @@ namespace Umbraco.Cms.Core.Services.Implement
         [Obsolete("Please use constructor that takes an ")]
         public DataTypeService(
             IDataValueEditorFactory dataValueEditorFactory,
-            ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IDataTypeRepository dataTypeRepository, IDataTypeContainerRepository dataTypeContainerRepository,
-            IAuditRepository auditRepository, IEntityRepository entityRepository, IContentTypeRepository contentTypeRepository,
-            IIOHelper ioHelper, ILocalizedTextService localizedTextService, ILocalizationService localizationService,
+            ICoreScopeProvider provider,
+            ILoggerFactory loggerFactory,
+            IEventMessagesFactory eventMessagesFactory,
+            IDataTypeRepository dataTypeRepository,
+            IDataTypeContainerRepository dataTypeContainerRepository,
+            IAuditRepository auditRepository,
+            IEntityRepository entityRepository,
+            IContentTypeRepository contentTypeRepository,
+            IIOHelper ioHelper,
+            ILocalizedTextService localizedTextService,
+            ILocalizationService localizationService,
             IShortStringHelper shortStringHelper,
             IJsonSerializer jsonSerializer)
             : this(
@@ -77,10 +83,17 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         public DataTypeService(
             IDataValueEditorFactory dataValueEditorFactory,
-            ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IDataTypeRepository dataTypeRepository, IDataTypeContainerRepository dataTypeContainerRepository,
-            IAuditRepository auditRepository, IEntityRepository entityRepository, IContentTypeRepository contentTypeRepository,
-            IIOHelper ioHelper, ILocalizedTextService localizedTextService, ILocalizationService localizationService,
+            ICoreScopeProvider provider,
+            ILoggerFactory loggerFactory,
+            IEventMessagesFactory eventMessagesFactory,
+            IDataTypeRepository dataTypeRepository,
+            IDataTypeContainerRepository dataTypeContainerRepository,
+            IAuditRepository auditRepository,
+            IEntityRepository entityRepository,
+            IContentTypeRepository contentTypeRepository,
+            IIOHelper ioHelper,
+            ILocalizedTextService localizedTextService,
+            ILocalizationService localizationService,
             IShortStringHelper shortStringHelper,
             IJsonSerializer jsonSerializer,
             IEditorConfigurationParser editorConfigurationParser)
@@ -102,14 +115,14 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         #region Containers
 
-        public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
                 try
                 {
-                    var container = new EntityContainer(Cms.Core.Constants.ObjectTypes.DataType)
+                    var container = new EntityContainer(Constants.ObjectTypes.DataType)
                     {
                         Name = name,
                         ParentId = parentId,
@@ -142,26 +155,20 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         public EntityContainer? GetContainer(int containerId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _dataTypeContainerRepository.Get(containerId);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return _dataTypeContainerRepository.Get(containerId);
         }
 
         public EntityContainer? GetContainer(Guid containerId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _dataTypeContainerRepository.Get(containerId);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return _dataTypeContainerRepository.Get(containerId);
         }
 
         public IEnumerable GetContainers(string name, int level)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _dataTypeContainerRepository.Get(name, level);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return _dataTypeContainerRepository.Get(name, level);
         }
 
         public IEnumerable GetContainers(IDataType dataType)
@@ -169,7 +176,7 @@ namespace Umbraco.Cms.Core.Services.Implement
             var ancestorIds = dataType.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
                 .Select(x =>
                 {
-                    var asInt = x.TryConvertTo();
+                    Attempt asInt = x.TryConvertTo();
                     return asInt.Success ? asInt.Result : int.MinValue;
                 })
                 .Where(x => x != int.MinValue && x != dataType.Id)
@@ -180,19 +187,17 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         public IEnumerable GetContainers(int[] containerIds)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _dataTypeContainerRepository.GetMany(containerIds);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return _dataTypeContainerRepository.GetMany(containerIds);
         }
 
-        public Attempt SaveContainer(EntityContainer container, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            if (container.ContainedObjectType != Cms.Core.Constants.ObjectTypes.DataType)
+            if (container.ContainedObjectType != Constants.ObjectTypes.DataType)
             {
-                var ex = new InvalidOperationException("Not a " + Cms.Core.Constants.ObjectTypes.DataType + " container.");
+                var ex = new InvalidOperationException("Not a " + Constants.ObjectTypes.DataType + " container.");
                 return OperationResult.Attempt.Fail(evtMsgs, ex);
             }
 
@@ -202,7 +207,7 @@ namespace Umbraco.Cms.Core.Services.Implement
                 return OperationResult.Attempt.Fail(evtMsgs, ex);
             }
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
                 var savingEntityContainerNotification = new EntityContainerSavingNotification(container, evtMsgs);
                 if (scope.Notifications.PublishCancelable(savingEntityContainerNotification))
@@ -221,17 +226,20 @@ namespace Umbraco.Cms.Core.Services.Implement
             return OperationResult.Attempt.Succeed(evtMsgs);
         }
 
-        public Attempt DeleteContainer(int containerId, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                var container = _dataTypeContainerRepository.Get(containerId);
-                if (container == null) return OperationResult.Attempt.NoOperation(evtMsgs);
+                EntityContainer? container = _dataTypeContainerRepository.Get(containerId);
+                if (container == null)
+                {
+                    return OperationResult.Attempt.NoOperation(evtMsgs);
+                }
 
                 // 'container' here does not know about its children, so we need
                 // to get it again from the entity repository, as a light entity
-                var entity = _entityRepository.Get(container.Id);
+                IEntitySlim? entity = _entityRepository.Get(container.Id);
                 if (entity?.HasChildren ?? false)
                 {
                     scope.Complete();
@@ -255,18 +263,20 @@ namespace Umbraco.Cms.Core.Services.Implement
             return OperationResult.Attempt.Succeed(evtMsgs);
         }
 
-        public Attempt?> RenameContainer(int id, string name, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
                 try
                 {
-                    var container = _dataTypeContainerRepository.Get(id);
+                    EntityContainer? container = _dataTypeContainerRepository.Get(id);
 
                     //throw if null, this will be caught by the catch and a failed returned
                     if (container == null)
+                    {
                         throw new InvalidOperationException("No container found with id " + id);
+                    }
 
                     container.Name = name;
 
@@ -300,12 +310,10 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         public IDataType? GetDataType(string name)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var dataType = _dataTypeRepository.Get(Query().Where(x => x.Name == name))?.FirstOrDefault();
-                ConvertMissingEditorOfDataTypeToLabel(dataType);
-                return dataType;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IDataType? dataType = _dataTypeRepository.Get(Query().Where(x => x.Name == name))?.FirstOrDefault();
+            ConvertMissingEditorOfDataTypeToLabel(dataType);
+            return dataType;
         }
 
         /// 
@@ -315,12 +323,10 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         public IDataType? GetDataType(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var dataType = _dataTypeRepository.Get(id);
-                ConvertMissingEditorOfDataTypeToLabel(dataType);
-                return dataType;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IDataType? dataType = _dataTypeRepository.Get(id);
+            ConvertMissingEditorOfDataTypeToLabel(dataType);
+            return dataType;
         }
 
         /// 
@@ -330,13 +336,11 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         public IDataType? GetDataType(Guid id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.Key == id);
-                var dataType = _dataTypeRepository.Get(query).FirstOrDefault();
-                ConvertMissingEditorOfDataTypeToLabel(dataType);
-                return dataType;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery query = Query().Where(x => x.Key == id);
+            IDataType? dataType = _dataTypeRepository.Get(query).FirstOrDefault();
+            ConvertMissingEditorOfDataTypeToLabel(dataType);
+            return dataType;
         }
 
         /// 
@@ -346,13 +350,11 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// Collection of  objects with a matching control id
         public IEnumerable GetByEditorAlias(string propertyEditorAlias)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.EditorAlias == propertyEditorAlias);
-                var dataType = _dataTypeRepository.Get(query);
-                ConvertMissingEditorsOfDataTypesToLabels(dataType);
-                return dataType;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery query = Query().Where(x => x.EditorAlias == propertyEditorAlias);
+            IEnumerable dataType = _dataTypeRepository.Get(query).ToArray();
+            ConvertMissingEditorsOfDataTypesToLabels(dataType);
+            return dataType;
         }
 
         /// 
@@ -362,13 +364,11 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// An enumerable list of  objects
         public IEnumerable GetAll(params int[] ids)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var dataTypes = _dataTypeRepository.GetMany(ids);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IEnumerable dataTypes = _dataTypeRepository.GetMany(ids).ToArray();
 
-                ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
-                return dataTypes;
-            }
+            ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
+            return dataTypes;
         }
 
         private void ConvertMissingEditorOfDataTypeToLabel(IDataType? dataType)
@@ -385,9 +385,9 @@ namespace Umbraco.Cms.Core.Services.Implement
         {
             // Any data types that don't have an associated editor are created of a specific type.
             // We convert them to labels to make clear to the user why the data type cannot be used.
-            var dataTypesWithMissingEditors = dataTypes
+            IEnumerable dataTypesWithMissingEditors = dataTypes
                 .Where(x => x.Editor is MissingPropertyEditor);
-            foreach (var dataType in dataTypesWithMissingEditors)
+            foreach (IDataType dataType in dataTypesWithMissingEditors)
             {
                 dataType.Editor = new LabelPropertyEditor(_dataValueEditorFactory, _ioHelper, _editorConfigurationParser);
             }
@@ -395,10 +395,10 @@ namespace Umbraco.Cms.Core.Services.Implement
 
         public Attempt?> Move(IDataType toMove, int parentId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
             var moveInfo = new List>();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
                 var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, parentId);
 
@@ -416,7 +416,9 @@ namespace Umbraco.Cms.Core.Services.Implement
                     {
                         container = _dataTypeContainerRepository.Get(parentId);
                         if (container == null)
+                        {
                             throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback
+                        }
                     }
                     moveInfo.AddRange(_dataTypeRepository.Move(toMove, container));
 
@@ -439,39 +441,37 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         ///  to save
         /// Id of the user issuing the save
-        public void Save(IDataType dataType, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void Save(IDataType dataType, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
             dataType.CreatorId = userId;
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var saveEventArgs = new SaveEventArgs(dataType);
+
+            var savingDataTypeNotification = new DataTypeSavingNotification(dataType, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
             {
-                var saveEventArgs = new SaveEventArgs(dataType);
-
-                var savingDataTypeNotification = new DataTypeSavingNotification(dataType, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (string.IsNullOrWhiteSpace(dataType.Name))
-                {
-                    throw new ArgumentException("Cannot save datatype with empty name.");
-                }
-
-                if (dataType.Name != null && dataType.Name.Length > 255)
-                {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-                }
-
-                _dataTypeRepository.Save(dataType);
-
-                scope.Notifications.Publish(new DataTypeSavedNotification(dataType, evtMsgs).WithStateFrom(savingDataTypeNotification));
-
-                Audit(AuditType.Save, userId, dataType.Id);
                 scope.Complete();
+                return;
             }
+
+            if (string.IsNullOrWhiteSpace(dataType.Name))
+            {
+                throw new ArgumentException("Cannot save datatype with empty name.");
+            }
+
+            if (dataType.Name != null && dataType.Name.Length > 255)
+            {
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+            }
+
+            _dataTypeRepository.Save(dataType);
+
+            scope.Notifications.Publish(new DataTypeSavedNotification(dataType, evtMsgs).WithStateFrom(savingDataTypeNotification));
+
+            Audit(AuditType.Save, userId, dataType.Id);
+            scope.Complete();
         }
 
         /// 
@@ -481,30 +481,28 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// Id of the user issuing the save
         public void Save(IEnumerable dataTypeDefinitions, int userId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            var dataTypeDefinitionsA = dataTypeDefinitions.ToArray();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            IDataType[] dataTypeDefinitionsA = dataTypeDefinitions.ToArray();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var savingDataTypeNotification = new DataTypeSavingNotification(dataTypeDefinitions, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
             {
-                var savingDataTypeNotification = new DataTypeSavingNotification(dataTypeDefinitions, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                foreach (var dataTypeDefinition in dataTypeDefinitionsA)
-                {
-                    dataTypeDefinition.CreatorId = userId;
-                    _dataTypeRepository.Save(dataTypeDefinition);
-                }
-
-                scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification));
-
-                Audit(AuditType.Save, userId, -1);
-
                 scope.Complete();
+                return;
             }
+
+            foreach (IDataType dataTypeDefinition in dataTypeDefinitionsA)
+            {
+                dataTypeDefinition.CreatorId = userId;
+                _dataTypeRepository.Save(dataTypeDefinition);
+            }
+
+            scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification));
+
+            Audit(AuditType.Save, userId, -1);
+
+            scope.Complete();
         }
 
         /// 
@@ -516,64 +514,60 @@ namespace Umbraco.Cms.Core.Services.Implement
         /// 
         ///  to delete
         /// Optional Id of the user issuing the deletion
-        public void Delete(IDataType dataType, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void Delete(IDataType dataType, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages evtMsgs = EventMessagesFactory.Get();
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var deletingDataTypeNotification = new DataTypeDeletingNotification(dataType, evtMsgs);
+            if (scope.Notifications.PublishCancelable(deletingDataTypeNotification))
             {
-                var deletingDataTypeNotification = new DataTypeDeletingNotification(dataType, evtMsgs);
-                if (scope.Notifications.PublishCancelable(deletingDataTypeNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
+                scope.Complete();
+                return;
+            }
 
-                // find ContentTypes using this IDataTypeDefinition on a PropertyType, and delete
-                // TODO: media and members?!
-                // TODO: non-group properties?!
-                var query = Query().Where(x => x.DataTypeId == dataType.Id);
-                var contentTypes = _contentTypeRepository.GetByQuery(query);
-                foreach (var contentType in contentTypes)
+            // find ContentTypes using this IDataTypeDefinition on a PropertyType, and delete
+            // TODO: media and members?!
+            // TODO: non-group properties?!
+            IQuery query = Query().Where(x => x.DataTypeId == dataType.Id);
+            IEnumerable contentTypes = _contentTypeRepository.GetByQuery(query);
+            foreach (IContentType contentType in contentTypes)
+            {
+                foreach (PropertyGroup propertyGroup in contentType.PropertyGroups)
                 {
-                    foreach (var propertyGroup in contentType.PropertyGroups)
+                    var types = propertyGroup.PropertyTypes?.Where(x => x.DataTypeId == dataType.Id).ToList();
+                    if (types is not null)
                     {
-                        var types = propertyGroup.PropertyTypes?.Where(x => x.DataTypeId == dataType.Id).ToList();
-                        if (types is not null)
+                        foreach (IPropertyType propertyType in types)
                         {
-                            foreach (var propertyType in types)
-                            {
-                                propertyGroup.PropertyTypes?.Remove(propertyType);
-                            }
+                            propertyGroup.PropertyTypes?.Remove(propertyType);
                         }
                     }
-
-                    // so... we are modifying content types here. the service will trigger Deleted event,
-                    // which will propagate to DataTypeCacheRefresher which will clear almost every cache
-                    // there is to clear... and in addition published snapshot caches will clear themselves too, so
-                    // this is probably safe although it looks... weird.
-                    //
-                    // what IS weird is that a content type is losing a property and we do NOT raise any
-                    // content type event... so ppl better listen on the data type events too.
-
-                    _contentTypeRepository.Save(contentType);
                 }
 
-                _dataTypeRepository.Delete(dataType);
+                // so... we are modifying content types here. the service will trigger Deleted event,
+                // which will propagate to DataTypeCacheRefresher which will clear almost every cache
+                // there is to clear... and in addition published snapshot caches will clear themselves too, so
+                // this is probably safe although it looks... weird.
+                //
+                // what IS weird is that a content type is losing a property and we do NOT raise any
+                // content type event... so ppl better listen on the data type events too.
 
-                scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, evtMsgs).WithStateFrom(deletingDataTypeNotification));
-
-                Audit(AuditType.Delete, userId, dataType.Id);
-
-                scope.Complete();
+                _contentTypeRepository.Save(contentType);
             }
+
+            _dataTypeRepository.Delete(dataType);
+
+            scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, evtMsgs).WithStateFrom(deletingDataTypeNotification));
+
+            Audit(AuditType.Delete, userId, dataType.Id);
+
+            scope.Complete();
         }
 
         public IReadOnlyDictionary> GetReferences(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete:true))
-            {
-                return _dataTypeRepository.FindUsages(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete:true);
+            return _dataTypeRepository.FindUsages(id);
         }
 
         private void Audit(AuditType type, int userId, int objectId)
diff --git a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs
index 312b939ec5..476a2ddd47 100644
--- a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs
@@ -1,21 +1,25 @@
-using System;
+using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.PropertyEditors;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+public static class DateTypeServiceExtensions
 {
-    public static class DateTypeServiceExtensions
+    public static bool IsDataTypeIgnoringUserStartNodes(this IDataTypeService dataTypeService, Guid key)
     {
-        public static bool IsDataTypeIgnoringUserStartNodes(this IDataTypeService dataTypeService, Guid key)
+        if (DataTypeExtensions.IsBuildInDataType(key))
         {
-            if (DataTypeExtensions.IsBuildInDataType(key)) return false; //built in ones can never be ignoring start nodes
-
-            var dataType = dataTypeService.GetDataType(key);
-
-            if (dataType != null && dataType.Configuration is IIgnoreUserStartNodesConfig ignoreStartNodesConfig)
-                return ignoreStartNodesConfig.IgnoreUserStartNodes;
-
-            return false;
+            return false; // built in ones can never be ignoring start nodes
         }
+
+        IDataType? dataType = dataTypeService.GetDataType(key);
+
+        if (dataType != null && dataType.Configuration is IIgnoreUserStartNodesConfig ignoreStartNodesConfig)
+        {
+            return ignoreStartNodesConfig.IgnoreUserStartNodes;
+        }
+
+        return false;
     }
 }
diff --git a/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs b/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs
index 810106e0ba..f51858fa5b 100644
--- a/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs
+++ b/src/Umbraco.Core/Services/DefaultContentVersionCleanupPolicy.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Options;
 using Umbraco.Cms.Core.Configuration.Models;
 using Umbraco.Cms.Core.Models;
@@ -8,93 +5,92 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using ContentVersionCleanupPolicySettings = Umbraco.Cms.Core.Models.ContentVersionCleanupPolicySettings;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy
 {
-    public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy
+    private readonly IOptions _contentSettings;
+    private readonly IDocumentVersionRepository _documentVersionRepository;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public DefaultContentVersionCleanupPolicy(
+        IOptions contentSettings,
+        ICoreScopeProvider scopeProvider,
+        IDocumentVersionRepository documentVersionRepository)
     {
-        private readonly IOptions _contentSettings;
-        private readonly ICoreScopeProvider _scopeProvider;
-        private readonly IDocumentVersionRepository _documentVersionRepository;
+        _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
+        _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
+        _documentVersionRepository = documentVersionRepository ??
+                                     throw new ArgumentNullException(nameof(documentVersionRepository));
+    }
 
-        public DefaultContentVersionCleanupPolicy(IOptions contentSettings, ICoreScopeProvider scopeProvider, IDocumentVersionRepository documentVersionRepository)
+    public IEnumerable Apply(DateTime asAtDate, IEnumerable items)
+    {
+        // Note: Not checking global enable flag, that's handled in the scheduled job.
+        // If this method is called and policy is globally disabled someone has chosen to run in code.
+        Configuration.Models.ContentVersionCleanupPolicySettings globalPolicy =
+            _contentSettings.Value.ContentVersionCleanupPolicy;
+
+        var theRest = new List();
+
+        using (_scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
-            _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
-            _documentVersionRepository = documentVersionRepository ?? throw new ArgumentNullException(nameof(documentVersionRepository));
-        }
+            var policyOverrides = _documentVersionRepository.GetCleanupPolicies()?
+                .ToDictionary(x => x.ContentTypeId);
 
-        public IEnumerable Apply(DateTime asAtDate, IEnumerable items)
-        {
-            // Note: Not checking global enable flag, that's handled in the scheduled job.
-            // If this method is called and policy is globally disabled someone has chosen to run in code.
-
-            var globalPolicy = _contentSettings.Value.ContentVersionCleanupPolicy;
-
-            var theRest = new List();
-
-            using(_scopeProvider.CreateCoreScope(autoComplete: true))
+            foreach (ContentVersionMeta version in items)
             {
-                var policyOverrides = _documentVersionRepository.GetCleanupPolicies()?
-                    .ToDictionary(x => x.ContentTypeId);
+                TimeSpan age = asAtDate - version.VersionDate;
 
-                foreach (var version in items)
+                ContentVersionCleanupPolicySettings? overrides = GetOverridePolicy(version, policyOverrides);
+
+                var keepAll = overrides?.KeepAllVersionsNewerThanDays ?? globalPolicy.KeepAllVersionsNewerThanDays;
+                var keepLatest = overrides?.KeepLatestVersionPerDayForDays ??
+                                 globalPolicy.KeepLatestVersionPerDayForDays;
+                var preventCleanup = overrides?.PreventCleanup ?? false;
+
+                if (preventCleanup)
                 {
-                    var age = asAtDate - version.VersionDate;
-
-                    var overrides = GetOverridePolicy(version, policyOverrides);
-
-                    var keepAll = overrides?.KeepAllVersionsNewerThanDays ?? globalPolicy.KeepAllVersionsNewerThanDays!;
-                    var keepLatest = overrides?.KeepLatestVersionPerDayForDays ?? globalPolicy.KeepLatestVersionPerDayForDays;
-                    var preventCleanup = overrides?.PreventCleanup ?? false;
-
-                    if (preventCleanup)
-                    {
-                        continue;
-                    }
-
-                    if (age.TotalDays <= keepAll)
-                    {
-                        continue;
-                    }
-
-                    if (age.TotalDays > keepLatest)
-                    {
-
-                        yield return version;
-                        continue;
-                    }
-
-                    theRest.Add(version);
+                    continue;
                 }
 
-                var grouped = theRest.GroupBy(x => new
+                if (age.TotalDays <= keepAll)
                 {
-                    x.ContentId,
-                    x.VersionDate.Date
-                });
+                    continue;
+                }
 
-                foreach (var group in grouped)
+                if (age.TotalDays > keepLatest)
                 {
-                    foreach (var version in group.OrderByDescending(x => x.VersionId).Skip(1))
-                    {
-                        yield return version;
-                    }
+                    yield return version;
+                    continue;
+                }
+
+                theRest.Add(version);
+            }
+
+            var grouped = theRest.GroupBy(x => new { x.ContentId, x.VersionDate.Date });
+
+            foreach (var group in grouped)
+            {
+                foreach (ContentVersionMeta version in group.OrderByDescending(x => x.VersionId).Skip(1))
+                {
+                    yield return version;
                 }
             }
         }
-
-        private ContentVersionCleanupPolicySettings? GetOverridePolicy(
-            ContentVersionMeta version,
-            IDictionary? overrides)
-        {
-            if (overrides is null)
-            {
-                return null;
-            }
-
-            _ = overrides.TryGetValue(version.ContentTypeId, out var value);
-
-            return value;
-        }
+    }
+
+    private ContentVersionCleanupPolicySettings? GetOverridePolicy(
+        ContentVersionMeta version,
+        IDictionary? overrides)
+    {
+        if (overrides is null)
+        {
+            return null;
+        }
+
+        _ = overrides.TryGetValue(version.ContentTypeId, out ContentVersionCleanupPolicySettings? value);
+
+        return value;
     }
 }
diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs
index b319f0fc42..38f27bb94c 100644
--- a/src/Umbraco.Core/Services/DomainService.cs
+++ b/src/Umbraco.Core/Services/DomainService.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -6,100 +5,102 @@ using Umbraco.Cms.Core.Notifications;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class DomainService : RepositoryService, IDomainService
 {
-    public class DomainService : RepositoryService, IDomainService
+    private readonly IDomainRepository _domainRepository;
+
+    public DomainService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IDomainRepository domainRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _domainRepository = domainRepository;
+
+    public bool Exists(string domainName)
     {
-        private readonly IDomainRepository _domainRepository;
-
-        public DomainService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IDomainRepository domainRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _domainRepository = domainRepository;
-        }
-
-        public bool Exists(string domainName)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.Exists(domainName);
-            }
-        }
-
-        public Attempt Delete(IDomain domain)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var deletingNotification = new DomainDeletingNotification(domain, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Cancel(eventMessages);
-                }
-
-                _domainRepository.Delete(domain);
-                scope.Complete();
-
-                scope.Notifications.Publish(new DomainDeletedNotification(domain, eventMessages).WithStateFrom(deletingNotification));
-            }
-
-            return OperationResult.Attempt.Succeed(eventMessages);
-        }
-
-        public IDomain? GetByName(string name)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetByName(name);
-            }
-        }
-
-        public IDomain? GetById(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.Get(id);
-            }
-        }
-
-        public IEnumerable GetAll(bool includeWildcards)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetAll(includeWildcards);
-            }
-        }
-
-        public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _domainRepository.GetAssignedDomains(contentId, includeWildcards);
-            }
-        }
-
-        public Attempt Save(IDomain domainEntity)
-        {
-            EventMessages eventMessages = EventMessagesFactory.Get();
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingNotification = new DomainSavingNotification(domainEntity, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Cancel(eventMessages);
-                }
-
-                _domainRepository.Save(domainEntity);
-                scope.Complete();
-                scope.Notifications.Publish(new DomainSavedNotification(domainEntity, eventMessages).WithStateFrom(savingNotification));
-            }
-
-            return OperationResult.Attempt.Succeed(eventMessages);
+            return _domainRepository.Exists(domainName);
         }
     }
+
+    public Attempt Delete(IDomain domain)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var deletingNotification = new DomainDeletingNotification(domain, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages);
+            }
+
+            _domainRepository.Delete(domain);
+            scope.Complete();
+
+            scope.Notifications.Publish(
+                new DomainDeletedNotification(domain, eventMessages).WithStateFrom(deletingNotification));
+        }
+
+        return OperationResult.Attempt.Succeed(eventMessages);
+    }
+
+    public IDomain? GetByName(string name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _domainRepository.GetByName(name);
+        }
+    }
+
+    public IDomain? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _domainRepository.Get(id);
+        }
+    }
+
+    public IEnumerable GetAll(bool includeWildcards)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _domainRepository.GetAll(includeWildcards);
+        }
+    }
+
+    public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _domainRepository.GetAssignedDomains(contentId, includeWildcards);
+        }
+    }
+
+    public Attempt Save(IDomain domainEntity)
+    {
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            var savingNotification = new DomainSavingNotification(domainEntity, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return OperationResult.Attempt.Cancel(eventMessages);
+            }
+
+            _domainRepository.Save(domainEntity);
+            scope.Complete();
+            scope.Notifications.Publish(
+                new DomainSavedNotification(domainEntity, eventMessages).WithStateFrom(savingNotification));
+        }
+
+        return OperationResult.Attempt.Succeed(eventMessages);
+    }
 }
diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs
index 5fa7ed24f7..591fa17909 100644
--- a/src/Umbraco.Core/Services/EntityService.cs
+++ b/src/Umbraco.Core/Services/EntityService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using System.Linq.Expressions;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
@@ -11,472 +8,519 @@ using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class EntityService : RepositoryService, IEntityService
 {
-    public class EntityService : RepositoryService, IEntityService
+    private readonly IEntityRepository _entityRepository;
+    private readonly IIdKeyMap _idKeyMap;
+    private readonly Dictionary _objectTypes;
+    private IQuery? _queryRootEntity;
+
+    public EntityService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IIdKeyMap idKeyMap,
+        IEntityRepository entityRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IEntityRepository _entityRepository;
-        private readonly Dictionary _objectTypes;
-        private IQuery? _queryRootEntity;
-        private readonly IIdKeyMap _idKeyMap;
+        _idKeyMap = idKeyMap;
+        _entityRepository = entityRepository;
 
-        public EntityService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IIdKeyMap idKeyMap, IEntityRepository entityRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+        _objectTypes = new Dictionary
         {
-            _idKeyMap = idKeyMap;
-            _entityRepository = entityRepository;
+            { typeof(IDataType).FullName!, UmbracoObjectTypes.DataType },
+            { typeof(IContent).FullName!, UmbracoObjectTypes.Document },
+            { typeof(IContentType).FullName!, UmbracoObjectTypes.DocumentType },
+            { typeof(IMedia).FullName!, UmbracoObjectTypes.Media },
+            { typeof(IMediaType).FullName!, UmbracoObjectTypes.MediaType },
+            { typeof(IMember).FullName!, UmbracoObjectTypes.Member },
+            { typeof(IMemberType).FullName!, UmbracoObjectTypes.MemberType },
+        };
+    }
 
-            _objectTypes = new Dictionary
-            {
-                { typeof (IDataType).FullName!, UmbracoObjectTypes.DataType },
-                { typeof (IContent).FullName!, UmbracoObjectTypes.Document },
-                { typeof (IContentType).FullName!, UmbracoObjectTypes.DocumentType },
-                { typeof (IMedia).FullName!, UmbracoObjectTypes.Media },
-                { typeof (IMediaType).FullName!, UmbracoObjectTypes.MediaType },
-                { typeof (IMember).FullName!, UmbracoObjectTypes.Member },
-                { typeof (IMemberType).FullName!, UmbracoObjectTypes.MemberType },
-            };
-        }
+    #region Static Queries
 
-        #region Static Queries
+    // lazy-constructed because when the ctor runs, the query factory may not be ready
+    private IQuery QueryRootEntity => _queryRootEntity ??= Query()
+                                                          .Where(x => x.ParentId == -1);
 
-        // lazy-constructed because when the ctor runs, the query factory may not be ready
-        private IQuery QueryRootEntity => _queryRootEntity
-            ?? (_queryRootEntity = Query().Where(x => x.ParentId == -1));
+    #endregion
 
-        #endregion
-
-        // gets the object type, throws if not supported
-        private UmbracoObjectTypes GetObjectType(Type ?type)
+    /// 
+    public IEntitySlim? Get(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out var objType))
-                throw new NotSupportedException($"Type \"{type?.FullName ?? ""}\" is not supported here.");
-            return objType;
-        }
-
-        /// 
-        public IEntitySlim? Get(int id)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id);
-            }
-        }
-
-        /// 
-        public IEntitySlim? Get(Guid key)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key);
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? Get(int id, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? Get(int id)
-            where T : IUmbracoEntity
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(id);
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? Get(Guid key)
-            where T : IUmbracoEntity
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Get(key);
-            }
-        }
-
-        /// 
-        public bool Exists(int id)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Exists(id);
-            }
-        }
-
-        /// 
-        public bool Exists(Guid key)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.Exists(key);
-            }
-        }
-
-
-        /// 
-        public virtual IEnumerable GetAll() where T : IUmbracoEntity
-            => GetAll(Array.Empty());
-
-        /// 
-        public virtual IEnumerable GetAll(params int[] ids)
-            where T : IUmbracoEntity
-        {
-            var entityType = typeof (T);
-            var objectType = GetObjectType(entityType);
-            var objectTypeId = objectType.GetGuid();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectTypeId, ids);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAll(UmbracoObjectTypes objectType)
-            => GetAll(objectType, Array.Empty());
-
-        /// 
-        public virtual IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids)
-        {
-            var entityType = objectType.GetClrType();
-            if (entityType == null)
-                throw new NotSupportedException($"Type \"{objectType}\" is not supported here.");
-
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType.GetGuid(), ids);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAll(Guid objectType)
-            => GetAll(objectType, Array.Empty());
-
-        /// 
-        public virtual IEnumerable GetAll(Guid objectType, params int[] ids)
-        {
-            var entityType = ObjectTypes.GetClrType(objectType);
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType, ids);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAll(params Guid[] keys)
-            where T : IUmbracoEntity
-        {
-            var entityType = typeof (T);
-            var objectType = GetObjectType(entityType);
-            var objectTypeId = objectType.GetGuid();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectTypeId, keys);
-            }
-        }
-
-        /// 
-        public IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys)
-        {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType.GetGuid(), keys);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys)
-        {
-            var entityType = ObjectTypes.GetClrType(objectType);
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAll(objectType, keys);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetRootEntities(UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetByQuery(QueryRootEntity, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? GetParent(int id)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var entity = _entityRepository.Get(id);
-                if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
-                    return null;
-                return _entityRepository.Get(entity.ParentId);
-            }
-        }
-
-        /// 
-        public virtual IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var entity = _entityRepository.Get(id);
-                if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
-                    return null;
-                return _entityRepository.Get(entity.ParentId, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetChildren(int parentId)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == parentId);
-                return _entityRepository.GetByQuery(query);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == parentId);
-                return _entityRepository.GetByQuery(query, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetDescendants(int id)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var entity = _entityRepository.Get(id);
-                var pathMatch = entity?.Path + ",";
-                var query = Query().Where(x => x.Path.StartsWith(pathMatch) && x.Id != id);
-                return _entityRepository.GetByQuery(query);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var entity = _entityRepository.Get(id);
-                if (entity is null)
-                {
-                    return Enumerable.Empty();
-                }
-                var query = Query().Where(x => x.Path.StartsWith(entity.Path) && x.Id != id);
-                return _entityRepository.GetByQuery(query, objectType.GetGuid());
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedChildren(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == id && x.Trashed == false);
-
-                return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var objectTypeGuid = objectType.GetGuid();
-                var query = Query();
-
-                if (id != Cms.Core.Constants.System.Root)
-                {
-                    // lookup the path so we can use it in the prefix query below
-                    var paths = _entityRepository.GetAllPaths(objectTypeGuid, id).ToArray();
-                    if (paths.Length == 0)
-                    {
-                        totalRecords = 0;
-                        return Enumerable.Empty();
-                    }
-                    var path = paths[0].Path;
-                    query.Where(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar));
-                }
-
-                return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
-        {
-            totalRecords = 0;
-
-            var idsA = ids.ToArray();
-            if (idsA.Length == 0)
-                return Enumerable.Empty();
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var objectTypeGuid = objectType.GetGuid();
-                var query = Query();
-
-                if (idsA.All(x => x != Cms.Core.Constants.System.Root))
-                {
-                    var paths = _entityRepository.GetAllPaths(objectTypeGuid, idsA).ToArray();
-                    if (paths.Length == 0)
-                    {
-                        totalRecords = 0;
-                        return Enumerable.Empty();
-                    }
-                    var clauses = new List>>();
-                    foreach (var id in idsA)
-                    {
-                        // if the id is root then don't add any clauses
-                        if (id == Cms.Core.Constants.System.Root) continue;
-
-                        var entityPath = paths.FirstOrDefault(x => x.Id == id);
-                        if (entityPath == null) continue;
-
-                        var path = entityPath.Path;
-                        var qid = id;
-                        clauses.Add(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) || x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar));
-                    }
-                    query.WhereAny(clauses);
-                }
-
-                return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        public IEnumerable GetPagedDescendants(UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null, bool includeTrashed = true)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query();
-                if (includeTrashed == false)
-                    query.Where(x => x.Trashed == false);
-
-                return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
-            }
-        }
-
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetObjectType(id);
-            }
-        }
-
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(Guid key)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetObjectType(key);
-            }
-        }
-
-        /// 
-        public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity)
-        {
-            return entity is IEntitySlim light
-                ? ObjectTypes.GetUmbracoObjectType(light.NodeObjectType)
-                : GetObjectType(entity.Id);
-        }
-
-        /// 
-        public virtual Type? GetEntityType(int id)
-        {
-            var objectType = GetObjectType(id);
-            return objectType.GetClrType();
-        }
-
-        /// 
-        public Attempt GetId(Guid key, UmbracoObjectTypes objectType)
-        {
-            return _idKeyMap.GetIdForKey(key, objectType);
-        }
-
-        /// 
-        public Attempt GetId(Udi udi)
-        {
-            return _idKeyMap.GetIdForUdi(udi);
-        }
-
-        /// 
-        public Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType)
-        {
-            return _idKeyMap.GetKeyForId(id, umbracoObjectType);
-        }
-
-        /// 
-        public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids)
-        {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAllPaths(objectType.GetGuid(), ids);
-            }
-        }
-
-        /// 
-        public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys)
-        {
-            var entityType = objectType.GetClrType();
-            GetObjectType(entityType);
-
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.GetAllPaths(objectType.GetGuid(), keys);
-            }
-        }
-
-        /// 
-        public int ReserveId(Guid key)
-        {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _entityRepository.ReserveId(key);
-            }
+            return _entityRepository.Get(id);
         }
     }
+
+    /// 
+    public IEntitySlim? Get(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(key);
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? Get(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(id, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(key, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? Get(int id)
+        where T : IUmbracoEntity
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(id);
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? Get(Guid key)
+        where T : IUmbracoEntity
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Get(key);
+        }
+    }
+
+    /// 
+    public bool Exists(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Exists(id);
+        }
+    }
+
+    /// 
+    public bool Exists(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.Exists(key);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll()
+        where T : IUmbracoEntity
+        => GetAll(Array.Empty());
+
+    /// 
+    public virtual IEnumerable GetAll(params int[] ids)
+        where T : IUmbracoEntity
+    {
+        Type entityType = typeof(T);
+        UmbracoObjectTypes objectType = GetObjectType(entityType);
+        Guid objectTypeId = objectType.GetGuid();
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectTypeId, ids);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll(UmbracoObjectTypes objectType)
+        => GetAll(objectType, Array.Empty());
+
+    /// 
+    public virtual IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids)
+    {
+        Type? entityType = objectType.GetClrType();
+        if (entityType == null)
+        {
+            throw new NotSupportedException($"Type \"{objectType}\" is not supported here.");
+        }
+
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType.GetGuid(), ids);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll(Guid objectType)
+        => GetAll(objectType, Array.Empty());
+
+    /// 
+    public virtual IEnumerable GetAll(Guid objectType, params int[] ids)
+    {
+        Type? entityType = ObjectTypes.GetClrType(objectType);
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType, ids);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll(params Guid[] keys)
+        where T : IUmbracoEntity
+    {
+        Type entityType = typeof(T);
+        UmbracoObjectTypes objectType = GetObjectType(entityType);
+        Guid objectTypeId = objectType.GetGuid();
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectTypeId, keys);
+        }
+    }
+
+    /// 
+    public IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys)
+    {
+        Type? entityType = objectType.GetClrType();
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType.GetGuid(), keys);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys)
+    {
+        Type? entityType = ObjectTypes.GetClrType(objectType);
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAll(objectType, keys);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetRootEntities(UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetByQuery(QueryRootEntity, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? GetParent(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
+            {
+                return null;
+            }
+
+            return _entityRepository.Get(entity.ParentId);
+        }
+    }
+
+    /// 
+    public virtual IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null || entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21)
+            {
+                return null;
+            }
+
+            return _entityRepository.Get(entity.ParentId, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetChildren(int parentId)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query().Where(x => x.ParentId == parentId);
+            return _entityRepository.GetByQuery(query);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query().Where(x => x.ParentId == parentId);
+            return _entityRepository.GetByQuery(query, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetDescendants(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IEntitySlim? entity = _entityRepository.Get(id);
+            var pathMatch = entity?.Path + ",";
+            IQuery query = Query()
+                .Where(x => x.Path.StartsWith(pathMatch) && x.Id != id);
+            return _entityRepository.GetByQuery(query);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IEntitySlim? entity = _entityRepository.Get(id);
+            if (entity is null)
+            {
+                return Enumerable.Empty();
+            }
+
+            IQuery query = Query()
+                .Where(x => x.Path.StartsWith(entity.Path) && x.Id != id);
+            return _entityRepository.GetByQuery(query, objectType.GetGuid());
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedChildren(
+        int id,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == false);
+
+            return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedDescendants(
+        int id,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            Guid objectTypeGuid = objectType.GetGuid();
+            IQuery query = Query();
+
+            if (id != Constants.System.Root)
+            {
+                // lookup the path so we can use it in the prefix query below
+                TreeEntityPath[] paths = _entityRepository.GetAllPaths(objectTypeGuid, id).ToArray();
+                if (paths.Length == 0)
+                {
+                    totalRecords = 0;
+                    return Enumerable.Empty();
+                }
+
+                var path = paths[0].Path;
+                query.Where(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar));
+            }
+
+            return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedDescendants(
+        IEnumerable ids,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null)
+    {
+        totalRecords = 0;
+
+        var idsA = ids.ToArray();
+        if (idsA.Length == 0)
+        {
+            return Enumerable.Empty();
+        }
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            Guid objectTypeGuid = objectType.GetGuid();
+            IQuery query = Query();
+
+            if (idsA.All(x => x != Constants.System.Root))
+            {
+                TreeEntityPath[] paths = _entityRepository.GetAllPaths(objectTypeGuid, idsA).ToArray();
+                if (paths.Length == 0)
+                {
+                    totalRecords = 0;
+                    return Enumerable.Empty();
+                }
+
+                var clauses = new List>>();
+                foreach (var id in idsA)
+                {
+                    // if the id is root then don't add any clauses
+                    if (id == Constants.System.Root)
+                    {
+                        continue;
+                    }
+
+                    TreeEntityPath? entityPath = paths.FirstOrDefault(x => x.Id == id);
+                    if (entityPath == null)
+                    {
+                        continue;
+                    }
+
+                    var path = entityPath.Path;
+                    var qid = id;
+                    clauses.Add(x =>
+                        x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) ||
+                        x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar));
+                }
+
+                query.WhereAny(clauses);
+            }
+
+            return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuid, pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
+
+    /// 
+    public IEnumerable GetPagedDescendants(
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null,
+        bool includeTrashed = true)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery query = Query();
+            if (includeTrashed == false)
+            {
+                query.Where(x => x.Trashed == false);
+            }
+
+            return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
+        }
+    }
+
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetObjectType(id);
+        }
+    }
+
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(Guid key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetObjectType(key);
+        }
+    }
+
+    /// 
+    public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity) =>
+        entity is IEntitySlim light
+            ? ObjectTypes.GetUmbracoObjectType(light.NodeObjectType)
+            : GetObjectType(entity.Id);
+
+    /// 
+    public virtual Type? GetEntityType(int id)
+    {
+        UmbracoObjectTypes objectType = GetObjectType(id);
+        return objectType.GetClrType();
+    }
+
+    /// 
+    public Attempt GetId(Guid key, UmbracoObjectTypes objectType) => _idKeyMap.GetIdForKey(key, objectType);
+
+    /// 
+    public Attempt GetId(Udi udi) => _idKeyMap.GetIdForUdi(udi);
+
+    /// 
+    public Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType) =>
+        _idKeyMap.GetKeyForId(id, umbracoObjectType);
+
+    /// 
+    public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids)
+    {
+        Type? entityType = objectType.GetClrType();
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAllPaths(objectType.GetGuid(), ids);
+        }
+    }
+
+    /// 
+    public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys)
+    {
+        Type? entityType = objectType.GetClrType();
+        GetObjectType(entityType);
+
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.GetAllPaths(objectType.GetGuid(), keys);
+        }
+    }
+
+    /// 
+    public int ReserveId(Guid key)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _entityRepository.ReserveId(key);
+        }
+    }
+
+    // gets the object type, throws if not supported
+    private UmbracoObjectTypes GetObjectType(Type? type)
+    {
+        if (type?.FullName == null || !_objectTypes.TryGetValue(type.FullName, out UmbracoObjectTypes objType))
+        {
+            throw new NotSupportedException($"Type \"{type?.FullName ?? ""}\" is not supported here.");
+        }
+
+        return objType;
+    }
 }
diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs
index c91f536b38..0a744f3f0f 100644
--- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs
+++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using System.Net;
 using System.Xml.Linq;
 using Umbraco.Cms.Core.Models;
@@ -11,685 +8,745 @@ using Umbraco.Cms.Core.Serialization;
 using Umbraco.Cms.Core.Strings;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Serializes entities to XML
+/// 
+internal class EntityXmlSerializer : IEntityXmlSerializer
 {
-    /// 
-    /// Serializes entities to XML
-    /// 
-    internal class EntityXmlSerializer : IEntityXmlSerializer
+    private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
+    private readonly IContentService _contentService;
+    private readonly IContentTypeService _contentTypeService;
+    private readonly IDataTypeService _dataTypeService;
+    private readonly ILocalizationService _localizationService;
+    private readonly IMediaService _mediaService;
+    private readonly PropertyEditorCollection _propertyEditors;
+    private readonly IShortStringHelper _shortStringHelper;
+    private readonly UrlSegmentProviderCollection _urlSegmentProviders;
+    private readonly IUserService _userService;
+
+    public EntityXmlSerializer(
+        IContentService contentService,
+        IMediaService mediaService,
+        IDataTypeService dataTypeService,
+        IUserService userService,
+        ILocalizationService localizationService,
+        IContentTypeService contentTypeService,
+        UrlSegmentProviderCollection urlSegmentProviders,
+        IShortStringHelper shortStringHelper,
+        PropertyEditorCollection propertyEditors,
+        IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
     {
-        private readonly IContentTypeService _contentTypeService;
-        private readonly IMediaService _mediaService;
-        private readonly IContentService _contentService;
-        private readonly IDataTypeService _dataTypeService;
-        private readonly IUserService _userService;
-        private readonly ILocalizationService _localizationService;
-        private readonly UrlSegmentProviderCollection _urlSegmentProviders;
-        private readonly IShortStringHelper _shortStringHelper;
-        private readonly PropertyEditorCollection _propertyEditors;
-        private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
+        _contentTypeService = contentTypeService;
+        _mediaService = mediaService;
+        _contentService = contentService;
+        _dataTypeService = dataTypeService;
+        _userService = userService;
+        _localizationService = localizationService;
+        _urlSegmentProviders = urlSegmentProviders;
+        _shortStringHelper = shortStringHelper;
+        _propertyEditors = propertyEditors;
+        _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
+    }
 
-        public EntityXmlSerializer(
-            IContentService contentService,
-            IMediaService mediaService,
-            IDataTypeService dataTypeService,
-            IUserService userService,
-            ILocalizationService localizationService,
-            IContentTypeService contentTypeService,
-            UrlSegmentProviderCollection urlSegmentProviders,
-            IShortStringHelper shortStringHelper,
-            PropertyEditorCollection propertyEditors,
-            IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
+    /// 
+    ///     Exports an IContent item as an XElement.
+    /// 
+    public XElement Serialize(
+        IContent content,
+        bool published,
+        bool withDescendants = false) // TODO: take care of usage! only used for the packager
+    {
+        if (content == null)
         {
-            _contentTypeService = contentTypeService;
-            _mediaService = mediaService;
-            _contentService = contentService;
-            _dataTypeService = dataTypeService;
-            _userService = userService;
-            _localizationService = localizationService;
-            _urlSegmentProviders = urlSegmentProviders;
-            _shortStringHelper = shortStringHelper;
-            _propertyEditors = propertyEditors;
-            _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
+            throw new ArgumentNullException(nameof(content));
         }
 
-        /// 
-        /// Exports an IContent item as an XElement.
-        /// 
-        public XElement Serialize(IContent content,
-            bool published,
-            bool withDescendants = false) // TODO: take care of usage! only used for the packager
+        var nodeName = content.ContentType.Alias.ToSafeAlias(_shortStringHelper);
+
+        XElement xml = SerializeContentBase(content, content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders), nodeName, published);
+
+        xml.Add(new XAttribute("nodeType", content.ContentType.Id));
+        xml.Add(new XAttribute("nodeTypeAlias", content.ContentType.Alias));
+
+        xml.Add(new XAttribute("creatorName", content.GetCreatorProfile(_userService)?.Name ?? "??"));
+
+        // xml.Add(new XAttribute("creatorID", content.CreatorId));
+        xml.Add(new XAttribute("writerName", content.GetWriterProfile(_userService)?.Name ?? "??"));
+        xml.Add(new XAttribute("writerID", content.WriterId));
+
+        xml.Add(new XAttribute("template", content.TemplateId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty));
+
+        xml.Add(new XAttribute("isPublished", content.Published));
+
+        if (withDescendants)
         {
-            if (content == null) throw new ArgumentNullException(nameof(content));
-
-            var nodeName = content.ContentType.Alias.ToSafeAlias(_shortStringHelper);
-
-            var xml = SerializeContentBase(content, content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders), nodeName, published);
-
-            xml.Add(new XAttribute("nodeType", content.ContentType.Id));
-            xml.Add(new XAttribute("nodeTypeAlias", content.ContentType.Alias));
-
-            xml.Add(new XAttribute("creatorName", content.GetCreatorProfile(_userService)?.Name ?? "??"));
-            //xml.Add(new XAttribute("creatorID", content.CreatorId));
-            xml.Add(new XAttribute("writerName", content.GetWriterProfile(_userService)?.Name ?? "??"));
-            xml.Add(new XAttribute("writerID", content.WriterId));
-
-            xml.Add(new XAttribute("template", content.TemplateId?.ToString(CultureInfo.InvariantCulture) ?? ""));
-
-            xml.Add(new XAttribute("isPublished", content.Published));
-
-            if (withDescendants)
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
             {
-                const int pageSize = 500;
-                var page = 0;
-                var total = long.MaxValue;
-                while(page * pageSize < total)
+                IEnumerable children =
+                    _contentService.GetPagedChildren(content.Id, page++, pageSize, out total);
+                SerializeChildren(children, xml, published);
+            }
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports an IMedia item as an XElement.
+    /// 
+    public XElement Serialize(
+        IMedia media,
+        bool withDescendants = false,
+        Action? onMediaItemSerialized = null)
+    {
+        if (_mediaService == null)
+        {
+            throw new ArgumentNullException(nameof(_mediaService));
+        }
+
+        if (_dataTypeService == null)
+        {
+            throw new ArgumentNullException(nameof(_dataTypeService));
+        }
+
+        if (_userService == null)
+        {
+            throw new ArgumentNullException(nameof(_userService));
+        }
+
+        if (_localizationService == null)
+        {
+            throw new ArgumentNullException(nameof(_localizationService));
+        }
+
+        if (media == null)
+        {
+            throw new ArgumentNullException(nameof(media));
+        }
+
+        if (_urlSegmentProviders == null)
+        {
+            throw new ArgumentNullException(nameof(_urlSegmentProviders));
+        }
+
+        var nodeName = media.ContentType.Alias.ToSafeAlias(_shortStringHelper);
+
+        const bool published = false; // always false for media
+        var urlValue = media.GetUrlSegment(_shortStringHelper, _urlSegmentProviders);
+        XElement xml = SerializeContentBase(media, urlValue, nodeName, published);
+
+        xml.Add(new XAttribute("nodeType", media.ContentType.Id));
+        xml.Add(new XAttribute("nodeTypeAlias", media.ContentType.Alias));
+
+        // xml.Add(new XAttribute("creatorName", media.GetCreatorProfile(userService).Name));
+        // xml.Add(new XAttribute("creatorID", media.CreatorId));
+        xml.Add(new XAttribute("writerName", media.GetWriterProfile(_userService)?.Name ?? string.Empty));
+        xml.Add(new XAttribute("writerID", media.WriterId));
+        xml.Add(new XAttribute("udi", media.GetUdi()));
+
+        // xml.Add(new XAttribute("template", 0)); // no template for media
+        onMediaItemSerialized?.Invoke(media, xml);
+
+        if (withDescendants)
+        {
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
+            {
+                IEnumerable children = _mediaService.GetPagedChildren(media.Id, page++, pageSize, out total);
+                SerializeChildren(children, xml, onMediaItemSerialized);
+            }
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports an IMember item as an XElement.
+    /// 
+    public XElement Serialize(IMember member)
+    {
+        var nodeName = member.ContentType.Alias.ToSafeAlias(_shortStringHelper);
+
+        const bool published = false; // always false for member
+        XElement xml = SerializeContentBase(member, string.Empty, nodeName, published);
+
+        xml.Add(new XAttribute("nodeType", member.ContentType.Id));
+        xml.Add(new XAttribute("nodeTypeAlias", member.ContentType.Alias));
+
+        // what about writer/creator/version?
+        xml.Add(new XAttribute("loginName", member.Username));
+        xml.Add(new XAttribute("email", member.Email));
+        xml.Add(new XAttribute("icon", member.ContentType.Icon!));
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of Data Types
+    /// 
+    /// List of data types to export
+    ///  containing the xml representation of the IDataTypeDefinition objects
+    public XElement Serialize(IEnumerable dataTypeDefinitions)
+    {
+        var container = new XElement("DataTypes");
+        foreach (IDataType dataTypeDefinition in dataTypeDefinitions)
+        {
+            container.Add(Serialize(dataTypeDefinition));
+        }
+
+        return container;
+    }
+
+    public XElement Serialize(IDataType dataType)
+    {
+        var xml = new XElement("DataType");
+        xml.Add(new XAttribute("Name", dataType.Name!));
+
+        // The 'ID' when exporting is actually the property editor alias (in pre v7 it was the IDataType GUID id)
+        xml.Add(new XAttribute("Id", dataType.EditorAlias));
+        xml.Add(new XAttribute("Definition", dataType.Key));
+        xml.Add(new XAttribute("DatabaseType", dataType.DatabaseType.ToString()));
+        xml.Add(new XAttribute("Configuration", _configurationEditorJsonSerializer.Serialize(dataType.Configuration)));
+
+        var folderNames = string.Empty;
+        var folderKeys = string.Empty;
+        if (dataType.Level != 1)
+        {
+            // get URL encoded folder names
+            IOrderedEnumerable folders = _dataTypeService.GetContainers(dataType)
+                .OrderBy(x => x.Level);
+
+            folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
+            folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
+        }
+
+        if (string.IsNullOrWhiteSpace(folderNames) == false)
+        {
+            xml.Add(new XAttribute("Folders", folderNames));
+            xml.Add(new XAttribute("FolderKeys", folderKeys));
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// List of dictionary items to export
+    /// Optional boolean indicating whether or not to include children
+    ///  containing the xml representation of the IDictionaryItem objects
+    public XElement Serialize(IEnumerable dictionaryItem, bool includeChildren = true)
+    {
+        var xml = new XElement("DictionaryItems");
+        foreach (IDictionaryItem item in dictionaryItem)
+        {
+            xml.Add(Serialize(item, includeChildren));
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a single  item to xml as an 
+    /// 
+    /// Dictionary Item to export
+    /// Optional boolean indicating whether or not to include children
+    ///  containing the xml representation of the IDictionaryItem object
+    public XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren)
+    {
+        XElement xml = Serialize(dictionaryItem);
+
+        if (includeChildren)
+        {
+            IEnumerable? children = _localizationService.GetDictionaryItemChildren(dictionaryItem.Key);
+            if (children is not null)
+            {
+                foreach (IDictionaryItem child in children)
                 {
-                    var children = _contentService.GetPagedChildren(content.Id, page++, pageSize, out total);
-                    SerializeChildren(children, xml, published);
-                }
-
-            }
-
-            return xml;
-        }
-
-        /// 
-        /// Exports an IMedia item as an XElement.
-        /// 
-        public XElement Serialize(
-            IMedia media,
-            bool withDescendants = false,
-            Action? onMediaItemSerialized = null)
-        {
-            if (_mediaService == null) throw new ArgumentNullException(nameof(_mediaService));
-            if (_dataTypeService == null) throw new ArgumentNullException(nameof(_dataTypeService));
-            if (_userService == null) throw new ArgumentNullException(nameof(_userService));
-            if (_localizationService == null) throw new ArgumentNullException(nameof(_localizationService));
-            if (media == null) throw new ArgumentNullException(nameof(media));
-            if (_urlSegmentProviders == null) throw new ArgumentNullException(nameof(_urlSegmentProviders));
-
-            var nodeName = media.ContentType.Alias.ToSafeAlias(_shortStringHelper);
-
-            const bool published = false; // always false for media
-            string? urlValue = media.GetUrlSegment(_shortStringHelper, _urlSegmentProviders);
-            XElement xml = SerializeContentBase(media, urlValue, nodeName, published);
-
-
-            xml.Add(new XAttribute("nodeType", media.ContentType.Id));
-            xml.Add(new XAttribute("nodeTypeAlias", media.ContentType.Alias));
-
-            //xml.Add(new XAttribute("creatorName", media.GetCreatorProfile(userService).Name));
-            //xml.Add(new XAttribute("creatorID", media.CreatorId));
-            xml.Add(new XAttribute("writerName", media.GetWriterProfile(_userService)?.Name ?? string.Empty));
-            xml.Add(new XAttribute("writerID", media.WriterId));
-            xml.Add(new XAttribute("udi", media.GetUdi()));
-
-            //xml.Add(new XAttribute("template", 0)); // no template for media
-
-            onMediaItemSerialized?.Invoke(media, xml);
-
-            if (withDescendants)
-            {
-                const int pageSize = 500;
-                var page = 0;
-                var total = long.MaxValue;
-                while (page * pageSize < total)
-                {
-                    var children = _mediaService.GetPagedChildren(media.Id, page++, pageSize, out total);
-                    SerializeChildren(children, xml, onMediaItemSerialized);
-                }
-            }
-
-            return xml;
-        }
-
-        /// 
-        /// Exports an IMember item as an XElement.
-        /// 
-        public XElement Serialize(IMember member)
-        {
-            var nodeName = member.ContentType.Alias.ToSafeAlias(_shortStringHelper);
-
-            const bool published = false; // always false for member
-            var xml = SerializeContentBase(member, "", nodeName, published);
-
-            xml.Add(new XAttribute("nodeType", member.ContentType.Id));
-            xml.Add(new XAttribute("nodeTypeAlias", member.ContentType.Alias));
-
-            // what about writer/creator/version?
-
-            xml.Add(new XAttribute("loginName", member.Username!));
-            xml.Add(new XAttribute("email", member.Email!));
-            xml.Add(new XAttribute("icon", member.ContentType.Icon!));
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of Data Types
-        /// 
-        /// List of data types to export
-        ///  containing the xml representation of the IDataTypeDefinition objects
-        public XElement Serialize(IEnumerable dataTypeDefinitions)
-        {
-            var container = new XElement("DataTypes");
-            foreach (var dataTypeDefinition in dataTypeDefinitions)
-            {
-                container.Add(Serialize(dataTypeDefinition));
-            }
-            return container;
-        }
-
-        public XElement Serialize(IDataType dataType)
-        {
-            var xml = new XElement("DataType");
-            xml.Add(new XAttribute("Name", dataType.Name!));
-            //The 'ID' when exporting is actually the property editor alias (in pre v7 it was the IDataType GUID id)
-            xml.Add(new XAttribute("Id", dataType.EditorAlias));
-            xml.Add(new XAttribute("Definition", dataType.Key));
-            xml.Add(new XAttribute("DatabaseType", dataType.DatabaseType.ToString()));
-            xml.Add(new XAttribute("Configuration", _configurationEditorJsonSerializer.Serialize(dataType.Configuration)));
-
-            var folderNames = string.Empty;
-            var folderKeys = string.Empty;
-            if (dataType.Level != 1)
-            {
-                //get URL encoded folder names
-                IOrderedEnumerable folders = _dataTypeService.GetContainers(dataType)
-                    .OrderBy(x => x.Level);
-
-                folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
-                folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
-            }
-
-            if (string.IsNullOrWhiteSpace(folderNames) == false)
-            {
-                xml.Add(new XAttribute("Folders", folderNames));
-                xml.Add(new XAttribute("FolderKeys", folderKeys));
-            }
-
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// List of dictionary items to export
-        /// Optional boolean indicating whether or not to include children
-        ///  containing the xml representation of the IDictionaryItem objects
-        public XElement Serialize(IEnumerable dictionaryItem, bool includeChildren = true)
-        {
-            var xml = new XElement("DictionaryItems");
-            foreach (var item in dictionaryItem)
-            {
-                xml.Add(Serialize(item, includeChildren));
-            }
-            return xml;
-        }
-
-        /// 
-        /// Exports a single  item to xml as an 
-        /// 
-        /// Dictionary Item to export
-        /// Optional boolean indicating whether or not to include children
-        ///  containing the xml representation of the IDictionaryItem object
-        public XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren)
-        {
-            var xml = Serialize(dictionaryItem);
-
-            if (includeChildren)
-            {
-                var children = _localizationService.GetDictionaryItemChildren(dictionaryItem.Key);
-                if (children is not null)
-                {
-                    foreach (var child in children)
-                    {
-                        xml.Add(Serialize(child, true));
-                    }
-                }
-            }
-
-            return xml;
-        }
-
-        private XElement Serialize(IDictionaryItem dictionaryItem)
-        {
-            var xml = new XElement("DictionaryItem",
-                new XAttribute("Key", dictionaryItem.Key),
-                new XAttribute("Name", dictionaryItem.ItemKey));
-
-            foreach (IDictionaryTranslation translation in dictionaryItem.Translations!)
-            {
-                xml.Add(new XElement("Value",
-                    new XAttribute("LanguageId", translation.Language!.Id),
-                    new XAttribute("LanguageCultureAlias", translation.Language.IsoCode),
-                    new XCData(translation.Value!)));
-            }
-
-            return xml;
-        }
-
-        public XElement Serialize(IStylesheet stylesheet, bool includeProperties)
-        {
-            var xml = new XElement("Stylesheet",
-                new XElement("Name", stylesheet.Alias),
-                new XElement("FileName", stylesheet.Path),
-                new XElement("Content", new XCData(stylesheet.Content!)));
-
-            if (!includeProperties)
-            {
-                return xml;
-            }
-
-            var props = new XElement("Properties");
-            xml.Add(props);
-
-            if (stylesheet.Properties is not null)
-            {
-                foreach (var prop in stylesheet.Properties)
-                {
-                    props.Add(new XElement("Property",
-                        new XElement("Name", prop.Name),
-                        new XElement("Alias", prop.Alias),
-                        new XElement("Value", prop.Value)));
-                }
-            }
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// List of Languages to export
-        ///  containing the xml representation of the ILanguage objects
-        public XElement Serialize(IEnumerable languages)
-        {
-            var xml = new XElement("Languages");
-            foreach (var language in languages)
-            {
-                xml.Add(Serialize(language));
-            }
-            return xml;
-        }
-
-        public XElement Serialize(ILanguage language)
-        {
-            var xml = new XElement("Language",
-                new XAttribute("Id", language.Id),
-                new XAttribute("CultureAlias", language.IsoCode),
-                new XAttribute("FriendlyName", language.CultureName!));
-
-            return xml;
-        }
-
-        public XElement Serialize(ITemplate template)
-        {
-            var xml = new XElement("Template");
-            xml.Add(new XElement("Name", template.Name));
-            xml.Add(new XElement("Key", template.Key));
-            xml.Add(new XElement("Alias", template.Alias));
-            xml.Add(new XElement("Design", new XCData(template.Content!)));
-
-            if (template is Template concreteTemplate && concreteTemplate.MasterTemplateId != null)
-            {
-                if (concreteTemplate.MasterTemplateId.IsValueCreated &&
-                    concreteTemplate.MasterTemplateId.Value != default)
-                {
-                    xml.Add(new XElement("Master", concreteTemplate.MasterTemplateId.ToString()));
-                    xml.Add(new XElement("MasterAlias", concreteTemplate.MasterTemplateAlias));
-                }
-            }
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// 
-        /// 
-        public XElement Serialize(IEnumerable templates)
-        {
-            var xml = new XElement("Templates");
-            foreach (var item in templates)
-            {
-                xml.Add(Serialize(item));
-            }
-            return xml;
-        }
-
-
-        public XElement Serialize(IMediaType mediaType)
-        {
-            var info = new XElement("Info",
-                                    new XElement("Name", mediaType.Name),
-                                    new XElement("Alias", mediaType.Alias),
-                                    new XElement("Key", mediaType.Key),
-                                    new XElement("Icon", mediaType.Icon),
-                                    new XElement("Thumbnail", mediaType.Thumbnail),
-                                    new XElement("Description", mediaType.Description),
-                                    new XElement("AllowAtRoot", mediaType.AllowedAsRoot.ToString()));
-
-            var masterContentType = mediaType.CompositionAliases().FirstOrDefault();
-            if (masterContentType != null)
-            {
-                info.Add(new XElement("Master", masterContentType));
-            }
-
-            var structure = new XElement("Structure");
-            if (mediaType.AllowedContentTypes is not null)
-            {
-                foreach (var allowedType in mediaType.AllowedContentTypes)
-                {
-                    structure.Add(new XElement("MediaType", allowedType.Alias));
-                }
-            }
-
-            var genericProperties = new XElement("GenericProperties", SerializePropertyTypes(mediaType.PropertyTypes, mediaType.PropertyGroups)); // actually, all of them
-
-            var tabs = new XElement("Tabs", SerializePropertyGroups(mediaType.PropertyGroups)); // TODO Rename to PropertyGroups
-
-            var xml = new XElement("MediaType",
-                                   info,
-                                   structure,
-                                   genericProperties,
-                                   tabs);
-
-            return xml;
-        }
-
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// Macros to export
-        ///  containing the xml representation of the IMacro objects
-        public XElement Serialize(IEnumerable macros)
-        {
-            var xml = new XElement("Macros");
-            foreach (var item in macros)
-            {
-                xml.Add(Serialize(item));
-            }
-            return xml;
-        }
-
-        public XElement Serialize(IMacro macro)
-        {
-            var xml = new XElement("macro");
-            xml.Add(new XElement("name", macro.Name));
-            xml.Add(new XElement("key", macro.Key));
-            xml.Add(new XElement("alias", macro.Alias));
-            xml.Add(new XElement("macroSource", macro.MacroSource));
-            xml.Add(new XElement("useInEditor", macro.UseInEditor.ToString()));
-            xml.Add(new XElement("dontRender", macro.DontRender.ToString()));
-            xml.Add(new XElement("refreshRate", macro.CacheDuration.ToString(CultureInfo.InvariantCulture)));
-            xml.Add(new XElement("cacheByMember", macro.CacheByMember.ToString()));
-            xml.Add(new XElement("cacheByPage", macro.CacheByPage.ToString()));
-
-            var properties = new XElement("properties");
-            foreach (var property in macro.Properties)
-            {
-                properties.Add(new XElement("property",
-                    new XAttribute("key", property.Key),
-                    new XAttribute("name", property.Name!),
-                    new XAttribute("alias", property.Alias),
-                    new XAttribute("sortOrder", property.SortOrder),
-                    new XAttribute("propertyType", property.EditorAlias)));
-            }
-            xml.Add(properties);
-
-            return xml;
-        }
-
-        public XElement Serialize(IContentType contentType)
-        {
-            var info = new XElement("Info",
-                                    new XElement("Name", contentType.Name),
-                                    new XElement("Alias", contentType.Alias),
-                                    new XElement("Key", contentType.Key),
-                                    new XElement("Icon", contentType.Icon),
-                                    new XElement("Thumbnail", contentType.Thumbnail),
-                                    new XElement("Description", contentType.Description),
-                                    new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()),
-                                    new XElement("IsListView", contentType.IsContainer.ToString()),
-                                    new XElement("IsElement", contentType.IsElement.ToString()),
-                                    new XElement("Variations", contentType.Variations.ToString()));
-
-            var masterContentType = contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId);
-            if (masterContentType != null)
-            {
-                info.Add(new XElement("Master", masterContentType.Alias));
-            }
-
-            var compositionsElement = new XElement("Compositions");
-            var compositions = contentType.ContentTypeComposition;
-            foreach (var composition in compositions)
-            {
-                compositionsElement.Add(new XElement("Composition", composition.Alias));
-            }
-            info.Add(compositionsElement);
-
-            var allowedTemplates = new XElement("AllowedTemplates");
-            if (contentType.AllowedTemplates is not null)
-            {
-                foreach (var template in contentType.AllowedTemplates)
-                {
-                    allowedTemplates.Add(new XElement("Template", template.Alias));
-                }
-            }
-
-            info.Add(allowedTemplates);
-
-            if (contentType.DefaultTemplate != null && contentType.DefaultTemplate.Id != 0)
-            {
-                info.Add(new XElement("DefaultTemplate", contentType.DefaultTemplate.Alias));
-            }
-            else
-            {
-                info.Add(new XElement("DefaultTemplate", ""));
-            }
-
-            var structure = new XElement("Structure");
-            if (contentType.AllowedContentTypes is not null)
-            {
-                foreach (var allowedType in contentType.AllowedContentTypes)
-                {
-                    structure.Add(new XElement("DocumentType", allowedType.Alias));
-                }
-            }
-
-            var genericProperties = new XElement("GenericProperties", SerializePropertyTypes(contentType.PropertyTypes, contentType.PropertyGroups)); // actually, all of them
-
-            var tabs = new XElement("Tabs", SerializePropertyGroups(contentType.PropertyGroups)); // TODO Rename to PropertyGroups
-
-            var xml = new XElement("DocumentType",
-                info,
-                structure,
-                genericProperties,
-                tabs);
-
-            if (contentType is IContentTypeWithHistoryCleanup withCleanup && withCleanup.HistoryCleanup is not null)
-            {
-                xml.Add(SerializeCleanupPolicy(withCleanup.HistoryCleanup));
-            }
-
-            var folderNames = string.Empty;
-            var folderKeys = string.Empty;
-            //don't add folders if this is a child doc type
-            if (contentType.Level != 1 && masterContentType == null)
-            {
-                //get URL encoded folder names
-                IOrderedEnumerable folders = _contentTypeService.GetContainers(contentType)
-                    .OrderBy(x => x.Level);
-
-                folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
-                folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
-            }
-
-            if (string.IsNullOrWhiteSpace(folderNames) == false)
-            {
-                xml.Add(new XAttribute("Folders", folderNames));
-                xml.Add(new XAttribute("FolderKeys", folderKeys));
-            }
-
-
-            return xml;
-        }
-
-        private IEnumerable SerializePropertyTypes(IEnumerable propertyTypes, IEnumerable propertyGroups)
-        {
-            foreach (var propertyType in propertyTypes)
-            {
-                var definition = _dataTypeService.GetDataType(propertyType.DataTypeId);
-
-                var propertyGroup = propertyType.PropertyGroupId == null // true generic property
-                    ? null
-                    : propertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value);
-
-                XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
-                genericProperty.Add(new XElement("Variations", propertyType.Variations.ToString()));
-
-                yield return genericProperty;
-            }
-        }
-
-        private IEnumerable SerializePropertyGroups(IEnumerable propertyGroups)
-        {
-            foreach (var propertyGroup in propertyGroups)
-            {
-                yield return new XElement("Tab", // TODO Rename to PropertyGroup
-                    new XElement("Id", propertyGroup.Id),
-                    new XElement("Key", propertyGroup.Key),
-                    new XElement("Type", propertyGroup.Type.ToString()),
-                    new XElement("Caption", propertyGroup.Name), // TODO Rename to Name (same in PackageDataInstallation)
-                    new XElement("Alias", propertyGroup.Alias),
-                    new XElement("SortOrder", propertyGroup.SortOrder));
-            }
-        }
-
-        private XElement SerializePropertyType(IPropertyType propertyType, IDataType? definition, PropertyGroup? propertyGroup)
-            => new XElement("GenericProperty",
-                    new XElement("Name", propertyType.Name),
-                    new XElement("Alias", propertyType.Alias),
-                    new XElement("Key", propertyType.Key),
-                    new XElement("Type", propertyType.PropertyEditorAlias),
-                    definition is not null ? new XElement("Definition", definition.Key) : null,
-                    propertyGroup is not null ? new XElement("Tab", propertyGroup.Name, new XAttribute("Alias", propertyGroup.Alias)) : null, // TODO Replace with PropertyGroupAlias
-                    new XElement("SortOrder", propertyType.SortOrder),
-                    new XElement("Mandatory", propertyType.Mandatory.ToString()),
-                    new XElement("LabelOnTop", propertyType.LabelOnTop.ToString()),
-                    propertyType.MandatoryMessage != null ? new XElement("MandatoryMessage", propertyType.MandatoryMessage) : null,
-                    propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null,
-                    propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null,
-                    propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null);
-
-        private XElement SerializeCleanupPolicy(HistoryCleanup cleanupPolicy)
-        {
-            if (cleanupPolicy == null)
-            {
-                throw new ArgumentNullException(nameof(cleanupPolicy));
-            }
-
-            var element = new XElement("HistoryCleanupPolicy",
-                new XAttribute("preventCleanup", cleanupPolicy.PreventCleanup));
-
-            if (cleanupPolicy.KeepAllVersionsNewerThanDays.HasValue)
-            {
-                element.Add(new XAttribute("keepAllVersionsNewerThanDays", cleanupPolicy.KeepAllVersionsNewerThanDays));
-            }
-
-            if (cleanupPolicy.KeepLatestVersionPerDayForDays.HasValue)
-            {
-                element.Add(new XAttribute("keepLatestVersionPerDayForDays", cleanupPolicy.KeepLatestVersionPerDayForDays));
-            }
-
-            return element;
-        }
-
-        // exports an IContentBase (IContent, IMedia or IMember) as an XElement.
-        private XElement SerializeContentBase(IContentBase contentBase, string? urlValue, string nodeName, bool published)
-        {
-            var xml = new XElement(nodeName,
-                new XAttribute("id", contentBase.Id.ToInvariantString()),
-                new XAttribute("key", contentBase.Key),
-                new XAttribute("parentID", (contentBase.Level > 1 ? contentBase.ParentId : -1).ToInvariantString()),
-                new XAttribute("level", contentBase.Level),
-                new XAttribute("creatorID", contentBase.CreatorId.ToInvariantString()),
-                new XAttribute("sortOrder", contentBase.SortOrder),
-                new XAttribute("createDate", contentBase.CreateDate.ToString("s")),
-                new XAttribute("updateDate", contentBase.UpdateDate.ToString("s")),
-                new XAttribute("nodeName", contentBase.Name!),
-                new XAttribute("urlName", urlValue!),
-                new XAttribute("path", contentBase.Path),
-                new XAttribute("isDoc", ""));
-
-
-            // Add culture specific node names
-            foreach (var culture in contentBase.AvailableCultures)
-            {
-                xml.Add(new XAttribute("nodeName-" + culture, contentBase.GetCultureName(culture)!));
-            }
-
-            foreach (var property in contentBase.Properties)
-                xml.Add(SerializeProperty(property, published));
-
-            return xml;
-        }
-
-        // exports a property as XElements.
-        private IEnumerable SerializeProperty(IProperty property, bool published)
-        {
-            var propertyType = property.PropertyType;
-
-            // get the property editor for this property and let it convert it to the xml structure
-            var propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias];
-            return propertyEditor == null
-                ? Array.Empty()
-                : propertyEditor.GetValueEditor().ConvertDbToXml(property, published);
-        }
-
-        // exports an IContent item descendants.
-        private void SerializeChildren(IEnumerable children, XElement xml, bool published)
-        {
-            foreach (var child in children)
-            {
-                // add the child xml
-                var childXml = Serialize(child, published);
-                xml.Add(childXml);
-
-                const int pageSize = 500;
-                var page = 0;
-                var total = long.MaxValue;
-                while(page * pageSize < total)
-                {
-                    var grandChildren = _contentService.GetPagedChildren(child.Id, page++, pageSize, out total);
-                    // recurse
-                    SerializeChildren(grandChildren, childXml, published);
+                    xml.Add(Serialize(child, true));
                 }
             }
         }
 
-        // exports an IMedia item descendants.
-        private void SerializeChildren(IEnumerable children, XElement xml, Action? onMediaItemSerialized)
-        {
-            foreach (var child in children)
-            {
-                // add the child xml
-                var childXml = Serialize(child, onMediaItemSerialized: onMediaItemSerialized);
-                xml.Add(childXml);
+        return xml;
+    }
 
-                const int pageSize = 500;
-                var page = 0;
-                var total = long.MaxValue;
-                while (page * pageSize < total)
-                {
-                    var grandChildren = _mediaService.GetPagedChildren(child.Id, page++, pageSize, out total);
-                    // recurse
-                    SerializeChildren(grandChildren, childXml, onMediaItemSerialized);
-                }
+    public XElement Serialize(IStylesheet stylesheet, bool includeProperties)
+    {
+        var xml = new XElement(
+            "Stylesheet",
+            new XElement("Name", stylesheet.Alias),
+            new XElement("FileName", stylesheet.Path),
+            new XElement("Content", new XCData(stylesheet.Content!)));
+
+        if (!includeProperties)
+        {
+            return xml;
+        }
+
+        var props = new XElement("Properties");
+        xml.Add(props);
+
+        if (stylesheet.Properties is not null)
+        {
+            foreach (IStylesheetProperty prop in stylesheet.Properties)
+            {
+                props.Add(new XElement(
+                    "Property",
+                    new XElement("Name", prop.Name),
+                    new XElement("Alias", prop.Alias),
+                    new XElement("Value", prop.Value)));
+            }
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// List of Languages to export
+    ///  containing the xml representation of the ILanguage objects
+    public XElement Serialize(IEnumerable languages)
+    {
+        var xml = new XElement("Languages");
+        foreach (ILanguage language in languages)
+        {
+            xml.Add(Serialize(language));
+        }
+
+        return xml;
+    }
+
+    public XElement Serialize(ILanguage language)
+    {
+        var xml = new XElement(
+            "Language",
+            new XAttribute("Id", language.Id),
+            new XAttribute("CultureAlias", language.IsoCode),
+            new XAttribute("FriendlyName", language.CultureName));
+
+        return xml;
+    }
+
+    public XElement Serialize(ITemplate template)
+    {
+        var xml = new XElement("Template");
+        xml.Add(new XElement("Name", template.Name));
+        xml.Add(new XElement("Key", template.Key));
+        xml.Add(new XElement("Alias", template.Alias));
+        xml.Add(new XElement("Design", new XCData(template.Content!)));
+
+        if (template is Template concreteTemplate && concreteTemplate.MasterTemplateId != null)
+        {
+            if (concreteTemplate.MasterTemplateId.IsValueCreated &&
+                concreteTemplate.MasterTemplateId.Value != default)
+            {
+                xml.Add(new XElement("Master", concreteTemplate.MasterTemplateId.ToString()));
+                xml.Add(new XElement("MasterAlias", concreteTemplate.MasterTemplateAlias));
+            }
+        }
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// 
+    /// 
+    public XElement Serialize(IEnumerable templates)
+    {
+        var xml = new XElement("Templates");
+        foreach (ITemplate item in templates)
+        {
+            xml.Add(Serialize(item));
+        }
+
+        return xml;
+    }
+
+    public XElement Serialize(IMediaType mediaType)
+    {
+        var info = new XElement(
+            "Info",
+            new XElement("Name", mediaType.Name),
+            new XElement("Alias", mediaType.Alias),
+            new XElement("Key", mediaType.Key),
+            new XElement("Icon", mediaType.Icon),
+            new XElement("Thumbnail", mediaType.Thumbnail),
+            new XElement("Description", mediaType.Description),
+            new XElement("AllowAtRoot", mediaType.AllowedAsRoot.ToString()));
+
+        var masterContentType = mediaType.CompositionAliases().FirstOrDefault();
+        if (masterContentType != null)
+        {
+            info.Add(new XElement("Master", masterContentType));
+        }
+
+        var structure = new XElement("Structure");
+        if (mediaType.AllowedContentTypes is not null)
+        {
+            foreach (ContentTypeSort allowedType in mediaType.AllowedContentTypes)
+            {
+                structure.Add(new XElement("MediaType", allowedType.Alias));
+            }
+        }
+
+        var genericProperties = new XElement(
+            "GenericProperties",
+            SerializePropertyTypes(mediaType.PropertyTypes, mediaType.PropertyGroups)); // actually, all of them
+
+        var tabs = new XElement(
+            "Tabs",
+            SerializePropertyGroups(mediaType.PropertyGroups)); // TODO Rename to PropertyGroups
+
+        var xml = new XElement(
+            "MediaType",
+            info,
+            structure,
+            genericProperties,
+            tabs);
+
+        return xml;
+    }
+
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// Macros to export
+    ///  containing the xml representation of the IMacro objects
+    public XElement Serialize(IEnumerable macros)
+    {
+        var xml = new XElement("Macros");
+        foreach (IMacro item in macros)
+        {
+            xml.Add(Serialize(item));
+        }
+
+        return xml;
+    }
+
+    public XElement Serialize(IMacro macro)
+    {
+        var xml = new XElement("macro");
+        xml.Add(new XElement("name", macro.Name));
+        xml.Add(new XElement("key", macro.Key));
+        xml.Add(new XElement("alias", macro.Alias));
+        xml.Add(new XElement("macroSource", macro.MacroSource));
+        xml.Add(new XElement("useInEditor", macro.UseInEditor.ToString()));
+        xml.Add(new XElement("dontRender", macro.DontRender.ToString()));
+        xml.Add(new XElement("refreshRate", macro.CacheDuration.ToString(CultureInfo.InvariantCulture)));
+        xml.Add(new XElement("cacheByMember", macro.CacheByMember.ToString()));
+        xml.Add(new XElement("cacheByPage", macro.CacheByPage.ToString()));
+
+        var properties = new XElement("properties");
+        foreach (IMacroProperty property in macro.Properties)
+        {
+            properties.Add(new XElement(
+                "property",
+                new XAttribute("key", property.Key),
+                new XAttribute("name", property.Name!),
+                new XAttribute("alias", property.Alias),
+                new XAttribute("sortOrder", property.SortOrder),
+                new XAttribute("propertyType", property.EditorAlias)));
+        }
+
+        xml.Add(properties);
+
+        return xml;
+    }
+
+    public XElement Serialize(IContentType contentType)
+    {
+        var info = new XElement(
+            "Info",
+            new XElement("Name", contentType.Name),
+            new XElement("Alias", contentType.Alias),
+            new XElement("Key", contentType.Key),
+            new XElement("Icon", contentType.Icon),
+            new XElement("Thumbnail", contentType.Thumbnail),
+            new XElement("Description", contentType.Description),
+            new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()),
+            new XElement("IsListView", contentType.IsContainer.ToString()),
+            new XElement("IsElement", contentType.IsElement.ToString()),
+            new XElement("Variations", contentType.Variations.ToString()));
+
+        IContentTypeComposition? masterContentType =
+            contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId);
+        if (masterContentType != null)
+        {
+            info.Add(new XElement("Master", masterContentType.Alias));
+        }
+
+        var compositionsElement = new XElement("Compositions");
+        IEnumerable compositions = contentType.ContentTypeComposition;
+        foreach (IContentTypeComposition composition in compositions)
+        {
+            compositionsElement.Add(new XElement("Composition", composition.Alias));
+        }
+
+        info.Add(compositionsElement);
+
+        var allowedTemplates = new XElement("AllowedTemplates");
+        if (contentType.AllowedTemplates is not null)
+        {
+            foreach (ITemplate template in contentType.AllowedTemplates)
+            {
+                allowedTemplates.Add(new XElement("Template", template.Alias));
+            }
+        }
+
+        info.Add(allowedTemplates);
+
+        if (contentType.DefaultTemplate != null && contentType.DefaultTemplate.Id != 0)
+        {
+            info.Add(new XElement("DefaultTemplate", contentType.DefaultTemplate.Alias));
+        }
+        else
+        {
+            info.Add(new XElement("DefaultTemplate", string.Empty));
+        }
+
+        var structure = new XElement("Structure");
+        if (contentType.AllowedContentTypes is not null)
+        {
+            foreach (ContentTypeSort allowedType in contentType.AllowedContentTypes)
+            {
+                structure.Add(new XElement("DocumentType", allowedType.Alias));
+            }
+        }
+
+        var genericProperties = new XElement(
+            "GenericProperties",
+            SerializePropertyTypes(contentType.PropertyTypes, contentType.PropertyGroups)); // actually, all of them
+
+        var tabs = new XElement(
+            "Tabs",
+            SerializePropertyGroups(contentType.PropertyGroups)); // TODO Rename to PropertyGroups
+
+        var xml = new XElement(
+            "DocumentType",
+            info,
+            structure,
+            genericProperties,
+            tabs);
+
+        if (contentType is IContentTypeWithHistoryCleanup withCleanup && withCleanup.HistoryCleanup is not null)
+        {
+            xml.Add(SerializeCleanupPolicy(withCleanup.HistoryCleanup));
+        }
+
+        var folderNames = string.Empty;
+        var folderKeys = string.Empty;
+
+        // don't add folders if this is a child doc type
+        if (contentType.Level != 1 && masterContentType == null)
+        {
+            // get URL encoded folder names
+            IOrderedEnumerable folders = _contentTypeService.GetContainers(contentType)
+                .OrderBy(x => x.Level);
+
+            folderNames = string.Join("/", folders.Select(x => WebUtility.UrlEncode(x.Name)).ToArray());
+            folderKeys = string.Join("/", folders.Select(x => x.Key).ToArray());
+        }
+
+        if (string.IsNullOrWhiteSpace(folderNames) == false)
+        {
+            xml.Add(new XAttribute("Folders", folderNames));
+            xml.Add(new XAttribute("FolderKeys", folderKeys));
+        }
+
+        return xml;
+    }
+
+    private XElement Serialize(IDictionaryItem dictionaryItem)
+    {
+        var xml = new XElement(
+            "DictionaryItem",
+            new XAttribute("Key", dictionaryItem.Key),
+            new XAttribute("Name", dictionaryItem.ItemKey));
+
+        foreach (IDictionaryTranslation translation in dictionaryItem.Translations)
+        {
+            xml.Add(new XElement(
+                "Value",
+                new XAttribute("LanguageId", translation.Language!.Id),
+                new XAttribute("LanguageCultureAlias", translation.Language.IsoCode),
+                new XCData(translation.Value)));
+        }
+
+        return xml;
+    }
+
+    private IEnumerable SerializePropertyTypes(
+        IEnumerable propertyTypes,
+        IEnumerable propertyGroups)
+    {
+        foreach (IPropertyType propertyType in propertyTypes)
+        {
+            IDataType? definition = _dataTypeService.GetDataType(propertyType.DataTypeId);
+
+            PropertyGroup? propertyGroup = propertyType.PropertyGroupId == null // true generic property
+                ? null
+                : propertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value);
+
+            XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
+            genericProperty.Add(new XElement("Variations", propertyType.Variations.ToString()));
+
+            yield return genericProperty;
+        }
+    }
+
+    private IEnumerable SerializePropertyGroups(IEnumerable propertyGroups)
+    {
+        foreach (PropertyGroup propertyGroup in propertyGroups)
+        {
+            yield return new XElement(
+                "Tab", // TODO Rename to PropertyGroup
+                new XElement("Id", propertyGroup.Id),
+                new XElement("Key", propertyGroup.Key),
+                new XElement("Type", propertyGroup.Type.ToString()),
+                new XElement("Caption", propertyGroup.Name), // TODO Rename to Name (same in PackageDataInstallation)
+                new XElement("Alias", propertyGroup.Alias),
+                new XElement("SortOrder", propertyGroup.SortOrder));
+        }
+    }
+
+    private XElement SerializePropertyType(IPropertyType propertyType, IDataType? definition, PropertyGroup? propertyGroup)
+        => new(
+            "GenericProperty",
+            new XElement("Name", propertyType.Name),
+            new XElement("Alias", propertyType.Alias),
+            new XElement("Key", propertyType.Key),
+            new XElement("Type", propertyType.PropertyEditorAlias),
+            definition is not null ? new XElement("Definition", definition.Key) : null,
+            propertyGroup is not null ? new XElement("Tab", propertyGroup.Name, new XAttribute("Alias", propertyGroup.Alias)) : null, // TODO Replace with PropertyGroupAlias
+            new XElement("SortOrder", propertyType.SortOrder),
+            new XElement("Mandatory", propertyType.Mandatory.ToString()),
+            new XElement("LabelOnTop", propertyType.LabelOnTop.ToString()),
+            propertyType.MandatoryMessage != null ? new XElement("MandatoryMessage", propertyType.MandatoryMessage) : null,
+            propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null,
+            propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null,
+            propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null);
+
+    private XElement SerializeCleanupPolicy(HistoryCleanup cleanupPolicy)
+    {
+        if (cleanupPolicy == null)
+        {
+            throw new ArgumentNullException(nameof(cleanupPolicy));
+        }
+
+        var element = new XElement(
+            "HistoryCleanupPolicy",
+            new XAttribute("preventCleanup", cleanupPolicy.PreventCleanup));
+
+        if (cleanupPolicy.KeepAllVersionsNewerThanDays.HasValue)
+        {
+            element.Add(new XAttribute("keepAllVersionsNewerThanDays", cleanupPolicy.KeepAllVersionsNewerThanDays));
+        }
+
+        if (cleanupPolicy.KeepLatestVersionPerDayForDays.HasValue)
+        {
+            element.Add(new XAttribute("keepLatestVersionPerDayForDays", cleanupPolicy.KeepLatestVersionPerDayForDays));
+        }
+
+        return element;
+    }
+
+    // exports an IContentBase (IContent, IMedia or IMember) as an XElement.
+    private XElement SerializeContentBase(IContentBase contentBase, string? urlValue, string nodeName, bool published)
+    {
+        var xml = new XElement(
+            nodeName,
+            new XAttribute("id", contentBase.Id.ToInvariantString()),
+            new XAttribute("key", contentBase.Key),
+            new XAttribute("parentID", (contentBase.Level > 1 ? contentBase.ParentId : -1).ToInvariantString()),
+            new XAttribute("level", contentBase.Level),
+            new XAttribute("creatorID", contentBase.CreatorId.ToInvariantString()),
+            new XAttribute("sortOrder", contentBase.SortOrder),
+            new XAttribute("createDate", contentBase.CreateDate.ToString("s")),
+            new XAttribute("updateDate", contentBase.UpdateDate.ToString("s")),
+            new XAttribute("nodeName", contentBase.Name!),
+            new XAttribute("urlName", urlValue!),
+            new XAttribute("path", contentBase.Path),
+            new XAttribute("isDoc", string.Empty));
+
+        // Add culture specific node names
+        foreach (var culture in contentBase.AvailableCultures)
+        {
+            xml.Add(new XAttribute("nodeName-" + culture, contentBase.GetCultureName(culture)!));
+        }
+
+        foreach (IProperty property in contentBase.Properties)
+        {
+            xml.Add(SerializeProperty(property, published));
+        }
+
+        return xml;
+    }
+
+    // exports a property as XElements.
+    private IEnumerable SerializeProperty(IProperty property, bool published)
+    {
+        IPropertyType propertyType = property.PropertyType;
+
+        // get the property editor for this property and let it convert it to the xml structure
+        IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias];
+        return propertyEditor == null
+            ? Array.Empty()
+            : propertyEditor.GetValueEditor().ConvertDbToXml(property, published);
+    }
+
+    // exports an IContent item descendants.
+    private void SerializeChildren(IEnumerable children, XElement xml, bool published)
+    {
+        foreach (IContent child in children)
+        {
+            // add the child xml
+            XElement childXml = Serialize(child, published);
+            xml.Add(childXml);
+
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
+            {
+                IEnumerable grandChildren =
+                    _contentService.GetPagedChildren(child.Id, page++, pageSize, out total);
+
+                // recurse
+                SerializeChildren(grandChildren, childXml, published);
+            }
+        }
+    }
+
+    // exports an IMedia item descendants.
+    private void SerializeChildren(IEnumerable children, XElement xml, Action? onMediaItemSerialized)
+    {
+        foreach (IMedia child in children)
+        {
+            // add the child xml
+            XElement childXml = Serialize(child, onMediaItemSerialized: onMediaItemSerialized);
+            xml.Add(childXml);
+
+            const int pageSize = 500;
+            var page = 0;
+            var total = long.MaxValue;
+            while (page * pageSize < total)
+            {
+                IEnumerable grandChildren =
+                    _mediaService.GetPagedChildren(child.Id, page++, pageSize, out total);
+
+                // recurse
+                SerializeChildren(grandChildren, childXml, onMediaItemSerialized);
             }
         }
     }
diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs
index d934e89528..677108dbcd 100644
--- a/src/Umbraco.Core/Services/ExternalLoginService.cs
+++ b/src/Umbraco.Core/Services/ExternalLoginService.cs
@@ -1,7 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
@@ -11,110 +7,108 @@ using Umbraco.Cms.Core.Security;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class ExternalLoginService : RepositoryService, IExternalLoginService, IExternalLoginWithKeyService
 {
-    public class ExternalLoginService : RepositoryService, IExternalLoginService, IExternalLoginWithKeyService
+    private readonly IExternalLoginWithKeyRepository _externalLoginRepository;
+
+    public ExternalLoginService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IExternalLoginWithKeyRepository externalLoginRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _externalLoginRepository = externalLoginRepository;
+
+    [Obsolete("Use ctor injecting IExternalLoginWithKeyRepository")]
+    public ExternalLoginService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IExternalLoginRepository externalLoginRepository)
+        : this(provider, loggerFactory, eventMessagesFactory, StaticServiceProvider.Instance.GetRequiredService())
     {
-        private readonly IExternalLoginWithKeyRepository _externalLoginRepository;
+    }
 
-        public ExternalLoginService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IExternalLoginWithKeyRepository externalLoginRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    /// 
+    [Obsolete("Use overload that takes a user/member key (Guid).")]
+    public IEnumerable GetExternalLogins(int userId)
+        => GetExternalLogins(userId.ToGuid());
+
+    /// 
+    [Obsolete("Use overload that takes a user/member key (Guid).")]
+    public IEnumerable GetExternalLoginTokens(int userId) =>
+        GetExternalLoginTokens(userId.ToGuid());
+
+    /// 
+    [Obsolete("Use overload that takes a user/member key (Guid).")]
+    public void Save(int userId, IEnumerable logins)
+        => Save(userId.ToGuid(), logins);
+
+    /// 
+    [Obsolete("Use overload that takes a user/member key (Guid).")]
+    public void Save(int userId, IEnumerable tokens)
+        => Save(userId.ToGuid(), tokens);
+
+    /// 
+    [Obsolete("Use overload that takes a user/member key (Guid).")]
+    public void DeleteUserLogins(int userId)
+        => DeleteUserLogins(userId.ToGuid());
+
+    public IEnumerable Find(string loginProvider, string providerKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _externalLoginRepository = externalLoginRepository;
-        }
-
-        [Obsolete("Use ctor injecting IExternalLoginWithKeyRepository")]
-        public ExternalLoginService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IExternalLoginRepository externalLoginRepository)
-            : this(provider, loggerFactory, eventMessagesFactory, StaticServiceProvider.Instance.GetRequiredService())
-        {
-        }
-
-        /// 
-        [Obsolete("Use overload that takes a user/member key (Guid).")]
-        public IEnumerable GetExternalLogins(int userId)
-            => GetExternalLogins(userId.ToGuid());
-
-        /// 
-        [Obsolete("Use overload that takes a user/member key (Guid).")]
-        public IEnumerable GetExternalLoginTokens(int userId) =>
-            GetExternalLoginTokens(userId.ToGuid());
-
-        /// 
-        [Obsolete("Use overload that takes a user/member key (Guid).")]
-        public void Save(int userId, IEnumerable logins)
-            => Save(userId.ToGuid(), logins);
-
-        /// 
-        [Obsolete("Use overload that takes a user/member key (Guid).")]
-        public void Save(int userId, IEnumerable tokens)
-            => Save(userId.ToGuid(), tokens);
-
-        /// 
-        [Obsolete("Use overload that takes a user/member key (Guid).")]
-        public void DeleteUserLogins(int userId)
-            => DeleteUserLogins(userId.ToGuid());
-
-        /// 
-        public IEnumerable GetExternalLogins(Guid userOrMemberKey)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
-                    .ToList();
-            }
-        }
-
-        /// 
-        public IEnumerable GetExternalLoginTokens(Guid userOrMemberKey)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
-                    .ToList();
-            }
-        }
-
-        /// 
-        public IEnumerable Find(string loginProvider, string providerKey)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _externalLoginRepository.Get(Query()
+            return _externalLoginRepository.Get(Query()
                     .Where(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider))
-                    .ToList();
-            }
+                .ToList();
         }
+    }
 
-        /// 
-        public void Save(Guid userOrMemberKey, IEnumerable logins)
+    /// 
+    public IEnumerable GetExternalLogins(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.Save(userOrMemberKey, logins);
-                scope.Complete();
-            }
+            return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
+                .ToList();
         }
+    }
 
-        /// 
-        public void Save(Guid userOrMemberKey, IEnumerable tokens)
+    /// 
+    public IEnumerable GetExternalLoginTokens(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.Save(userOrMemberKey, tokens);
-                scope.Complete();
-            }
+            return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey))
+                .ToList();
         }
+    }
 
-        /// 
-        public void DeleteUserLogins(Guid userOrMemberKey)
+    /// 
+    public void Save(Guid userOrMemberKey, IEnumerable logins)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                _externalLoginRepository.DeleteUserLogins(userOrMemberKey);
-                scope.Complete();
-            }
+            _externalLoginRepository.Save(userOrMemberKey, logins);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void Save(Guid userOrMemberKey, IEnumerable tokens)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _externalLoginRepository.Save(userOrMemberKey, tokens);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteUserLogins(Guid userOrMemberKey)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _externalLoginRepository.DeleteUserLogins(userOrMemberKey);
+            scope.Complete();
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs
index d692765620..758df3d102 100644
--- a/src/Umbraco.Core/Services/FileService.cs
+++ b/src/Umbraco.Core/Services/FileService.cs
@@ -1,7 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
 using System.Text.RegularExpressions;
 using Microsoft.Extensions.FileProviders;
 using Microsoft.Extensions.Logging;
@@ -16,1002 +12,1038 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Strings;
 using Umbraco.Extensions;
+using File = System.IO.File;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents the File Service, which is an easy access to operations involving  objects like
+///     Scripts, Stylesheets and Templates
+/// 
+public class FileService : RepositoryService, IFileService
 {
-    /// 
-    /// Represents the File Service, which is an easy access to operations involving  objects like Scripts, Stylesheets and Templates
-    /// 
-    public class FileService : RepositoryService, IFileService
+    private const string PartialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage";
+    private const string PartialViewMacroHeader = "@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage";
+    private readonly IAuditRepository _auditRepository;
+    private readonly GlobalSettings _globalSettings;
+    private readonly IHostingEnvironment _hostingEnvironment;
+    private readonly IPartialViewMacroRepository _partialViewMacroRepository;
+    private readonly IPartialViewRepository _partialViewRepository;
+    private readonly IScriptRepository _scriptRepository;
+    private readonly IShortStringHelper _shortStringHelper;
+    private readonly IStylesheetRepository _stylesheetRepository;
+    private readonly ITemplateRepository _templateRepository;
+
+    public FileService(
+        ICoreScopeProvider uowProvider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IStylesheetRepository stylesheetRepository,
+        IScriptRepository scriptRepository,
+        ITemplateRepository templateRepository,
+        IPartialViewRepository partialViewRepository,
+        IPartialViewMacroRepository partialViewMacroRepository,
+        IAuditRepository auditRepository,
+        IShortStringHelper shortStringHelper,
+        IOptions globalSettings,
+        IHostingEnvironment hostingEnvironment)
+        : base(uowProvider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IStylesheetRepository _stylesheetRepository;
-        private readonly IScriptRepository _scriptRepository;
-        private readonly ITemplateRepository _templateRepository;
-        private readonly IPartialViewRepository _partialViewRepository;
-        private readonly IPartialViewMacroRepository _partialViewMacroRepository;
-        private readonly IAuditRepository _auditRepository;
-        private readonly IShortStringHelper _shortStringHelper;
-        private readonly GlobalSettings _globalSettings;
-        private readonly IHostingEnvironment _hostingEnvironment;
+        _stylesheetRepository = stylesheetRepository;
+        _scriptRepository = scriptRepository;
+        _templateRepository = templateRepository;
+        _partialViewRepository = partialViewRepository;
+        _partialViewMacroRepository = partialViewMacroRepository;
+        _auditRepository = auditRepository;
+        _shortStringHelper = shortStringHelper;
+        _globalSettings = globalSettings.Value;
+        _hostingEnvironment = hostingEnvironment;
+    }
 
-        private const string PartialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage";
-        private const string PartialViewMacroHeader = "@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage";
+    #region Stylesheets
 
-        public FileService(ICoreScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IStylesheetRepository stylesheetRepository, IScriptRepository scriptRepository, ITemplateRepository templateRepository,
-            IPartialViewRepository partialViewRepository, IPartialViewMacroRepository partialViewMacroRepository,
-            IAuditRepository auditRepository, IShortStringHelper shortStringHelper, IOptions globalSettings, IHostingEnvironment hostingEnvironment)
-            : base(uowProvider, loggerFactory, eventMessagesFactory)
+    /// 
+    public IEnumerable GetStylesheets(params string[] paths)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _stylesheetRepository = stylesheetRepository;
-            _scriptRepository = scriptRepository;
-            _templateRepository = templateRepository;
-            _partialViewRepository = partialViewRepository;
-            _partialViewMacroRepository = partialViewMacroRepository;
-            _auditRepository = auditRepository;
-            _shortStringHelper = shortStringHelper;
-            _globalSettings = globalSettings.Value;
-            _hostingEnvironment = hostingEnvironment;
+            return _stylesheetRepository.GetMany(paths);
+        }
+    }
+
+    private void Audit(AuditType type, int userId, int objectId, string? entityType) =>
+        _auditRepository.Save(new AuditItem(objectId, type, userId, entityType));
+
+    /// 
+    public IStylesheet? GetStylesheet(string? path)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _stylesheetRepository.Get(path);
+        }
+    }
+
+    /// 
+    public void SaveStylesheet(IStylesheet? stylesheet, int? userId = null)
+    {
+        if (stylesheet is null)
+        {
+            return;
         }
 
-        #region Stylesheets
-
-        /// 
-        public IEnumerable GetStylesheets(params string[] paths)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetMany(paths);
-            }
-        }
-
-        /// 
-        public IStylesheet? GetStylesheet(string? path)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.Get(path);
-            }
-        }
-
-        /// 
-        public void SaveStylesheet(IStylesheet? stylesheet, int? userId = null)
-        {
-            if (stylesheet is null)
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new StylesheetSavingNotification(stylesheet, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
+                scope.Complete();
                 return;
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            userId ??= Constants.Security.SuperUserId;
+            _stylesheetRepository.Save(stylesheet);
+            scope.Notifications.Publish(
+                new StylesheetSavedNotification(stylesheet, eventMessages).WithStateFrom(savingNotification));
+            Audit(AuditType.Save, userId.Value, -1, "Stylesheet");
 
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteStylesheet(string path, int? userId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IStylesheet? stylesheet = _stylesheetRepository.Get(path);
+            if (stylesheet == null)
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new StylesheetSavingNotification(stylesheet, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                _stylesheetRepository.Save(stylesheet);
-                scope.Notifications.Publish(new StylesheetSavedNotification(stylesheet, eventMessages).WithStateFrom(savingNotification));
-                Audit(AuditType.Save, userId.Value, -1, "Stylesheet");
-
                 scope.Complete();
-            }
-        }
-
-        /// 
-        public void DeleteStylesheet(string path, int? userId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                IStylesheet? stylesheet = _stylesheetRepository.Get(path);
-                if (stylesheet == null)
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new StylesheetDeletingNotification(stylesheet, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return; // causes rollback
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                _stylesheetRepository.Delete(stylesheet);
-
-                scope.Notifications.Publish(new StylesheetDeletedNotification(stylesheet, eventMessages).WithStateFrom(deletingNotification));
-                Audit(AuditType.Delete, userId.Value, -1, "Stylesheet");
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void CreateStyleSheetFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void DeleteStyleSheetFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public Stream GetStylesheetFileContentStream(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetStylesheetFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _stylesheetRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetStylesheetFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _stylesheetRepository.GetFileSize(filepath);
-            }
-        }
-
-        #endregion
-
-        #region Scripts
-
-        /// 
-        public IEnumerable GetScripts(params string[] names)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetMany(names);
-            }
-        }
-
-        /// 
-        public IScript? GetScript(string? name)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.Get(name);
-            }
-        }
-
-        /// 
-        public void SaveScript(IScript? script, int? userId)
-        {
-            if (userId is null)
-            {
-                userId = Constants.Security.SuperUserId;
-            }
-            if (script is null)
-            {
                 return;
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new ScriptSavingNotification(script, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _scriptRepository.Save(script);
-                scope.Notifications.Publish(new ScriptSavedNotification(script, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, userId.Value, -1, "Script");
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void DeleteScript(string path, int? userId = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                IScript? script = _scriptRepository.Get(path);
-                if (script == null)
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new ScriptDeletingNotification(script, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                _scriptRepository.Delete(script);
-                scope.Notifications.Publish(new ScriptDeletedNotification(script, eventMessages).WithStateFrom(deletingNotification));
-
-                Audit(AuditType.Delete, userId.Value, -1, "Script");
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void CreateScriptFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _scriptRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void DeleteScriptFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _scriptRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public Stream GetScriptFileContentStream(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetScriptFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _scriptRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetScriptFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _scriptRepository.GetFileSize(filepath);
-            }
-        }
-
-        #endregion
-
-        #region Templates
-
-        /// 
-        /// Creates a template for a content type
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// The template created
-        /// 
-        public Attempt?> CreateTemplateForContentType(string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId)
-        {
-            var template = new Template(_shortStringHelper, contentTypeName,
-                //NOTE: We are NOT passing in the content type alias here, we want to use it's name since we don't
-                // want to save template file names as camelCase, the Template ctor will clean the alias as
-                // `alias.ToCleanString(CleanStringType.UnderscoreAlias)` which has been the default.
-                // This fixes: http://issues.umbraco.org/issue/U4-7953
-                contentTypeName);
-
             EventMessages eventMessages = EventMessagesFactory.Get();
-
-            if (contentTypeAlias != null && contentTypeAlias.Length > 255)
+            var deletingNotification = new StylesheetDeletingNotification(stylesheet, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-            }
-
-            // check that the template hasn't been created on disk before creating the content type
-            // if it exists, set the new template content to the existing file content
-            string? content = GetViewContent(contentTypeAlias);
-            if (content != null)
-            {
-                template.Content = content;
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingEvent = new TemplateSavingNotification(template, eventMessages, true, contentTypeAlias!);
-                if (scope.Notifications.PublishCancelable(savingEvent))
-                {
-                    scope.Complete();
-                    return OperationResult.Attempt.Fail(OperationResultType.FailedCancelledByEvent, eventMessages, template);
-                }
-
-                _templateRepository.Save(template);
-                scope.Notifications.Publish(new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingEvent));
-
-                Audit(AuditType.Save, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template));
                 scope.Complete();
+                return; // causes rollback
             }
 
-            return OperationResult.Attempt.Succeed(OperationResultType.Success, eventMessages, template);
+            userId ??= Constants.Security.SuperUserId;
+            _stylesheetRepository.Delete(stylesheet);
+
+            scope.Notifications.Publish(
+                new StylesheetDeletedNotification(stylesheet, eventMessages).WithStateFrom(deletingNotification));
+            Audit(AuditType.Delete, userId.Value, -1, "Stylesheet");
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void CreateStyleSheetFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _stylesheetRepository.AddFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteStyleSheetFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _stylesheetRepository.DeleteFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public Stream GetStylesheetFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _stylesheetRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    /// 
+    public void SetStylesheetFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _stylesheetRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public long GetStylesheetFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _stylesheetRepository.GetFileSize(filepath);
+        }
+    }
+
+    #endregion
+
+    #region Scripts
+
+    /// 
+    public IEnumerable GetScripts(params string[] names)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _scriptRepository.GetMany(names);
+        }
+    }
+
+    /// 
+    public IScript? GetScript(string? name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _scriptRepository.Get(name);
+        }
+    }
+
+    /// 
+    public void SaveScript(IScript? script, int? userId)
+    {
+        if (userId is null)
+        {
+            userId = Constants.Security.SuperUserId;
         }
 
-        /// 
-        /// Create a new template, setting the content if a view exists in the filesystem
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public ITemplate CreateTemplateWithIdentity(string? name, string? alias, string? content, ITemplate? masterTemplate = null, int userId = Constants.Security.SuperUserId)
+        if (script is null)
         {
-            if (name == null)
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name));
-            }
-
-            if (name.Length > 255)
-            {
-                throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length.");
-            }
-
-            // file might already be on disk, if so grab the content to avoid overwriting
-            var template = new Template(_shortStringHelper, name, alias)
-            {
-                Content = GetViewContent(alias) ?? content
-            };
-
-            if (masterTemplate != null)
-            {
-                template.SetMasterTemplate(masterTemplate);
-            }
-
-            SaveTemplate(template, userId);
-
-            return template;
+            return;
         }
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetTemplates(params string[] aliases)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new ScriptSavingNotification(script, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                return _templateRepository.GetAll(aliases).OrderBy(x => x.Name);
+                scope.Complete();
+                return;
             }
+
+            _scriptRepository.Save(script);
+            scope.Notifications.Publish(
+                new ScriptSavedNotification(script, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, userId.Value, -1, "Script");
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteScript(string path, int? userId = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IScript? script = _scriptRepository.Get(path);
+            if (script == null)
+            {
+                scope.Complete();
+                return;
+            }
+
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new ScriptDeletingNotification(script, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            userId ??= Constants.Security.SuperUserId;
+            _scriptRepository.Delete(script);
+            scope.Notifications.Publish(
+                new ScriptDeletedNotification(script, eventMessages).WithStateFrom(deletingNotification));
+
+            Audit(AuditType.Delete, userId.Value, -1, "Script");
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void CreateScriptFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _scriptRepository.AddFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void DeleteScriptFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _scriptRepository.DeleteFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public Stream GetScriptFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _scriptRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    /// 
+    public void SetScriptFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _scriptRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public long GetScriptFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _scriptRepository.GetFileSize(filepath);
+        }
+    }
+
+    #endregion
+
+    #region Templates
+
+    /// 
+    ///     Creates a template for a content type
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     The template created
+    /// 
+    public Attempt?> CreateTemplateForContentType(
+        string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId)
+    {
+        var template = new Template(_shortStringHelper, contentTypeName,
+
+            // NOTE: We are NOT passing in the content type alias here, we want to use it's name since we don't
+            // want to save template file names as camelCase, the Template ctor will clean the alias as
+            // `alias.ToCleanString(CleanStringType.UnderscoreAlias)` which has been the default.
+            // This fixes: http://issues.umbraco.org/issue/U4-7953
+            contentTypeName);
+
+        EventMessages eventMessages = EventMessagesFactory.Get();
+
+        if (contentTypeAlias != null && contentTypeAlias.Length > 255)
+        {
+            throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
         }
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetTemplates(int masterTemplateId)
+        // check that the template hasn't been created on disk before creating the content type
+        // if it exists, set the new template content to the existing file content
+        var content = GetViewContent(contentTypeAlias);
+        if (content != null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetChildren(masterTemplateId).OrderBy(x => x.Name);
-            }
+            template.Content = content;
         }
 
-        /// 
-        /// Gets a  object by its alias.
-        /// 
-        /// The alias of the template.
-        /// The  object matching the alias, or null.
-        public ITemplate? GetTemplate(string? alias)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            var savingEvent = new TemplateSavingNotification(template, eventMessages, true, contentTypeAlias!);
+            if (scope.Notifications.PublishCancelable(savingEvent))
             {
-                return _templateRepository.Get(alias);
+                scope.Complete();
+                return OperationResult.Attempt.Fail(
+                    OperationResultType.FailedCancelledByEvent, eventMessages, template);
             }
+
+            _templateRepository.Save(template);
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingEvent));
+
+            Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
         }
 
-        /// 
-        /// Gets a  object by its identifier.
-        /// 
-        /// The identifier of the template.
-        /// The  object matching the identifier, or null.
-        public ITemplate? GetTemplate(int id)
+        return OperationResult.Attempt.Succeed(
+            OperationResultType.Success,
+            eventMessages,
+            template);
+    }
+
+    /// 
+    ///     Create a new template, setting the content if a view exists in the filesystem
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public ITemplate CreateTemplateWithIdentity(
+        string? name,
+        string? alias,
+        string? content,
+        ITemplate? masterTemplate = null,
+        int userId = Constants.Security.SuperUserId)
+    {
+        if (name == null)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.Get(id);
-            }
+            throw new ArgumentNullException(nameof(name));
         }
 
-        /// 
-        /// Gets a  object by its guid identifier.
-        /// 
-        /// The guid identifier of the template.
-        /// The  object matching the identifier, or null.
-        public ITemplate? GetTemplate(Guid id)
+        if (string.IsNullOrWhiteSpace(name))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                IQuery? query = Query().Where(x => x.Key == id);
-                return _templateRepository.Get(query)?.SingleOrDefault();
-            }
+            throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name));
         }
 
-        /// 
-        /// Gets the template descendants
-        /// 
-        /// 
-        /// 
-        public IEnumerable GetTemplateDescendants(int masterTemplateId)
+        if (name.Length > 255)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetDescendants(masterTemplateId);
-            }
+            throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length.");
         }
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// 
-        public void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId)
+        // file might already be on disk, if so grab the content to avoid overwriting
+        var template = new Template(_shortStringHelper, name, alias) { Content = GetViewContent(alias) ?? content };
+
+        if (masterTemplate != null)
         {
+            template.SetMasterTemplate(masterTemplate);
+        }
+
+        SaveTemplate(template, userId);
+
+        return template;
+    }
+
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    public IEnumerable GetTemplates(params string[] aliases)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetAll(aliases).OrderBy(x => x.Name);
+        }
+    }
+
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    public IEnumerable GetTemplates(int masterTemplateId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetChildren(masterTemplateId).OrderBy(x => x.Name);
+        }
+    }
+
+    /// 
+    ///     Gets a  object by its alias.
+    /// 
+    /// The alias of the template.
+    /// The  object matching the alias, or null.
+    public ITemplate? GetTemplate(string? alias)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.Get(alias);
+        }
+    }
+
+    /// 
+    ///     Gets a  object by its identifier.
+    /// 
+    /// The identifier of the template.
+    /// The  object matching the identifier, or null.
+    public ITemplate? GetTemplate(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.Get(id);
+        }
+    }
+
+    /// 
+    ///     Gets a  object by its guid identifier.
+    /// 
+    /// The guid identifier of the template.
+    /// The  object matching the identifier, or null.
+    public ITemplate? GetTemplate(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IQuery? query = Query().Where(x => x.Key == id);
+            return _templateRepository.Get(query)?.SingleOrDefault();
+        }
+    }
+
+    /// 
+    ///     Gets the template descendants
+    /// 
+    /// 
+    /// 
+    public IEnumerable GetTemplateDescendants(int masterTemplateId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetDescendants(masterTemplateId);
+        }
+    }
+
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// 
+    public void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId)
+    {
+        if (template == null)
+        {
+            throw new ArgumentNullException(nameof(template));
+        }
+
+        if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 255)
+        {
+            throw new InvalidOperationException(
+                "Name cannot be null, empty, contain only white-space characters or be more than 255 characters in length.");
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new TemplateSavingNotification(template, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            _templateRepository.Save(template);
+
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Saves a collection of  objects
+    /// 
+    /// List of  to save
+    /// Optional id of the user
+    public void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId)
+    {
+        ITemplate[] templatesA = templates.ToArray();
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new TemplateSavingNotification(templatesA, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            foreach (ITemplate template in templatesA)
+            {
+                _templateRepository.Save(template);
+            }
+
+            scope.Notifications.Publish(
+                new TemplateSavedNotification(templatesA, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, userId, -1, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Deletes a template by its alias
+    /// 
+    /// Alias of the  to delete
+    /// 
+    public void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            ITemplate? template = _templateRepository.Get(alias);
             if (template == null)
             {
-                throw new ArgumentNullException(nameof(template));
-            }
-
-            if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 255)
-            {
-                throw new InvalidOperationException("Name cannot be null, empty, contain only white-space characters or be more than 255 characters in length.");
-            }
-
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new TemplateSavingNotification(template, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _templateRepository.Save(template);
-
-                scope.Notifications.Publish(new TemplateSavedNotification(template, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, userId, template.Id, UmbracoObjectTypes.Template.GetName());
                 scope.Complete();
+                return;
             }
-        }
 
-        /// 
-        /// Saves a collection of  objects
-        /// 
-        /// List of  to save
-        /// Optional id of the user
-        public void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId)
-        {
-            ITemplate[] templatesA = templates.ToArray();
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new TemplateDeletingNotification(template, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new TemplateSavingNotification(templatesA, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                foreach (ITemplate template in templatesA)
-                {
-                    _templateRepository.Save(template);
-                }
-
-                scope.Notifications.Publish(new TemplateSavedNotification(templatesA, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, userId, -1, UmbracoObjectTypes.Template.GetName());
                 scope.Complete();
+                return;
             }
+
+            _templateRepository.Delete(template);
+
+            scope.Notifications.Publish(
+                new TemplateDeletedNotification(template, eventMessages).WithStateFrom(deletingNotification));
+
+            Audit(AuditType.Delete, userId, template.Id, UmbracoObjectTypes.Template.GetName());
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public Stream GetTemplateFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    private string? GetViewContent(string? fileName)
+    {
+        if (fileName.IsNullOrWhiteSpace())
+        {
+            throw new ArgumentNullException(nameof(fileName));
         }
 
-        /// 
-        /// Deletes a template by its alias
-        /// 
-        /// Alias of the  to delete
-        /// 
-        public void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId)
+        if (!fileName!.EndsWith(".cshtml"))
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                ITemplate? template = _templateRepository.Get(alias);
-                if (template == null)
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new TemplateDeletingNotification(template, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _templateRepository.Delete(template);
-
-                scope.Notifications.Publish(new TemplateDeletedNotification(template, eventMessages).WithStateFrom(deletingNotification));
-
-                Audit(AuditType.Delete, userId, template.Id, ObjectTypes.GetName(UmbracoObjectTypes.Template));
-                scope.Complete();
-            }
+            fileName = $"{fileName}.cshtml";
         }
 
-        private string? GetViewContent(string? fileName)
+        Stream fs = _templateRepository.GetFileContentStream(fileName);
+
+        using (var view = new StreamReader(fs))
         {
-            if (fileName.IsNullOrWhiteSpace())
-            {
-                throw new ArgumentNullException(nameof(fileName));
-            }
+            return view.ReadToEnd().Trim();
+        }
+    }
 
-            if (!fileName!.EndsWith(".cshtml"))
-            {
-                fileName = $"{fileName}.cshtml";
-            }
+    /// 
+    public void SetTemplateFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _templateRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
 
-            Stream fs = _templateRepository.GetFileContentStream(fileName);
+    /// 
+    public long GetTemplateFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _templateRepository.GetFileSize(filepath);
+        }
+    }
 
-            using (var view = new StreamReader(fs))
-            {
-                return view.ReadToEnd().Trim();
-            }
+    #endregion
+
+    #region Partial Views
+
+    public IEnumerable GetPartialViewSnippetNames(params string[] filterNames)
+    {
+        var snippetProvider =
+            new EmbeddedFileProvider(GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
+
+        var files = snippetProvider.GetDirectoryContents(string.Empty)
+            .Where(x => !x.IsDirectory && x.Name.EndsWith(".cshtml"))
+            .Select(x => Path.GetFileNameWithoutExtension(x.Name))
+            .Except(filterNames, StringComparer.InvariantCultureIgnoreCase)
+            .ToArray();
+
+        // Ensure the ones that are called 'Empty' are at the top
+        var empty = files.Where(x => Path.GetFileName(x)?.InvariantStartsWith("Empty") ?? false)
+            .OrderBy(x => x?.Length)
+            .ToArray();
+
+        return empty.Union(files.Except(empty)).WhereNotNull();
+    }
+
+    public void DeletePartialViewFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewRepository.DeleteFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    public void DeletePartialViewMacroFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewMacroRepository.DeleteFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    public IEnumerable GetPartialViews(params string[] names)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewRepository.GetMany(names);
+        }
+    }
+
+    public IPartialView? GetPartialView(string path)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewRepository.Get(path);
+        }
+    }
+
+    public IPartialView? GetPartialViewMacro(string path)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewMacroRepository.Get(path);
+        }
+    }
+
+    public Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) =>
+        CreatePartialViewMacro(partialView, PartialViewType.PartialView, snippetName, userId);
+
+    public Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) =>
+        CreatePartialViewMacro(partialView, PartialViewType.PartialViewMacro, snippetName, userId);
+
+    public bool DeletePartialView(string path, int? userId = null) =>
+        DeletePartialViewMacro(path, PartialViewType.PartialView, userId);
+
+    private Attempt CreatePartialViewMacro(
+        IPartialView partialView,
+        PartialViewType partialViewType,
+        string? snippetName = null,
+        int? userId = Constants.Security.SuperUserId)
+    {
+        string partialViewHeader;
+        switch (partialViewType)
+        {
+            case PartialViewType.PartialView:
+                partialViewHeader = PartialViewHeader;
+                break;
+            case PartialViewType.PartialViewMacro:
+                partialViewHeader = PartialViewMacroHeader;
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(partialViewType));
         }
 
-        /// 
-        public Stream GetTemplateFileContentStream(string filepath)
+        string? partialViewContent = null;
+        if (snippetName.IsNullOrWhiteSpace() == false)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetTemplateFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _templateRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetTemplateFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _templateRepository.GetFileSize(filepath);
-            }
-        }
-
-        #endregion
-
-        #region Partial Views
-
-        public IEnumerable GetPartialViewSnippetNames(params string[] filterNames)
-        {
-            var snippetProvider =
-                new EmbeddedFileProvider(this.GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
-
-            var files = snippetProvider.GetDirectoryContents(string.Empty)
-                .Where(x => !x.IsDirectory && x.Name.EndsWith(".cshtml"))
-                .Select(x => Path.GetFileNameWithoutExtension(x.Name))
-                .Except(filterNames, StringComparer.InvariantCultureIgnoreCase)
-                .ToArray();
-
-            //Ensure the ones that are called 'Empty' are at the top
-            var empty = files.Where(x => Path.GetFileName(x)?.InvariantStartsWith("Empty") ?? false)
-                .OrderBy(x => x?.Length)
-                .ToArray();
-
-            return empty.Union(files.Except(empty)).WhereNotNull();
-        }
-
-        public void DeletePartialViewFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        public void DeletePartialViewMacroFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.DeleteFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        public IEnumerable GetPartialViews(params string[] names)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetMany(names);
-            }
-        }
-
-        public IPartialView? GetPartialView(string path)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.Get(path);
-            }
-        }
-
-        public IPartialView? GetPartialViewMacro(string path)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.Get(path);
-            }
-        }
-
-        public Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) =>
-            CreatePartialViewMacro(partialView, PartialViewType.PartialView, snippetName, userId);
-
-        public Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) =>
-            CreatePartialViewMacro(partialView, PartialViewType.PartialViewMacro, snippetName, userId);
-
-        private Attempt CreatePartialViewMacro(IPartialView partialView, PartialViewType partialViewType, string? snippetName = null, int? userId = Constants.Security.SuperUserId)
-        {
-            string partialViewHeader;
-            switch (partialViewType)
-            {
-                case PartialViewType.PartialView:
-                    partialViewHeader = PartialViewHeader;
-                    break;
-                case PartialViewType.PartialViewMacro:
-                    partialViewHeader = PartialViewMacroHeader;
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(partialViewType));
-            }
-
-            string? partialViewContent = null;
-            if (snippetName.IsNullOrWhiteSpace() == false)
-            {
-                //create the file
-                Attempt snippetPathAttempt = TryGetSnippetPath(snippetName);
-                if (snippetPathAttempt.Success == false)
-                {
-                    throw new InvalidOperationException("Could not load snippet with name " + snippetName);
-                }
-
-                using (var snippetFile = new StreamReader(System.IO.File.OpenRead(snippetPathAttempt.Result!)))
-                {
-                    var snippetContent = snippetFile.ReadToEnd().Trim();
-
-                    //strip the @inherits if it's there
-                    snippetContent = StripPartialViewHeader(snippetContent);
-
-                    //Update Model.Content. to be Model. when used as PartialView
-                    if(partialViewType == PartialViewType.PartialView)
-                    {
-                        snippetContent = snippetContent.Replace("Model.Content.", "Model.");
-                    }
-
-                    partialViewContent = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
-                }
-            }
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var creatingNotification = new PartialViewCreatingNotification(partialView, eventMessages);
-                if (scope.Notifications.PublishCancelable(creatingNotification))
-                {
-                    scope.Complete();
-                    return Attempt.Fail();
-                }
-
-                IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
-                if (partialViewContent != null)
-                {
-                    partialView.Content = partialViewContent;
-                }
-
-                repository.Save(partialView);
-
-                scope.Notifications.Publish(new PartialViewCreatedNotification(partialView, eventMessages).WithStateFrom(creatingNotification));
-
-                Audit(AuditType.Save, userId!.Value, -1, partialViewType.ToString());
-
-                scope.Complete();
-            }
-
-            return Attempt.Succeed(partialView);
-        }
-
-        public bool DeletePartialView(string path, int? userId = null) =>
-            DeletePartialViewMacro(path, PartialViewType.PartialView, userId);
-
-        public bool DeletePartialViewMacro(string path, int? userId = null) =>
-            DeletePartialViewMacro(path, PartialViewType.PartialViewMacro, userId);
-
-        private bool DeletePartialViewMacro(string path, PartialViewType partialViewType, int? userId = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-
-                IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
-                IPartialView? partialView = repository.Get(path);
-                if (partialView == null)
-                {
-                    scope.Complete();
-                    return true;
-                }
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new PartialViewDeletingNotification(partialView, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return false;
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                repository.Delete(partialView);
-                scope.Notifications.Publish(new PartialViewDeletedNotification(partialView, eventMessages).WithStateFrom(deletingNotification));
-                Audit(AuditType.Delete, userId.Value, -1, partialViewType.ToString());
-
-                scope.Complete();
-            }
-
-            return true;
-        }
-
-        public Attempt SavePartialView(IPartialView partialView, int? userId = null) =>
-            SavePartialView(partialView, PartialViewType.PartialView, userId);
-
-        public Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null) =>
-            SavePartialView(partialView, PartialViewType.PartialViewMacro, userId);
-
-        private Attempt SavePartialView(IPartialView partialView, PartialViewType partialViewType, int? userId = null)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new PartialViewSavingNotification(partialView, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return Attempt.Fail();
-                }
-
-                userId ??= Constants.Security.SuperUserId;
-                IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
-                repository.Save(partialView);
-
-                Audit(AuditType.Save, userId.Value, -1, partialViewType.ToString());
-                scope.Notifications.Publish(new PartialViewSavedNotification(partialView, eventMessages).WithStateFrom(savingNotification));
-
-                scope.Complete();
-            }
-
-            return Attempt.Succeed(partialView);
-        }
-
-        internal string StripPartialViewHeader(string contents)
-        {
-            var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline);
-            return headerMatch.Replace(contents, string.Empty);
-        }
-
-        internal Attempt TryGetSnippetPath(string? fileName)
-        {
-            if (fileName?.EndsWith(".cshtml") == false)
-            {
-                fileName += ".cshtml";
-            }
-
-            var snippetPath = _hostingEnvironment.MapPathContentRoot($"{Constants.SystemDirectories.Umbraco}/PartialViewMacros/Templates/{fileName}");
-            return System.IO.File.Exists(snippetPath)
-                ? Attempt.Succeed(snippetPath)
-                : Attempt.Fail();
-        }
-
-        public void CreatePartialViewFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        public void CreatePartialViewMacroFolder(string folderPath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.AddFolder(folderPath);
-                scope.Complete();
-            }
-        }
-
-        private IPartialViewRepository GetPartialViewRepository(PartialViewType partialViewType)
-        {
-            switch (partialViewType)
-            {
-                case PartialViewType.PartialView:
-                    return _partialViewRepository;
-                case PartialViewType.PartialViewMacro:
-                    return _partialViewMacroRepository;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(partialViewType));
-            }
-        }
-
-        /// 
-        public Stream GetPartialViewFileContentStream(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetPartialViewFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetPartialViewFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewRepository.GetFileSize(filepath);
-            }
-        }
-
-        /// 
-        public Stream GetPartialViewMacroFileContentStream(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.GetFileContentStream(filepath);
-            }
-        }
-
-        /// 
-        public void SetPartialViewMacroFileContent(string filepath, Stream content)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                _partialViewMacroRepository.SetFileContent(filepath, content);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public long GetPartialViewMacroFileSize(string filepath)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _partialViewMacroRepository.GetFileSize(filepath);
-            }
-        }
-
-        #endregion
-
-        #region Snippets
-
-        public string GetPartialViewSnippetContent(string snippetName) => GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialView);
-
-        public string GetPartialViewMacroSnippetContent(string snippetName) => GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialViewMacro);
-
-        private string GetPartialViewMacroSnippetContent(string snippetName, PartialViewType partialViewType)
-        {
-            if (snippetName.IsNullOrWhiteSpace())
-            {
-                throw new ArgumentNullException(nameof(snippetName));
-            }
-
-            string partialViewHeader;
-            switch (partialViewType)
-            {
-                case PartialViewType.PartialView:
-                    partialViewHeader = PartialViewHeader;
-                    break;
-                case PartialViewType.PartialViewMacro:
-                    partialViewHeader = PartialViewMacroHeader;
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(partialViewType));
-            }
-
-            var snippetProvider =
-                new EmbeddedFileProvider(this.GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
-
-            var file = snippetProvider.GetDirectoryContents(string.Empty).FirstOrDefault(x=>x.Exists && x.Name.Equals(snippetName + ".cshtml"));
-
-            // Try and get the snippet path
-            if (file is null)
+            // create the file
+            Attempt snippetPathAttempt = TryGetSnippetPath(snippetName);
+            if (snippetPathAttempt.Success == false)
             {
                 throw new InvalidOperationException("Could not load snippet with name " + snippetName);
             }
 
-            using (var snippetFile = new StreamReader(file.CreateReadStream()))
+            using (var snippetFile = new StreamReader(File.OpenRead(snippetPathAttempt.Result!)))
             {
                 var snippetContent = snippetFile.ReadToEnd().Trim();
 
-                //strip the @inherits if it's there
+                // strip the @inherits if it's there
                 snippetContent = StripPartialViewHeader(snippetContent);
 
-                //Update Model.Content to be Model when used as PartialView
+                // Update Model.Content. to be Model. when used as PartialView
                 if (partialViewType == PartialViewType.PartialView)
                 {
-                    snippetContent = snippetContent
-                        .Replace("Model.Content.", "Model.")
-                        .Replace("(Model.Content)", "(Model)");
+                    snippetContent = snippetContent.Replace("Model.Content.", "Model.");
                 }
 
-                var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
-                return content;
+                partialViewContent = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
             }
         }
 
-        #endregion
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var creatingNotification = new PartialViewCreatingNotification(partialView, eventMessages);
+            if (scope.Notifications.PublishCancelable(creatingNotification))
+            {
+                scope.Complete();
+                return Attempt.Fail();
+            }
 
-        private void Audit(AuditType type, int userId, int objectId, string? entityType) => _auditRepository.Save(new AuditItem(objectId, type, userId, entityType));
+            IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
+            if (partialViewContent != null)
+            {
+                partialView.Content = partialViewContent;
+            }
 
-        // TODO: Method to change name and/or alias of view template
+            repository.Save(partialView);
+
+            scope.Notifications.Publish(
+                new PartialViewCreatedNotification(partialView, eventMessages).WithStateFrom(creatingNotification));
+
+            Audit(AuditType.Save, userId!.Value, -1, partialViewType.ToString());
+
+            scope.Complete();
+        }
+
+        return Attempt.Succeed(partialView);
     }
+
+    public bool DeletePartialViewMacro(string path, int? userId = null) =>
+        DeletePartialViewMacro(path, PartialViewType.PartialViewMacro, userId);
+
+    public Attempt SavePartialView(IPartialView partialView, int? userId = null) =>
+        SavePartialView(partialView, PartialViewType.PartialView, userId);
+
+    private bool DeletePartialViewMacro(string path, PartialViewType partialViewType, int? userId = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
+            IPartialView? partialView = repository.Get(path);
+            if (partialView == null)
+            {
+                scope.Complete();
+                return true;
+            }
+
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new PartialViewDeletingNotification(partialView, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
+                scope.Complete();
+                return false;
+            }
+
+            userId ??= Constants.Security.SuperUserId;
+            repository.Delete(partialView);
+            scope.Notifications.Publish(
+                new PartialViewDeletedNotification(partialView, eventMessages).WithStateFrom(deletingNotification));
+            Audit(AuditType.Delete, userId.Value, -1, partialViewType.ToString());
+
+            scope.Complete();
+        }
+
+        return true;
+    }
+
+    public Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null) =>
+        SavePartialView(partialView, PartialViewType.PartialViewMacro, userId);
+
+    public void CreatePartialViewFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewRepository.AddFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    internal string StripPartialViewHeader(string contents)
+    {
+        var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline);
+        return headerMatch.Replace(contents, string.Empty);
+    }
+
+    private Attempt SavePartialView(IPartialView partialView, PartialViewType partialViewType, int? userId = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new PartialViewSavingNotification(partialView, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return Attempt.Fail();
+            }
+
+            userId ??= Constants.Security.SuperUserId;
+            IPartialViewRepository repository = GetPartialViewRepository(partialViewType);
+            repository.Save(partialView);
+
+            Audit(AuditType.Save, userId.Value, -1, partialViewType.ToString());
+            scope.Notifications.Publish(
+                new PartialViewSavedNotification(partialView, eventMessages).WithStateFrom(savingNotification));
+
+            scope.Complete();
+        }
+
+        return Attempt.Succeed(partialView);
+    }
+
+    internal Attempt TryGetSnippetPath(string? fileName)
+    {
+        if (fileName?.EndsWith(".cshtml") == false)
+        {
+            fileName += ".cshtml";
+        }
+
+        var snippetPath =
+            _hostingEnvironment.MapPathContentRoot(
+                $"{Constants.SystemDirectories.Umbraco}/PartialViewMacros/Templates/{fileName}");
+        return File.Exists(snippetPath)
+            ? Attempt.Succeed(snippetPath)
+            : Attempt.Fail();
+    }
+
+    public void CreatePartialViewMacroFolder(string folderPath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewMacroRepository.AddFolder(folderPath);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public Stream GetPartialViewFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    private IPartialViewRepository GetPartialViewRepository(PartialViewType partialViewType)
+    {
+        switch (partialViewType)
+        {
+            case PartialViewType.PartialView:
+                return _partialViewRepository;
+            case PartialViewType.PartialViewMacro:
+                return _partialViewMacroRepository;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(partialViewType));
+        }
+    }
+
+    /// 
+    public void SetPartialViewFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public long GetPartialViewFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewRepository.GetFileSize(filepath);
+        }
+    }
+
+    /// 
+    public Stream GetPartialViewMacroFileContentStream(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewMacroRepository.GetFileContentStream(filepath);
+        }
+    }
+
+    /// 
+    public void SetPartialViewMacroFileContent(string filepath, Stream content)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            _partialViewMacroRepository.SetFileContent(filepath, content);
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public long GetPartialViewMacroFileSize(string filepath)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _partialViewMacroRepository.GetFileSize(filepath);
+        }
+    }
+
+    #endregion
+
+    #region Snippets
+
+    public string GetPartialViewSnippetContent(string snippetName) =>
+        GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialView);
+
+    public string GetPartialViewMacroSnippetContent(string snippetName) =>
+        GetPartialViewMacroSnippetContent(snippetName, PartialViewType.PartialViewMacro);
+
+    private string GetPartialViewMacroSnippetContent(string snippetName, PartialViewType partialViewType)
+    {
+        if (snippetName.IsNullOrWhiteSpace())
+        {
+            throw new ArgumentNullException(nameof(snippetName));
+        }
+
+        string partialViewHeader;
+        switch (partialViewType)
+        {
+            case PartialViewType.PartialView:
+                partialViewHeader = PartialViewHeader;
+                break;
+            case PartialViewType.PartialViewMacro:
+                partialViewHeader = PartialViewMacroHeader;
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(partialViewType));
+        }
+
+        var snippetProvider =
+            new EmbeddedFileProvider(GetType().Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets");
+
+        IFileInfo? file = snippetProvider.GetDirectoryContents(string.Empty)
+            .FirstOrDefault(x => x.Exists && x.Name.Equals(snippetName + ".cshtml"));
+
+        // Try and get the snippet path
+        if (file is null)
+        {
+            throw new InvalidOperationException("Could not load snippet with name " + snippetName);
+        }
+
+        using (var snippetFile = new StreamReader(file.CreateReadStream()))
+        {
+            var snippetContent = snippetFile.ReadToEnd().Trim();
+
+            // strip the @inherits if it's there
+            snippetContent = StripPartialViewHeader(snippetContent);
+
+            // Update Model.Content to be Model when used as PartialView
+            if (partialViewType == PartialViewType.PartialView)
+            {
+                snippetContent = snippetContent
+                    .Replace("Model.Content.", "Model.")
+                    .Replace("(Model.Content)", "(Model)");
+            }
+
+            var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}";
+            return content;
+        }
+    }
+
+    #endregion
+
+    // TODO: Method to change name and/or alias of view template
 }
diff --git a/src/Umbraco.Core/Services/IAuditService.cs b/src/Umbraco.Core/Services/IAuditService.cs
index df816960a3..f58da53174 100644
--- a/src/Umbraco.Core/Services/IAuditService.cs
+++ b/src/Umbraco.Core/Services/IAuditService.cs
@@ -1,86 +1,104 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents a service for handling audit.
+/// 
+public interface IAuditService : IService
 {
+    void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null);
+
+    IEnumerable GetLogs(int objectId);
+
+    IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null);
+
+    IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null);
+
+    void CleanLogs(int maximumAgeOfLogsInMinutes);
+
     /// 
-    /// Represents a service for handling audit.
+    ///     Returns paged items in the audit trail for a given entity
     /// 
-    public interface IAuditService : IService
-    {
-        void Add(AuditType type, int userId, int objectId, string? entityType, string comment, string? parameters = null);
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     By default this will always be ordered descending (newest first)
+    /// 
+    /// 
+    ///     Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
+    ///     or the custom filter
+    ///     so we need to do that here
+    /// 
+    /// 
+    ///     Optional filter to be applied
+    /// 
+    /// 
+    IEnumerable GetPagedItemsByEntity(
+        int entityId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        Direction orderDirection = Direction.Descending,
+        AuditType[]? auditTypeFilter = null,
+        IQuery? customFilter = null);
 
-        IEnumerable GetLogs(int objectId);
-        IEnumerable GetUserLogs(int userId, AuditType type, DateTime? sinceDate = null);
-        IEnumerable GetLogs(AuditType type, DateTime? sinceDate = null);
-        void CleanLogs(int maximumAgeOfLogsInMinutes);
+    /// 
+    ///     Returns paged items in the audit trail for a given user
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     By default this will always be ordered descending (newest first)
+    /// 
+    /// 
+    ///     Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query
+    ///     or the custom filter
+    ///     so we need to do that here
+    /// 
+    /// 
+    ///     Optional filter to be applied
+    /// 
+    /// 
+    IEnumerable GetPagedItemsByUser(
+        int userId,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        Direction orderDirection = Direction.Descending,
+        AuditType[]? auditTypeFilter = null,
+        IQuery? customFilter = null);
 
-        /// 
-        /// Returns paged items in the audit trail for a given entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// By default this will always be ordered descending (newest first)
-        /// 
-        /// 
-        /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter
-        /// so we need to do that here
-        /// 
-        /// 
-        /// Optional filter to be applied
-        /// 
-        /// 
-        IEnumerable GetPagedItemsByEntity(int entityId, long pageIndex, int pageSize, out long totalRecords,
-            Direction orderDirection = Direction.Descending,
-            AuditType[]? auditTypeFilter = null,
-            IQuery? customFilter = null);
-
-        /// 
-        /// Returns paged items in the audit trail for a given user
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// By default this will always be ordered descending (newest first)
-        /// 
-        /// 
-        /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter
-        /// so we need to do that here
-        /// 
-        /// 
-        /// Optional filter to be applied
-        /// 
-        /// 
-        IEnumerable GetPagedItemsByUser(int userId, long pageIndex, int pageSize, out long totalRecords,
-            Direction orderDirection = Direction.Descending,
-            AuditType[]? auditTypeFilter = null,
-            IQuery? customFilter = null);
-
-        /// 
-        /// Writes an audit entry for an audited event.
-        /// 
-        /// The identifier of the user triggering the audited event.
-        /// Free-form details about the user triggering the audited event.
-        /// The IP address or the request triggering the audited event.
-        /// The date and time of the audited event.
-        /// The identifier of the user affected by the audited event.
-        /// Free-form details about the entity affected by the audited event.
-        /// 
-        /// The type of the audited event - must contain only alphanumeric chars and hyphens with forward slashes separating categories.
-        /// 
-        /// The eventType will generally be formatted like: {application}/{entity-type}/{category}/{sub-category}
-        /// Example: umbraco/user/sign-in/failed
-        /// 
-        /// 
-        /// Free-form details about the audited event.
-        IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string affectedDetails, string eventType, string eventDetails);
-
-    }
+    /// 
+    ///     Writes an audit entry for an audited event.
+    /// 
+    /// The identifier of the user triggering the audited event.
+    /// Free-form details about the user triggering the audited event.
+    /// The IP address or the request triggering the audited event.
+    /// The date and time of the audited event.
+    /// The identifier of the user affected by the audited event.
+    /// Free-form details about the entity affected by the audited event.
+    /// 
+    ///     The type of the audited event - must contain only alphanumeric chars and hyphens with forward slashes separating
+    ///     categories.
+    ///     
+    ///         The eventType will generally be formatted like: {application}/{entity-type}/{category}/{sub-category}
+    ///         Example: umbraco/user/sign-in/failed
+    ///     
+    /// 
+    /// Free-form details about the audited event.
+    IAuditEntry Write(
+        int performingUserId,
+        string perfomingDetails,
+        string performingIp,
+        DateTime eventDateUtc,
+        int affectedUserId,
+        string affectedDetails,
+        string eventType,
+        string eventDetails);
 }
diff --git a/src/Umbraco.Core/Services/IBasicAuthService.cs b/src/Umbraco.Core/Services/IBasicAuthService.cs
index 82e48e1180..c371376f85 100644
--- a/src/Umbraco.Core/Services/IBasicAuthService.cs
+++ b/src/Umbraco.Core/Services/IBasicAuthService.cs
@@ -1,14 +1,13 @@
 using System.Net;
 using Microsoft.Extensions.Primitives;
 
-namespace Umbraco.Cms.Core.Services
-{
-    public interface IBasicAuthService
-    {
-        bool IsBasicAuthEnabled();
-        bool IsIpAllowListed(IPAddress clientIpAddress);
-        bool HasCorrectSharedSecret(IDictionary headers) => false;
+namespace Umbraco.Cms.Core.Services;
 
-        bool IsRedirectToLoginPageEnabled() => false;
-    }
+public interface IBasicAuthService
+{
+    bool IsBasicAuthEnabled();
+    bool IsIpAllowListed(IPAddress clientIpAddress);
+    bool HasCorrectSharedSecret(IDictionary headers) => false;
+
+    bool IsRedirectToLoginPageEnabled() => false;
 }
diff --git a/src/Umbraco.Core/Services/ICacheInstructionService.cs b/src/Umbraco.Core/Services/ICacheInstructionService.cs
index c884b8bed8..0b71bde66d 100644
--- a/src/Umbraco.Core/Services/ICacheInstructionService.cs
+++ b/src/Umbraco.Core/Services/ICacheInstructionService.cs
@@ -1,53 +1,50 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
 using Umbraco.Cms.Core.Cache;
 using Umbraco.Cms.Core.Sync;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface ICacheInstructionService
 {
-    public interface ICacheInstructionService
-    {
-        /// 
-        /// Checks to see if a cold boot is required, either because instructions exist and none have been synced or
-        /// because the last recorded synced instruction can't be found in the database.
-        /// 
-        bool IsColdBootRequired(int lastId);
+    /// 
+    ///     Checks to see if a cold boot is required, either because instructions exist and none have been synced or
+    ///     because the last recorded synced instruction can't be found in the database.
+    /// 
+    bool IsColdBootRequired(int lastId);
 
-        /// 
-        /// Checks to see if the number of pending instructions are over the configured limit.
-        /// 
-        bool IsInstructionCountOverLimit(int lastId, int limit, out int count);
+    /// 
+    ///     Checks to see if the number of pending instructions are over the configured limit.
+    /// 
+    bool IsInstructionCountOverLimit(int lastId, int limit, out int count);
 
-        /// 
-        /// Gets the most recent cache instruction record Id.
-        /// 
-        /// 
-        int GetMaxInstructionId();
+    /// 
+    ///     Gets the most recent cache instruction record Id.
+    /// 
+    /// 
+    int GetMaxInstructionId();
 
-        /// 
-        /// Creates a cache instruction record from a set of individual instructions and saves it.
-        /// 
-        void DeliverInstructions(IEnumerable instructions, string localIdentity);
+    /// 
+    ///     Creates a cache instruction record from a set of individual instructions and saves it.
+    /// 
+    void DeliverInstructions(IEnumerable instructions, string localIdentity);
 
-        /// 
-        /// Creates one or more cache instruction records based on the configured batch size from a set of individual instructions and saves them.
-        /// 
-        void DeliverInstructionsInBatches(IEnumerable instructions, string localIdentity);
+    /// 
+    ///     Creates one or more cache instruction records based on the configured batch size from a set of individual
+    ///     instructions and saves them.
+    /// 
+    void DeliverInstructionsInBatches(IEnumerable instructions, string localIdentity);
 
-        /// 
-        /// Processes and then prunes pending database cache instructions.
-        /// 
-        /// Flag indicating if process is shutting now and operations should exit.
-        /// Local identity of the executing AppDomain.
-        /// Date of last prune operation.
-        /// Id of the latest processed instruction
-        ProcessInstructionsResult ProcessInstructions(
-            CacheRefresherCollection cacheRefreshers,
-            ServerRole serverRole,
-            CancellationToken cancellationToken,
-            string localIdentity,
-            DateTime lastPruned,
-            int lastId);
-    }
+    /// 
+    ///     Processes and then prunes pending database cache instructions.
+    /// 
+    /// Flag indicating if process is shutting now and operations should exit.
+    /// Local identity of the executing AppDomain.
+    /// Date of last prune operation.
+    /// Id of the latest processed instruction
+    ProcessInstructionsResult ProcessInstructions(
+        CacheRefresherCollection cacheRefreshers,
+        ServerRole serverRole,
+        CancellationToken cancellationToken,
+        string localIdentity,
+        DateTime lastPruned,
+        int lastId);
 }
diff --git a/src/Umbraco.Core/Services/IConflictingRouteService.cs b/src/Umbraco.Core/Services/IConflictingRouteService.cs
index 04d81d7f88..fe044362b7 100644
--- a/src/Umbraco.Core/Services/IConflictingRouteService.cs
+++ b/src/Umbraco.Core/Services/IConflictingRouteService.cs
@@ -1,7 +1,6 @@
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IConflictingRouteService
 {
-    public interface IConflictingRouteService
-    {
-        public bool HasConflictingRoutes(out string controllerName);
-    }
+    public bool HasConflictingRoutes(out string controllerName);
 }
diff --git a/src/Umbraco.Core/Services/IConsentService.cs b/src/Umbraco.Core/Services/IConsentService.cs
index d191caebe2..dc04008503 100644
--- a/src/Umbraco.Core/Services/IConsentService.cs
+++ b/src/Umbraco.Core/Services/IConsentService.cs
@@ -1,45 +1,52 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     A service for handling lawful data processing requirements
+/// 
+/// 
+///     
+///         Consent can be given or revoked or changed via the  method, which
+///         creates a new  entity to track the consent. Revoking a consent is performed by
+///         registering a revoked consent.
+///     
+///     A consent can be revoked, by registering a revoked consent, but cannot be deleted.
+///     
+///         Getter methods return the current state of a consent, i.e. the latest 
+///         entity that was created.
+///     
+/// 
+public interface IConsentService : IService
 {
     /// 
-    /// A service for handling lawful data processing requirements
+    ///     Registers consent.
     /// 
-    /// 
-    /// Consent can be given or revoked or changed via the  method, which
-    /// creates a new  entity to track the consent. Revoking a consent is performed by
-    /// registering a revoked consent.
-    /// A consent can be revoked, by registering a revoked consent, but cannot be deleted.
-    /// Getter methods return the current state of a consent, i.e. the latest 
-    /// entity that was created.
-    /// 
-    public interface IConsentService : IService
-    {
-        /// 
-        /// Registers consent.
-        /// 
-        /// The source, i.e. whoever is consenting.
-        /// 
-        /// 
-        /// The state of the consent.
-        /// Additional free text.
-        /// The corresponding consent entity.
-        IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null);
+    /// The source, i.e. whoever is consenting.
+    /// 
+    /// 
+    /// The state of the consent.
+    /// Additional free text.
+    /// The corresponding consent entity.
+    IConsent RegisterConsent(string source, string context, string action, ConsentState state, string? comment = null);
 
-        /// 
-        /// Retrieves consents.
-        /// 
-        /// The optional source.
-        /// The optional context.
-        /// The optional action.
-        /// Determines whether  is a start pattern.
-        /// Determines whether  is a start pattern.
-        /// Determines whether  is a start pattern.
-        /// Determines whether to include the history of consents.
-        /// Consents matching the parameters.
-        IEnumerable LookupConsent(string? source = null, string? context = null, string? action = null,
-            bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false,
-            bool includeHistory = false);
-    }
+    /// 
+    ///     Retrieves consents.
+    /// 
+    /// The optional source.
+    /// The optional context.
+    /// The optional action.
+    /// Determines whether  is a start pattern.
+    /// Determines whether  is a start pattern.
+    /// Determines whether  is a start pattern.
+    /// Determines whether to include the history of consents.
+    /// Consents matching the parameters.
+    IEnumerable LookupConsent(
+        string? source = null,
+        string? context = null,
+        string? action = null,
+        bool sourceStartsWith = false,
+        bool contextStartsWith = false,
+        bool actionStartsWith = false,
+        bool includeHistory = false);
 }
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index 93d51da757..1eb2db83bf 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -1,544 +1,559 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the ContentService, which is an easy access to operations involving 
+/// 
+public interface IContentService : IContentServiceBase
 {
+    #region Rollback
+
     /// 
-    /// Defines the ContentService, which is an easy access to operations involving 
+    ///     Rolls back the content to a specific version.
     /// 
-    public interface IContentService : IContentServiceBase
-    {
-        #region Blueprints
-
-        /// 
-        /// Gets a blueprint.
-        /// 
-        IContent? GetBlueprintById(int id);
-
-        /// 
-        /// Gets a blueprint.
-        /// 
-        IContent? GetBlueprintById(Guid id);
-
-        /// 
-        /// Gets blueprints for a content type.
-        /// 
-        IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeId);
-
-        /// 
-        /// Saves a blueprint.
-        /// 
-        void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes a blueprint.
-        /// 
-        void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a new content item from a blueprint.
-        /// 
-        IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes blueprints for a content type.
-        /// 
-        void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes blueprints for content types.
-        /// 
-        void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-        #region Get, Count Documents
-
-        /// 
-        /// Gets a document.
-        /// 
-        IContent? GetById(int id);
-
-        /// 
-        /// Gets a document.
-        /// 
-        IContent? GetById(Guid key);
-
-        /// 
-        /// Gets publish/unpublish schedule for a content node.
-        /// 
-        /// Id of the Content to load schedule for
-        /// 
-        ContentScheduleCollection GetContentScheduleByContentId(int contentId);
-
-        /// 
-        /// Persists publish/unpublish schedule for a content node.
-        /// 
-        /// 
-        /// 
-        void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule);
-
-        /// 
-        /// Gets documents.
-        /// 
-        IEnumerable GetByIds(IEnumerable ids);
-
-        /// 
-        /// Gets documents.
-        /// 
-        IEnumerable GetByIds(IEnumerable ids);
-
-        /// 
-        /// Gets documents at a given level.
-        /// 
-        IEnumerable GetByLevel(int level);
-
-        /// 
-        /// Gets the parent of a document.
-        /// 
-        IContent? GetParent(int id);
-
-        /// 
-        /// Gets the parent of a document.
-        /// 
-        IContent? GetParent(IContent content);
-
-        /// 
-        /// Gets ancestor documents of a document.
-        /// 
-        IEnumerable GetAncestors(int id);
-
-        /// 
-        /// Gets ancestor documents of a document.
-        /// 
-        IEnumerable GetAncestors(IContent content);
-
-        /// 
-        /// Gets all versions of a document.
-        /// 
-        /// Versions are ordered with current first, then most recent first.
-        IEnumerable GetVersions(int id);
-
-        /// 
-        /// Gets all versions of a document.
-        /// 
-        /// Versions are ordered with current first, then most recent first.
-        IEnumerable GetVersionsSlim(int id, int skip, int take);
-
-        /// 
-        /// Gets top versions of a document.
-        /// 
-        /// Versions are ordered with current first, then most recent first.
-        IEnumerable GetVersionIds(int id, int topRows);
-
-        /// 
-        /// Gets a version of a document.
-        /// 
-        IContent? GetVersion(int versionId);
-
-        /// 
-        /// Gets root-level documents.
-        /// 
-        IEnumerable GetRootContent();
-
-        /// 
-        /// Gets documents having an expiration date before (lower than, or equal to) a specified date.
-        /// 
-        /// An Enumerable list of  objects
-        /// 
-        /// The content returned from this method may be culture variant, in which case the resulting  should be queried
-        /// for which culture(s) have been scheduled.
-        /// 
-        IEnumerable GetContentForExpiration(DateTime date);
-
-        /// 
-        /// Gets documents having a release date before (lower than, or equal to) a specified date.
-        /// 
-        /// An Enumerable list of  objects
-        /// 
-        /// The content returned from this method may be culture variant, in which case the resulting  should be queried
-        /// for which culture(s) have been scheduled.
-        /// 
-        IEnumerable GetContentForRelease(DateTime date);
-
-        /// 
-        /// Gets documents in the recycle bin.
-        /// 
-        IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
-
-        /// 
-        /// Gets child documents of a parent.
-        /// 
-        /// The parent identifier.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Query filter.
-        /// Ordering infos.
-        IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
-
-        /// 
-        /// Gets descendant documents of a given parent.
-        /// 
-        /// The parent identifier.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Query filter.
-        /// Ordering infos.
-        IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
-
-        /// 
-        /// Gets paged documents of a content
-        /// 
-        /// The page number.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Search text filter.
-        /// Ordering infos.
-        IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords,
-            IQuery filter, Ordering? ordering = null);
-
-        /// 
-        /// Gets paged documents for specified content types
-        /// 
-        /// The page number.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Search text filter.
-        /// Ordering infos.
-        IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter, Ordering? ordering = null);
-
-        /// 
-        /// Counts documents of a given document type.
-        /// 
-        int Count(string? documentTypeAlias = null);
-
-        /// 
-        /// Counts published documents of a given document type.
-        /// 
-        int CountPublished(string? documentTypeAlias = null);
-
-        /// 
-        /// Counts child documents of a given parent, of a given document type.
-        /// 
-        int CountChildren(int parentId, string? documentTypeAlias = null);
-
-        /// 
-        /// Counts descendant documents of a given parent, of a given document type.
-        /// 
-        int CountDescendants(int parentId, string? documentTypeAlias = null);
-
-        /// 
-        /// Gets a value indicating whether a document has children.
-        /// 
-        bool HasChildren(int id);
-
-        #endregion
-
-        #region Save, Delete Document
-
-        /// 
-        /// Saves a document.
-        /// 
-        OperationResult Save(IContent content, int? userId = null, ContentScheduleCollection? contentSchedule = null);
-
-        /// 
-        /// Saves documents.
-        /// 
-        // TODO: why only 1 result not 1 per content?!
-        OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes a document.
-        /// 
-        /// 
-        /// This method will also delete associated media files, child content and possibly associated domains.
-        /// This method entirely clears the content from the database.
-        /// 
-        OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes all documents of a given document type.
-        /// 
-        /// 
-        /// All non-deleted descendants of the deleted documents are moved to the recycle bin.
-        /// This operation is potentially dangerous and expensive.
-        /// 
-        void DeleteOfType(int documentTypeId, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes all documents of given document types.
-        /// 
-        /// 
-        /// All non-deleted descendants of the deleted documents are moved to the recycle bin.
-        /// This operation is potentially dangerous and expensive.
-        /// 
-        void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes versions of a document prior to a given date.
-        /// 
-        void DeleteVersions(int id, DateTime date, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Deletes a version of a document.
-        /// 
-        void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-        #region Move, Copy, Sort Document
-
-        /// 
-        /// Moves a document under a new parent.
-        /// 
-        void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Copies a document.
-        /// 
-        /// 
-        /// Recursively copies all children.
-        /// 
-        IContent? Copy(IContent content, int parentId, bool relateToOriginal, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Copies a document.
-        /// 
-        /// 
-        /// Optionally recursively copies all children.
-        /// 
-        IContent? Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Moves a document to the recycle bin.
-        /// 
-        OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Empties the Recycle Bin by deleting all  that resides in the bin
-        /// 
-        /// Optional Id of the User emptying the Recycle Bin
-        OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Returns true if there is any content in the recycle bin
-        /// 
-        bool RecycleBinSmells();
-
-        /// 
-        /// Sorts documents.
-        /// 
-        OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Sorts documents.
-        /// 
-        OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-        #region Publish Document
-
-        /// 
-        /// Saves and publishes a document.
-        /// 
-        /// 
-        /// By default, publishes all variations of the document, but it is possible to specify a culture to be published.
-        /// When a culture is being published, it includes all varying values along with all invariant values.
-        /// The document is *always* saved, even when publishing fails.
-        /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
-        /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty.
-        /// 
-        /// The document to publish.
-        /// The culture to publish.
-        /// The identifier of the user performing the action.
-        PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Saves and publishes a document.
-        /// 
-        /// 
-        /// By default, publishes all variations of the document, but it is possible to specify a culture to be published.
-        /// When a culture is being published, it includes all varying values along with all invariant values.
-        /// The document is *always* saved, even when publishing fails.
-        /// 
-        /// The document to publish.
-        /// The cultures to publish.
-        /// The identifier of the user performing the action.
-        PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Saves and publishes a document branch.
-        /// 
-        /// The root document.
-        /// A value indicating whether to force-publish documents that are not already published.
-        /// A culture, or "*" for all cultures.
-        /// The identifier of the user performing the operation.
-        /// 
-        /// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more
-        /// than one culture, see the other overloads of this method.
-        /// The  parameter determines which documents are published. When false,
-        /// only those documents that are already published, are republished. When true, all documents are
-        /// published. The root of the branch is always published, regardless of .
-        /// 
-        IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Saves and publishes a document branch.
-        /// 
-        /// The root document.
-        /// A value indicating whether to force-publish documents that are not already published.
-        /// The cultures to publish.
-        /// The identifier of the user performing the operation.
-        /// 
-        /// The  parameter determines which documents are published. When false,
-        /// only those documents that are already published, are republished. When true, all documents are
-        /// published. The root of the branch is always published, regardless of .
-        /// 
-        IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
-
-        ///// 
-        ///// Saves and publishes a document branch.
-        ///// 
-        ///// The root document.
-        ///// A value indicating whether to force-publish documents that are not already published.
-        ///// A function determining cultures to publish.
-        ///// A function publishing cultures.
-        ///// The identifier of the user performing the operation.
-        ///// 
-        ///// The  parameter determines which documents are published. When false,
-        ///// only those documents that are already published, are republished. When true, all documents are
-        ///// published. The root of the branch is always published, regardless of .
-        ///// The  parameter is a function which determines whether a document has
-        ///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
-        ///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
-        ///// cultures may trigger an unwanted republish.
-        ///// The  parameter is a function to execute to publish cultures, on
-        ///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
-        ///// whether the cultures could be published.
-        ///// 
-        //IEnumerable SaveAndPublishBranch(IContent content, bool force,
-        //    Func> shouldPublish,
-        //    Func, bool> publishCultures,
-        //    int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Unpublishes a document.
-        /// 
-        /// 
-        /// By default, unpublishes the document as a whole, but it is possible to specify a culture to be
-        /// unpublished. Depending on whether that culture is mandatory, and other cultures remain published,
-        /// the document as a whole may or may not remain published.
-        /// If the content type is variant, then culture can be either '*' or an actual culture, but neither null nor
-        /// empty. If the content type is invariant, then culture can be either '*' or null or empty.
-        /// 
-        PublishResult Unpublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Gets a value indicating whether a document is path-publishable.
-        /// 
-        /// A document is path-publishable when all its ancestors are published.
-        bool IsPathPublishable(IContent content);
-
-        /// 
-        /// Gets a value indicating whether a document is path-published.
-        /// 
-        /// A document is path-published when all its ancestors, and the document itself, are published.
-        bool IsPathPublished(IContent content);
-
-        /// 
-        /// Saves a document and raises the "sent to publication" events.
-        /// 
-        bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Publishes and unpublishes scheduled documents.
-        /// 
-        IEnumerable PerformScheduledPublish(DateTime date);
-
-        #endregion
-
-        #region Permissions
-
-        /// 
-        /// Gets permissions assigned to a document.
-        /// 
-        EntityPermissionCollection GetPermissions(IContent content);
-
-        /// 
-        /// Sets the permission of a document.
-        /// 
-        /// Replaces all permissions with the new set of permissions.
-        void SetPermissions(EntityPermissionSet permissionSet);
-
-        /// 
-        /// Assigns a permission to a document.
-        /// 
-        /// Adds the permission to existing permissions.
-        void SetPermission(IContent entity, char permission, IEnumerable groupIds);
-
-        #endregion
-
-        #region Create
-
-        /// 
-        /// Creates a document.
-        /// 
-        IContent Create(string name, Guid parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a document.
-        /// 
-        IContent Create(string name, int parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a document
-        /// 
-        IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates a document.
-        /// 
-        IContent Create(string name, IContent? parent, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates and saves a document.
-        /// 
-        IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Creates and saves a document.
-        /// 
-        IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-        #region Rollback
-
-        /// 
-        /// Rolls back the content to a specific version.
-        /// 
-        /// The id of the content node.
-        /// The version id to roll back to.
-        /// An optional culture to roll back.
-        /// The identifier of the user who is performing the roll back.
-        /// 
-        /// When no culture is specified, all cultures are rolled back.
-        /// 
-        OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId);
-
-        #endregion
-
-    }
+    /// The id of the content node.
+    /// The version id to roll back to.
+    /// An optional culture to roll back.
+    /// The identifier of the user who is performing the roll back.
+    /// 
+    ///     When no culture is specified, all cultures are rolled back.
+    /// 
+    OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId);
+
+    #endregion
+
+    #region Blueprints
+
+    /// 
+    ///     Gets a blueprint.
+    /// 
+    IContent? GetBlueprintById(int id);
+
+    /// 
+    ///     Gets a blueprint.
+    /// 
+    IContent? GetBlueprintById(Guid id);
+
+    /// 
+    ///     Gets blueprints for a content type.
+    /// 
+    IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeId);
+
+    /// 
+    ///     Saves a blueprint.
+    /// 
+    void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes a blueprint.
+    /// 
+    void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a new content item from a blueprint.
+    /// 
+    IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes blueprints for a content type.
+    /// 
+    void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes blueprints for content types.
+    /// 
+    void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId);
+
+    #endregion
+
+    #region Get, Count Documents
+
+    /// 
+    ///     Gets a document.
+    /// 
+    IContent? GetById(int id);
+
+    new
+
+    /// 
+    ///     Gets a document.
+    /// 
+    IContent? GetById(Guid key);
+
+    /// 
+    ///     Gets publish/unpublish schedule for a content node.
+    /// 
+    /// Id of the Content to load schedule for
+    /// 
+    ///     
+    /// 
+    ContentScheduleCollection GetContentScheduleByContentId(int contentId);
+
+    /// 
+    ///     Persists publish/unpublish schedule for a content node.
+    /// 
+    /// 
+    /// 
+    void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule);
+
+    /// 
+    ///     Gets documents.
+    /// 
+    IEnumerable GetByIds(IEnumerable ids);
+
+    /// 
+    ///     Gets documents.
+    /// 
+    IEnumerable GetByIds(IEnumerable ids);
+
+    /// 
+    ///     Gets documents at a given level.
+    /// 
+    IEnumerable GetByLevel(int level);
+
+    /// 
+    ///     Gets the parent of a document.
+    /// 
+    IContent? GetParent(int id);
+
+    /// 
+    ///     Gets the parent of a document.
+    /// 
+    IContent? GetParent(IContent content);
+
+    /// 
+    ///     Gets ancestor documents of a document.
+    /// 
+    IEnumerable GetAncestors(int id);
+
+    /// 
+    ///     Gets ancestor documents of a document.
+    /// 
+    IEnumerable GetAncestors(IContent content);
+
+    /// 
+    ///     Gets all versions of a document.
+    /// 
+    /// Versions are ordered with current first, then most recent first.
+    IEnumerable GetVersions(int id);
+
+    /// 
+    ///     Gets all versions of a document.
+    /// 
+    /// Versions are ordered with current first, then most recent first.
+    IEnumerable GetVersionsSlim(int id, int skip, int take);
+
+    /// 
+    ///     Gets top versions of a document.
+    /// 
+    /// Versions are ordered with current first, then most recent first.
+    IEnumerable GetVersionIds(int id, int topRows);
+
+    /// 
+    ///     Gets a version of a document.
+    /// 
+    IContent? GetVersion(int versionId);
+
+    /// 
+    ///     Gets root-level documents.
+    /// 
+    IEnumerable GetRootContent();
+
+    /// 
+    ///     Gets documents having an expiration date before (lower than, or equal to) a specified date.
+    /// 
+    /// An Enumerable list of  objects
+    /// 
+    ///     The content returned from this method may be culture variant, in which case the resulting
+    ///      should be queried
+    ///     for which culture(s) have been scheduled.
+    /// 
+    IEnumerable GetContentForExpiration(DateTime date);
+
+    /// 
+    ///     Gets documents having a release date before (lower than, or equal to) a specified date.
+    /// 
+    /// An Enumerable list of  objects
+    /// 
+    ///     The content returned from this method may be culture variant, in which case the resulting
+    ///      should be queried
+    ///     for which culture(s) have been scheduled.
+    /// 
+    IEnumerable GetContentForRelease(DateTime date);
+
+    /// 
+    ///     Gets documents in the recycle bin.
+    /// 
+    IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
+
+    /// 
+    ///     Gets child documents of a parent.
+    /// 
+    /// The parent identifier.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Query filter.
+    /// Ordering infos.
+    IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
+
+    /// 
+    ///     Gets descendant documents of a given parent.
+    /// 
+    /// The parent identifier.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Query filter.
+    /// Ordering infos.
+    IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
+
+    /// 
+    ///     Gets paged documents of a content
+    /// 
+    /// The page number.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Search text filter.
+    /// Ordering infos.
+    IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering? ordering = null);
+
+    /// 
+    ///     Gets paged documents for specified content types
+    /// 
+    /// The page number.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Search text filter.
+    /// Ordering infos.
+    IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering = null);
+
+    /// 
+    ///     Counts documents of a given document type.
+    /// 
+    int Count(string? documentTypeAlias = null);
+
+    /// 
+    ///     Counts published documents of a given document type.
+    /// 
+    int CountPublished(string? documentTypeAlias = null);
+
+    /// 
+    ///     Counts child documents of a given parent, of a given document type.
+    /// 
+    int CountChildren(int parentId, string? documentTypeAlias = null);
+
+    /// 
+    ///     Counts descendant documents of a given parent, of a given document type.
+    /// 
+    int CountDescendants(int parentId, string? documentTypeAlias = null);
+
+    /// 
+    ///     Gets a value indicating whether a document has children.
+    /// 
+    bool HasChildren(int id);
+
+    #endregion
+
+    #region Save, Delete Document
+
+    /// 
+    ///     Saves a document.
+    /// 
+    OperationResult Save(IContent content, int? userId = null, ContentScheduleCollection? contentSchedule = null);
+
+    /// 
+    ///     Saves documents.
+    /// 
+    // TODO: why only 1 result not 1 per content?!
+    OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes a document.
+    /// 
+    /// 
+    ///     This method will also delete associated media files, child content and possibly associated domains.
+    ///     This method entirely clears the content from the database.
+    /// 
+    OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes all documents of a given document type.
+    /// 
+    /// 
+    ///     All non-deleted descendants of the deleted documents are moved to the recycle bin.
+    ///     This operation is potentially dangerous and expensive.
+    /// 
+    void DeleteOfType(int documentTypeId, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes all documents of given document types.
+    /// 
+    /// 
+    ///     All non-deleted descendants of the deleted documents are moved to the recycle bin.
+    ///     This operation is potentially dangerous and expensive.
+    /// 
+    void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes versions of a document prior to a given date.
+    /// 
+    void DeleteVersions(int id, DateTime date, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes a version of a document.
+    /// 
+    void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId);
+
+    #endregion
+
+    #region Move, Copy, Sort Document
+
+    /// 
+    ///     Moves a document under a new parent.
+    /// 
+    void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Copies a document.
+    /// 
+    /// 
+    ///     Recursively copies all children.
+    /// 
+    IContent? Copy(IContent content, int parentId, bool relateToOriginal, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Copies a document.
+    /// 
+    /// 
+    ///     Optionally recursively copies all children.
+    /// 
+    IContent? Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Moves a document to the recycle bin.
+    /// 
+    OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Empties the Recycle Bin by deleting all  that resides in the bin
+    /// 
+    /// Optional Id of the User emptying the Recycle Bin
+    OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Returns true if there is any content in the recycle bin
+    /// 
+    bool RecycleBinSmells();
+
+    /// 
+    ///     Sorts documents.
+    /// 
+    OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Sorts documents.
+    /// 
+    OperationResult Sort(IEnumerable? ids, int userId = Constants.Security.SuperUserId);
+
+    #endregion
+
+    #region Publish Document
+
+    /// 
+    ///     Saves and publishes a document.
+    /// 
+    /// 
+    ///     
+    ///         By default, publishes all variations of the document, but it is possible to specify a culture to be
+    ///         published.
+    ///     
+    ///     When a culture is being published, it includes all varying values along with all invariant values.
+    ///     The document is *always* saved, even when publishing fails.
+    ///     
+    ///         If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
+    ///         'empty'. If the content type is invariant, then culture can be either '*' or null or empty.
+    ///     
+    /// 
+    /// The document to publish.
+    /// The culture to publish.
+    /// The identifier of the user performing the action.
+    PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Saves and publishes a document.
+    /// 
+    /// 
+    ///     
+    ///         By default, publishes all variations of the document, but it is possible to specify a culture to be
+    ///         published.
+    ///     
+    ///     When a culture is being published, it includes all varying values along with all invariant values.
+    ///     The document is *always* saved, even when publishing fails.
+    /// 
+    /// The document to publish.
+    /// The cultures to publish.
+    /// The identifier of the user performing the action.
+    PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Saves and publishes a document branch.
+    /// 
+    /// The root document.
+    /// A value indicating whether to force-publish documents that are not already published.
+    /// A culture, or "*" for all cultures.
+    /// The identifier of the user performing the operation.
+    /// 
+    ///     
+    ///         Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more
+    ///         than one culture, see the other overloads of this method.
+    ///     
+    ///     
+    ///         The  parameter determines which documents are published. When false,
+    ///         only those documents that are already published, are republished. When true, all documents are
+    ///         published. The root of the branch is always published, regardless of .
+    ///     
+    /// 
+    IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Saves and publishes a document branch.
+    /// 
+    /// The root document.
+    /// A value indicating whether to force-publish documents that are not already published.
+    /// The cultures to publish.
+    /// The identifier of the user performing the operation.
+    /// 
+    ///     
+    ///         The  parameter determines which documents are published. When false,
+    ///         only those documents that are already published, are republished. When true, all documents are
+    ///         published. The root of the branch is always published, regardless of .
+    ///     
+    /// 
+    IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
+
+    ///// 
+    ///// Saves and publishes a document branch.
+    ///// 
+    ///// The root document.
+    ///// A value indicating whether to force-publish documents that are not already published.
+    ///// A function determining cultures to publish.
+    ///// A function publishing cultures.
+    ///// The identifier of the user performing the operation.
+    ///// 
+    ///// The  parameter determines which documents are published. When false,
+    ///// only those documents that are already published, are republished. When true, all documents are
+    ///// published. The root of the branch is always published, regardless of .
+    ///// The  parameter is a function which determines whether a document has
+    ///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
+    ///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
+    ///// cultures may trigger an unwanted republish.
+    ///// The  parameter is a function to execute to publish cultures, on
+    ///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
+    ///// whether the cultures could be published.
+    ///// 
+    // IEnumerable SaveAndPublishBranch(IContent content, bool force,
+    //    Func> shouldPublish,
+    //    Func, bool> publishCultures,
+    //    int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Unpublishes a document.
+    /// 
+    /// 
+    ///     
+    ///         By default, unpublishes the document as a whole, but it is possible to specify a culture to be
+    ///         unpublished. Depending on whether that culture is mandatory, and other cultures remain published,
+    ///         the document as a whole may or may not remain published.
+    ///     
+    ///     
+    ///         If the content type is variant, then culture can be either '*' or an actual culture, but neither null nor
+    ///         empty. If the content type is invariant, then culture can be either '*' or null or empty.
+    ///     
+    /// 
+    PublishResult Unpublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Gets a value indicating whether a document is path-publishable.
+    /// 
+    /// A document is path-publishable when all its ancestors are published.
+    bool IsPathPublishable(IContent content);
+
+    /// 
+    ///     Gets a value indicating whether a document is path-published.
+    /// 
+    /// A document is path-published when all its ancestors, and the document itself, are published.
+    bool IsPathPublished(IContent content);
+
+    /// 
+    ///     Saves a document and raises the "sent to publication" events.
+    /// 
+    bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Publishes and unpublishes scheduled documents.
+    /// 
+    IEnumerable PerformScheduledPublish(DateTime date);
+
+    #endregion
+
+    #region Permissions
+
+    /// 
+    ///     Gets permissions assigned to a document.
+    /// 
+    EntityPermissionCollection GetPermissions(IContent content);
+
+    /// 
+    ///     Sets the permission of a document.
+    /// 
+    /// Replaces all permissions with the new set of permissions.
+    void SetPermissions(EntityPermissionSet permissionSet);
+
+    /// 
+    ///     Assigns a permission to a document.
+    /// 
+    /// Adds the permission to existing permissions.
+    void SetPermission(IContent entity, char permission, IEnumerable groupIds);
+
+    #endregion
+
+    #region Create
+
+    /// 
+    ///     Creates a document.
+    /// 
+    IContent Create(string name, Guid parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a document.
+    /// 
+    IContent Create(string name, int parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a document
+    /// 
+    IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates a document.
+    /// 
+    IContent Create(string name, IContent? parent, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates and saves a document.
+    /// 
+    IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Creates and saves a document.
+    /// 
+    IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = Constants.Security.SuperUserId);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/IContentServiceBase.cs b/src/Umbraco.Core/Services/IContentServiceBase.cs
index 1916fb49c4..1e07da7d8f 100644
--- a/src/Umbraco.Core/Services/IContentServiceBase.cs
+++ b/src/Umbraco.Core/Services/IContentServiceBase.cs
@@ -1,25 +1,23 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
-{
-    public interface IContentServiceBase : IContentServiceBase
-        where TItem: class, IContentBase
-    {
-        TItem? GetById(Guid key);
-        Attempt Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
-    }
+namespace Umbraco.Cms.Core.Services;
 
-    /// 
-    /// Placeholder for sharing logic between the content, media (and member) services
-    /// TODO: Start sharing the logic!
-    /// 
-    public interface IContentServiceBase : IService
-    {
-        /// 
-        /// Checks/fixes the data integrity of node paths/levels stored in the database
-        /// 
-        ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
-    }
+public interface IContentServiceBase : IContentServiceBase
+    where TItem : class, IContentBase
+{
+    TItem? GetById(Guid key);
+
+    Attempt Save(IEnumerable contents, int userId = Constants.Security.SuperUserId);
+}
+
+/// 
+///     Placeholder for sharing logic between the content, media (and member) services
+///     TODO: Start sharing the logic!
+/// 
+public interface IContentServiceBase : IService
+{
+    /// 
+    ///     Checks/fixes the data integrity of node paths/levels stored in the database
+    /// 
+    ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
 }
diff --git a/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs b/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs
index 4b6a78850c..be8cef8fd1 100644
--- a/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs
+++ b/src/Umbraco.Core/Services/IContentTypeBaseServiceProvider.cs
@@ -1,27 +1,30 @@
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Provides the  corresponding to an  object.
+/// 
+public interface IContentTypeBaseServiceProvider
 {
     /// 
-    /// Provides the  corresponding to an  object.
+    ///     Gets the content type service base managing types for the specified content base.
     /// 
-    public interface IContentTypeBaseServiceProvider
-    {
-        /// 
-        /// Gets the content type service base managing types for the specified content base.
-        /// 
-        /// 
-        /// If  is an , this returns the
-        /// , and if it's an , this returns
-        /// the , etc.
-        /// Services are returned as  and can be used
-        /// to retrieve the content / media / whatever type as .
-        /// 
-        IContentTypeBaseService For(IContentBase contentBase);
+    /// 
+    ///     
+    ///         If  is an , this returns the
+    ///         , and if it's an , this returns
+    ///         the , etc.
+    ///     
+    ///     
+    ///         Services are returned as  and can be used
+    ///         to retrieve the content / media / whatever type as .
+    ///     
+    /// 
+    IContentTypeBaseService For(IContentBase contentBase);
 
-        /// 
-        /// Gets the content type of an  object.
-        /// 
-        IContentTypeComposition? GetContentTypeOf(IContentBase contentBase);
-    }
+    /// 
+    ///     Gets the content type of an  object.
+    /// 
+    IContentTypeComposition? GetContentTypeOf(IContentBase contentBase);
 }
diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs
index 4b34baa869..d38139349b 100644
--- a/src/Umbraco.Core/Services/IContentTypeService.cs
+++ b/src/Umbraco.Core/Services/IContentTypeService.cs
@@ -1,35 +1,32 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Manages  objects.
+/// 
+public interface IContentTypeService : IContentTypeBaseService
 {
     /// 
-    /// Manages  objects.
+    ///     Gets all property type aliases.
     /// 
-    public interface IContentTypeService : IContentTypeBaseService
-    {
-        /// 
-        /// Gets all property type aliases.
-        /// 
-        /// 
-        IEnumerable GetAllPropertyTypeAliases();
+    /// 
+    IEnumerable GetAllPropertyTypeAliases();
 
-        /// 
-        /// Gets all content type aliases
-        /// 
-        /// 
-        /// If this list is empty, it will return all content type aliases for media, members and content, otherwise
-        /// it will only return content type aliases for the object types specified
-        /// 
-        /// 
-        IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes);
+    /// 
+    ///     Gets all content type aliases
+    /// 
+    /// 
+    ///     If this list is empty, it will return all content type aliases for media, members and content, otherwise
+    ///     it will only return content type aliases for the object types specified
+    /// 
+    /// 
+    IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes);
 
-        /// 
-        /// Returns all content type Ids for the aliases given
-        /// 
-        /// 
-        /// 
-        IEnumerable GetAllContentTypeIds(string[] aliases);
-    }
+    /// 
+    ///     Returns all content type Ids for the aliases given
+    /// 
+    /// 
+    /// 
+    IEnumerable GetAllContentTypeIds(string[] aliases);
 }
diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
index 5614d87bf3..8e67c78a20 100644
--- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
+++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
@@ -1,96 +1,112 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Provides a common base interface for .
+/// 
+public interface IContentTypeBaseService
 {
     /// 
-    /// Provides a common base interface for .
+    ///     Gets a content type.
     /// 
-    public interface IContentTypeBaseService
-    {
-        /// 
-        /// Gets a content type.
-        /// 
-        IContentTypeComposition? Get(int id);
-    }
+    IContentTypeComposition? Get(int id);
+}
+
+/// 
+///     Provides a common base interface for ,  and
+///     .
+/// 
+/// The type of the item.
+public interface IContentTypeBaseService : IContentTypeBaseService, IService
+    where TItem : IContentTypeComposition
+{
+    /// 
+    ///     Gets a content type.
+    /// 
+    new TItem? Get(int id);
 
     /// 
-    /// Provides a common base interface for ,  and .
+    ///     Gets a content type.
     /// 
-    /// The type of the item.
-    public interface IContentTypeBaseService : IContentTypeBaseService, IService
-        where TItem : IContentTypeComposition
-    {
-        /// 
-        /// Gets a content type.
-        /// 
-        new TItem? Get(int id);
+    TItem? Get(Guid key);
 
-        /// 
-        /// Gets a content type.
-        /// 
-        TItem? Get(Guid key);
+    /// 
+    ///     Gets a content type.
+    /// 
+    TItem? Get(string alias);
 
-        /// 
-        /// Gets a content type.
-        /// 
-        TItem? Get(string alias);
+    int Count();
 
-        int Count();
+    /// 
+    ///     Returns true or false depending on whether content nodes have been created based on the provided content type id.
+    /// 
+    bool HasContentNodes(int id);
 
-        /// 
-        /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
-        /// 
-        bool HasContentNodes(int id);
+    IEnumerable GetAll(params int[] ids);
 
-        IEnumerable GetAll(params int[] ids);
-        IEnumerable GetAll(IEnumerable? ids);
+    IEnumerable GetAll(IEnumerable? ids);
 
-        IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis
-        IEnumerable GetComposedOf(int id); // composition axis
+    IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis
 
-        IEnumerable GetChildren(int id);
-        IEnumerable GetChildren(Guid id);
+    IEnumerable GetComposedOf(int id); // composition axis
 
-        bool HasChildren(int id);
-        bool HasChildren(Guid id);
+    IEnumerable GetChildren(int id);
 
-        void Save(TItem? item, int userId = Constants.Security.SuperUserId);
-        void Save(IEnumerable items, int userId = Constants.Security.SuperUserId);
-        void Delete(TItem item, int userId = Constants.Security.SuperUserId);
-        void Delete(IEnumerable item, int userId = Constants.Security.SuperUserId);
+    IEnumerable GetChildren(Guid id);
 
+    bool HasChildren(int id);
 
-        Attempt ValidateComposition(TItem? compo);
+    bool HasChildren(Guid id);
 
-        /// 
-        /// Given the path of a content item, this will return true if the content item exists underneath a list view content item
-        /// 
-        /// 
-        /// 
-        bool HasContainerInPath(string contentPath);
+    void Save(TItem? item, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets a value indicating whether there is a list view content item in the path.
-        /// 
-        /// 
-        /// 
-        bool HasContainerInPath(params int[] ids);
+    void Save(IEnumerable items, int userId = Constants.Security.SuperUserId);
 
-        Attempt?> CreateContainer(int parentContainerId, Guid key, string name, int userId = Constants.Security.SuperUserId);
-        Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
-        EntityContainer? GetContainer(int containerId);
-        EntityContainer? GetContainer(Guid containerId);
-        IEnumerable GetContainers(int[] containerIds);
-        IEnumerable GetContainers(TItem contentType);
-        IEnumerable GetContainers(string folderName, int level);
-        Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
-        Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
+    void Delete(TItem item, int userId = Constants.Security.SuperUserId);
 
-        Attempt?> Move(TItem moving, int containerId);
-        Attempt?> Copy(TItem copying, int containerId);
-        TItem Copy(TItem original, string alias, string name, int parentId = -1);
-        TItem Copy(TItem original, string alias, string name, TItem parent);
-    }
+    void Delete(IEnumerable item, int userId = Constants.Security.SuperUserId);
+
+    Attempt ValidateComposition(TItem? compo);
+
+    /// 
+    ///     Given the path of a content item, this will return true if the content item exists underneath a list view content
+    ///     item
+    /// 
+    /// 
+    /// 
+    bool HasContainerInPath(string contentPath);
+
+    /// 
+    ///     Gets a value indicating whether there is a list view content item in the path.
+    /// 
+    /// 
+    /// 
+    bool HasContainerInPath(params int[] ids);
+
+    Attempt?> CreateContainer(int parentContainerId, Guid key, string name, int userId = Constants.Security.SuperUserId);
+
+    Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
+
+    EntityContainer? GetContainer(int containerId);
+
+    EntityContainer? GetContainer(Guid containerId);
+
+    IEnumerable GetContainers(int[] containerIds);
+
+    IEnumerable GetContainers(TItem contentType);
+
+    IEnumerable GetContainers(string folderName, int level);
+
+    Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
+
+    Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
+
+    Attempt?> Move(TItem moving, int containerId);
+
+    Attempt?> Copy(TItem copying, int containerId);
+
+    TItem Copy(TItem original, string alias, string name, int parentId = -1);
+
+    TItem Copy(TItem original, string alias, string name, TItem parent);
 }
diff --git a/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs b/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
index 86e2988307..d9cbcc0cda 100644
--- a/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
+++ b/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
@@ -1,17 +1,14 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Used to filter historic content versions for cleanup.
+/// 
+public interface IContentVersionCleanupPolicy
 {
     /// 
-    /// Used to filter historic content versions for cleanup.
+    ///     Filters a set of candidates historic content versions for cleanup according to policy settings.
     /// 
-    public interface IContentVersionCleanupPolicy
-    {
-        /// 
-        /// Filters a set of candidates historic content versions for cleanup according to policy settings.
-        /// 
-        IEnumerable Apply(DateTime asAtDate, IEnumerable items);
-    }
+    IEnumerable Apply(DateTime asAtDate, IEnumerable items);
 }
diff --git a/src/Umbraco.Core/Services/IContentVersionService.cs b/src/Umbraco.Core/Services/IContentVersionService.cs
index d0f203b2ef..e0d518f52a 100644
--- a/src/Umbraco.Core/Services/IContentVersionService.cs
+++ b/src/Umbraco.Core/Services/IContentVersionService.cs
@@ -1,25 +1,22 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IContentVersionService
 {
-    public interface IContentVersionService
-    {
-        /// 
-        /// Removes historic content versions according to a policy.
-        /// 
-        IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate);
+    /// 
+    ///     Removes historic content versions according to a policy.
+    /// 
+    IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate);
 
-        /// 
-        /// Gets paginated content versions for given content id paginated.
-        /// 
-        /// Thrown when  is invalid.
-        IEnumerable? GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string? culture = null);
+    /// 
+    ///     Gets paginated content versions for given content id paginated.
+    /// 
+    /// Thrown when  is invalid.
+    IEnumerable? GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string? culture = null);
 
-        /// 
-        /// Updates preventCleanup value for given content version.
-        /// 
-        void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1);
-    }
+    /// 
+    ///     Updates preventCleanup value for given content version.
+    /// 
+    void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1);
 }
diff --git a/src/Umbraco.Core/Services/IDashboardService.cs b/src/Umbraco.Core/Services/IDashboardService.cs
index 70e3410627..2792b142fe 100644
--- a/src/Umbraco.Core/Services/IDashboardService.cs
+++ b/src/Umbraco.Core/Services/IDashboardService.cs
@@ -1,27 +1,24 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Dashboards;
 using Umbraco.Cms.Core.Models.ContentEditing;
 using Umbraco.Cms.Core.Models.Membership;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IDashboardService
 {
-    public interface IDashboardService
-    {
-        /// 
-        /// Gets dashboard for a specific section/application
-        /// For a specific backoffice user
-        /// 
-        /// 
-        /// 
-        /// 
-        IEnumerable> GetDashboards(string section, IUser? currentUser);
+    /// 
+    ///     Gets dashboard for a specific section/application
+    ///     For a specific backoffice user
+    /// 
+    /// 
+    /// 
+    /// 
+    IEnumerable> GetDashboards(string section, IUser? currentUser);
 
-        /// 
-        /// Gets all dashboards, organized by section, for a user.
-        /// 
-        /// 
-        /// 
-        IDictionary>> GetDashboards(IUser? currentUser);
-
-    }
+    /// 
+    ///     Gets all dashboards, organized by section, for a user.
+    /// 
+    /// 
+    /// 
+    IDictionary>> GetDashboards(IUser? currentUser);
 }
diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs
index 898b24355e..effb4573b4 100644
--- a/src/Umbraco.Core/Services/IDataTypeService.cs
+++ b/src/Umbraco.Core/Services/IDataTypeService.cs
@@ -1,92 +1,103 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the DataType Service, which is an easy access to operations involving 
+/// 
+public interface IDataTypeService : IService
 {
+    /// 
+    ///     Returns a dictionary of content type s and the property type aliases that use a
+    ///     
+    /// 
+    /// 
+    /// 
+    IReadOnlyDictionary> GetReferences(int id);
+
+    Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId);
+
+    Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
+
+    EntityContainer? GetContainer(int containerId);
+
+    EntityContainer? GetContainer(Guid containerId);
+
+    IEnumerable GetContainers(string folderName, int level);
+
+    IEnumerable GetContainers(IDataType dataType);
+
+    IEnumerable GetContainers(int[] containerIds);
+
+    Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
+
+    Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
 
     /// 
-    /// Defines the DataType Service, which is an easy access to operations involving 
+    ///     Gets a  by its Name
     /// 
-    public interface IDataTypeService : IService
-    {
-        /// 
-        /// Returns a dictionary of content type s and the property type aliases that use a 
-        /// 
-        /// 
-        /// 
-        IReadOnlyDictionary> GetReferences(int id);
+    /// Name of the 
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(string name);
 
-        Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId);
-        Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
-        EntityContainer? GetContainer(int containerId);
-        EntityContainer? GetContainer(Guid containerId);
-        IEnumerable GetContainers(string folderName, int level);
-        IEnumerable GetContainers(IDataType dataType);
-        IEnumerable GetContainers(int[] containerIds);
-        Attempt DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
-        Attempt?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(int id);
 
-        /// 
-        /// Gets a  by its Name
-        /// 
-        /// Name of the 
-        /// 
-        IDataType? GetDataType(string name);
+    /// 
+    ///     Gets a  by its unique guid Id
+    /// 
+    /// Unique guid Id of the DataType
+    /// 
+    ///     
+    /// 
+    IDataType? GetDataType(Guid id);
 
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// 
-        IDataType? GetDataType(int id);
+    /// 
+    ///     Gets all  objects or those with the ids passed in
+    /// 
+    /// Optional array of Ids
+    /// An enumerable list of  objects
+    IEnumerable GetAll(params int[] ids);
 
-        /// 
-        /// Gets a  by its unique guid Id
-        /// 
-        /// Unique guid Id of the DataType
-        /// 
-        IDataType? GetDataType(Guid id);
+    /// 
+    ///     Saves an 
+    /// 
+    ///  to save
+    /// Id of the user issuing the save
+    void Save(IDataType dataType, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets all  objects or those with the ids passed in
-        /// 
-        /// Optional array of Ids
-        /// An enumerable list of  objects
-        IEnumerable GetAll(params int[] ids);
+    /// 
+    ///     Saves a collection of 
+    /// 
+    ///  to save
+    /// Id of the user issuing the save
+    void Save(IEnumerable dataTypeDefinitions, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves an 
-        /// 
-        ///  to save
-        /// Id of the user issuing the save
-        void Save(IDataType dataType, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes an 
+    /// 
+    /// 
+    ///     Please note that deleting a  will remove
+    ///     all the  data that references this .
+    /// 
+    ///  to delete
+    /// Id of the user issuing the deletion
+    void Delete(IDataType dataType, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves a collection of 
-        /// 
-        ///  to save
-        /// Id of the user issuing the save
-        void Save(IEnumerable dataTypeDefinitions, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  by its control Id
+    /// 
+    /// Alias of the property editor
+    /// Collection of  objects with a matching control id
+    IEnumerable GetByEditorAlias(string propertyEditorAlias);
 
-        /// 
-        /// Deletes an 
-        /// 
-        /// 
-        /// Please note that deleting a  will remove
-        /// all the  data that references this .
-        /// 
-        ///  to delete
-        /// Id of the user issuing the deletion
-        void Delete(IDataType dataType, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Gets a  by its control Id
-        /// 
-        /// Alias of the property editor
-        /// Collection of  objects with a matching control id
-        IEnumerable GetByEditorAlias(string propertyEditorAlias);
-
-        Attempt?> Move(IDataType toMove, int parentId);
-    }
+    Attempt?> Move(IDataType toMove, int parentId);
 }
diff --git a/src/Umbraco.Core/Services/IDomainService.cs b/src/Umbraco.Core/Services/IDomainService.cs
index 952eaecfde..54a006ecb1 100644
--- a/src/Umbraco.Core/Services/IDomainService.cs
+++ b/src/Umbraco.Core/Services/IDomainService.cs
@@ -1,16 +1,20 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IDomainService : IService
 {
-    public interface IDomainService : IService
-    {
-        bool Exists(string domainName);
-        Attempt Delete(IDomain domain);
-        IDomain? GetByName(string name);
-        IDomain? GetById(int id);
-        IEnumerable GetAll(bool includeWildcards);
-        IEnumerable GetAssignedDomains(int contentId, bool includeWildcards);
-        Attempt Save(IDomain domainEntity);
-    }
+    bool Exists(string domainName);
+
+    Attempt Delete(IDomain domain);
+
+    IDomain? GetByName(string name);
+
+    IDomain? GetById(int id);
+
+    IEnumerable GetAll(bool includeWildcards);
+
+    IEnumerable GetAssignedDomains(int contentId, bool includeWildcards);
+
+    Attempt Save(IDomain domainEntity);
 }
diff --git a/src/Umbraco.Core/Services/IEditorConfigurationParser.cs b/src/Umbraco.Core/Services/IEditorConfigurationParser.cs
index 8dc1210d11..1a37045490 100644
--- a/src/Umbraco.Core/Services/IEditorConfigurationParser.cs
+++ b/src/Umbraco.Core/Services/IEditorConfigurationParser.cs
@@ -1,11 +1,12 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.PropertyEditors;
 
 namespace Umbraco.Cms.Core.Services;
 
 public interface IEditorConfigurationParser
 {
-    TConfiguration? ParseFromConfigurationEditor(IDictionary? editorValues, IEnumerable fields);
+    TConfiguration? ParseFromConfigurationEditor(
+        IDictionary? editorValues,
+        IEnumerable fields);
 
     Dictionary ParseToConfigurationEditor(TConfiguration? configuration);
 }
diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs
index 66298aba1d..74a416a8fe 100644
--- a/src/Umbraco.Core/Services/IEntityService.cs
+++ b/src/Umbraco.Core/Services/IEntityService.cs
@@ -1,252 +1,279 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IEntityService
 {
-    public interface IEntityService
-    {
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The identifier of the entity.
-        IEntitySlim? Get(int id);
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The identifier of the entity.
+    IEntitySlim? Get(int id);
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The unique key of the entity.
-        IEntitySlim? Get(Guid key);
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The unique key of the entity.
+    IEntitySlim? Get(Guid key);
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The identifier of the entity.
-        /// The object type of the entity.
-        IEntitySlim? Get(int id, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The identifier of the entity.
+    /// The object type of the entity.
+    IEntitySlim? Get(int id, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The unique key of the entity.
-        /// The object type of the entity.
-        IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The unique key of the entity.
+    /// The object type of the entity.
+    IEntitySlim? Get(Guid key, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The type used to determine the object type of the entity.
-        /// The identifier of the entity.
-        IEntitySlim? Get(int id) where T : IUmbracoEntity;
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The type used to determine the object type of the entity.
+    /// The identifier of the entity.
+    IEntitySlim? Get(int id)
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Gets an entity.
-        /// 
-        /// The type used to determine the object type of the entity.
-        /// The unique key of the entity.
-        IEntitySlim? Get(Guid key) where T : IUmbracoEntity;
+    /// 
+    ///     Gets an entity.
+    /// 
+    /// The type used to determine the object type of the entity.
+    /// The unique key of the entity.
+    IEntitySlim? Get(Guid key)
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Determines whether an entity exists.
-        /// 
-        /// The identifier of the entity.
-        bool Exists(int id);
+    /// 
+    ///     Determines whether an entity exists.
+    /// 
+    /// The identifier of the entity.
+    bool Exists(int id);
 
-        /// 
-        /// Determines whether an entity exists.
-        /// 
-        /// The unique key of the entity.
-        bool Exists(Guid key);
+    /// 
+    ///     Determines whether an entity exists.
+    /// 
+    /// The unique key of the entity.
+    bool Exists(Guid key);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The type used to determine the object type of the entities.
-        IEnumerable GetAll() where T : IUmbracoEntity;
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The type used to determine the object type of the entities.
+    IEnumerable GetAll()
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The type used to determine the object type of the entities.
-        /// The identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(params int[] ids) where T : IUmbracoEntity;
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The type used to determine the object type of the entities.
+    /// The identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(params int[] ids)
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        IEnumerable GetAll(UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    IEnumerable GetAll(UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        /// The identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    /// The identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        IEnumerable GetAll(Guid objectType);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    IEnumerable GetAll(Guid objectType);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        /// The identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(Guid objectType, params int[] ids);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    /// The identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(Guid objectType, params int[] ids);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The type used to determine the object type of the entities.
-        /// The unique identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(params Guid[] keys) where T : IUmbracoEntity;
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The type used to determine the object type of the entities.
+    /// The unique identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(params Guid[] keys)
+        where T : IUmbracoEntity;
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        /// The unique identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    /// The unique identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(UmbracoObjectTypes objectType, Guid[] keys);
 
-        /// 
-        /// Gets entities of a given object type.
-        /// 
-        /// The object type of the entities.
-        /// The unique identifiers of the entities.
-        /// If  is empty, returns all entities.
-        IEnumerable GetAll(Guid objectType, params Guid[] keys);
+    /// 
+    ///     Gets entities of a given object type.
+    /// 
+    /// The object type of the entities.
+    /// The unique identifiers of the entities.
+    /// If  is empty, returns all entities.
+    IEnumerable GetAll(Guid objectType, params Guid[] keys);
 
-        /// 
-        /// Gets entities at root.
-        /// 
-        /// The object type of the entities.
-        IEnumerable GetRootEntities(UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets entities at root.
+    /// 
+    /// The object type of the entities.
+    IEnumerable GetRootEntities(UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets the parent of an entity.
-        /// 
-        /// The identifier of the entity.
-        IEntitySlim? GetParent(int id);
+    /// 
+    ///     Gets the parent of an entity.
+    /// 
+    /// The identifier of the entity.
+    IEntitySlim? GetParent(int id);
 
-        /// 
-        /// Gets the parent of an entity.
-        /// 
-        /// The identifier of the entity.
-        /// The object type of the parent.
-        IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets the parent of an entity.
+    /// 
+    /// The identifier of the entity.
+    /// The object type of the parent.
+    IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets the children of an entity.
-        /// 
-        /// The identifier of the entity.
-        IEnumerable GetChildren(int id);
+    /// 
+    ///     Gets the children of an entity.
+    /// 
+    /// The identifier of the entity.
+    IEnumerable GetChildren(int id);
 
-        /// 
-        /// Gets the children of an entity.
-        /// 
-        /// The identifier of the entity.
-        /// The object type of the children.
-        IEnumerable GetChildren(int id, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets the children of an entity.
+    /// 
+    /// The identifier of the entity.
+    /// The object type of the children.
+    IEnumerable GetChildren(int id, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets the descendants of an entity.
-        /// 
-        /// The identifier of the entity.
-        IEnumerable GetDescendants(int id);
+    /// 
+    ///     Gets the descendants of an entity.
+    /// 
+    /// The identifier of the entity.
+    IEnumerable GetDescendants(int id);
 
-        /// 
-        /// Gets the descendants of an entity.
-        /// 
-        /// The identifier of the entity.
-        /// The object type of the descendants.
-        IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets the descendants of an entity.
+    /// 
+    /// The identifier of the entity.
+    /// The object type of the descendants.
+    IEnumerable GetDescendants(int id, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets children of an entity.
-        /// 
-        IEnumerable GetPagedChildren(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets children of an entity.
+    /// 
+    IEnumerable GetPagedChildren(
+        int id,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        /// 
-        /// Gets descendants of an entity.
-        /// 
-        IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets descendants of an entity.
+    /// 
+    IEnumerable GetPagedDescendants(
+        int id,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        /// 
-        /// Gets descendants of entities.
-        /// 
-        IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets descendants of entities.
+    /// 
+    IEnumerable GetPagedDescendants(
+        IEnumerable ids,
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        // TODO: Do we really need this? why not just pass in -1
-        /// 
-        /// Gets descendants of root.
-        /// 
-        IEnumerable GetPagedDescendants(UmbracoObjectTypes objectType, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null, bool includeTrashed = true);
+    // TODO: Do we really need this? why not just pass in -1
 
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(int id);
+    /// 
+    ///     Gets descendants of root.
+    /// 
+    IEnumerable GetPagedDescendants(
+        UmbracoObjectTypes objectType,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null,
+        bool includeTrashed = true);
 
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(Guid key);
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(int id);
 
-        /// 
-        /// Gets the object type of an entity.
-        /// 
-        UmbracoObjectTypes GetObjectType(IUmbracoEntity entity);
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(Guid key);
 
-        /// 
-        /// Gets the CLR type of an entity.
-        /// 
-        Type? GetEntityType(int id);
+    /// 
+    ///     Gets the object type of an entity.
+    /// 
+    UmbracoObjectTypes GetObjectType(IUmbracoEntity entity);
 
-        /// 
-        /// Gets the integer identifier corresponding to a unique Guid identifier.
-        /// 
-        Attempt GetId(Guid key, UmbracoObjectTypes objectType);
+    /// 
+    ///     Gets the CLR type of an entity.
+    /// 
+    Type? GetEntityType(int id);
 
-        /// 
-        /// Gets the integer identifier corresponding to a Udi.
-        /// 
-        Attempt GetId(Udi udi);
+    /// 
+    ///     Gets the integer identifier corresponding to a unique Guid identifier.
+    /// 
+    Attempt GetId(Guid key, UmbracoObjectTypes objectType);
 
-        /// 
-        /// Gets the unique Guid identifier corresponding to an integer identifier.
-        /// 
-        Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType);
+    /// 
+    ///     Gets the integer identifier corresponding to a Udi.
+    /// 
+    Attempt GetId(Udi udi);
 
-        /// 
-        /// Gets paths for entities.
-        /// 
-        IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids);
+    /// 
+    ///     Gets the unique Guid identifier corresponding to an integer identifier.
+    /// 
+    Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType);
 
-        /// 
-        /// Gets paths for entities.
-        /// 
-        IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys);
+    /// 
+    ///     Gets paths for entities.
+    /// 
+    IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids);
 
-        /// 
-        /// Reserves an identifier for a key.
-        /// 
-        /// They key.
-        /// The identifier.
-        /// When a new content or a media is saved with the key, it will have the reserved identifier.
-        int ReserveId(Guid key);
-    }
+    /// 
+    ///     Gets paths for entities.
+    /// 
+    IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys);
+
+    /// 
+    ///     Reserves an identifier for a key.
+    /// 
+    /// They key.
+    /// The identifier.
+    /// When a new content or a media is saved with the key, it will have the reserved identifier.
+    int ReserveId(Guid key);
 }
diff --git a/src/Umbraco.Core/Services/IEntityXmlSerializer.cs b/src/Umbraco.Core/Services/IEntityXmlSerializer.cs
index fd68a9dfca..5ada7ab5b6 100644
--- a/src/Umbraco.Core/Services/IEntityXmlSerializer.cs
+++ b/src/Umbraco.Core/Services/IEntityXmlSerializer.cs
@@ -1,90 +1,90 @@
-using System;
-using System.Collections.Generic;
 using System.Xml.Linq;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Serializes entities to XML
+/// 
+public interface IEntityXmlSerializer
 {
     /// 
-    /// Serializes entities to XML
+    ///     Exports an IContent item as an XElement.
     /// 
-    public interface IEntityXmlSerializer
-    {
-        /// 
-        /// Exports an IContent item as an XElement.
-        /// 
-        XElement Serialize(IContent content,
-                bool published,
-                bool withDescendants = false) // TODO: take care of usage! only used for the packager
-            ;
+    XElement Serialize(
+        IContent content,
+        bool published,
+        bool withDescendants = false) // TODO: take care of usage! only used for the packager
+        ;
 
-        /// 
-        /// Exports an IMedia item as an XElement.
-        /// 
-        XElement Serialize(
-            IMedia media,
-            bool withDescendants = false,
-            Action? onMediaItemSerialized = null);
+    /// 
+    ///     Exports an IMedia item as an XElement.
+    /// 
+    XElement Serialize(
+        IMedia media,
+        bool withDescendants = false,
+        Action? onMediaItemSerialized = null);
 
-        /// 
-        /// Exports an IMember item as an XElement.
-        /// 
-        XElement Serialize(IMember member);
+    /// 
+    ///     Exports an IMember item as an XElement.
+    /// 
+    XElement Serialize(IMember member);
 
-        /// 
-        /// Exports a list of Data Types
-        /// 
-        /// List of data types to export
-        ///  containing the xml representation of the IDataTypeDefinition objects
-        XElement Serialize(IEnumerable dataTypeDefinitions);
+    /// 
+    ///     Exports a list of Data Types
+    /// 
+    /// List of data types to export
+    ///  containing the xml representation of the IDataTypeDefinition objects
+    XElement Serialize(IEnumerable dataTypeDefinitions);
 
-        XElement Serialize(IDataType dataType);
+    XElement Serialize(IDataType dataType);
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// List of dictionary items to export
-        /// Optional boolean indicating whether or not to include children
-        ///  containing the xml representation of the IDictionaryItem objects
-        XElement Serialize(IEnumerable dictionaryItem, bool includeChildren = true);
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// List of dictionary items to export
+    /// Optional boolean indicating whether or not to include children
+    ///  containing the xml representation of the IDictionaryItem objects
+    XElement Serialize(IEnumerable dictionaryItem, bool includeChildren = true);
 
-        /// 
-        /// Exports a single  item to xml as an 
-        /// 
-        /// Dictionary Item to export
-        /// Optional boolean indicating whether or not to include children
-        ///  containing the xml representation of the IDictionaryItem object
-        XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren);
+    /// 
+    ///     Exports a single  item to xml as an 
+    /// 
+    /// Dictionary Item to export
+    /// Optional boolean indicating whether or not to include children
+    ///  containing the xml representation of the IDictionaryItem object
+    XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren);
 
-        XElement Serialize(IStylesheet stylesheet, bool includeProperties);
+    XElement Serialize(IStylesheet stylesheet, bool includeProperties);
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// List of Languages to export
-        ///  containing the xml representation of the ILanguage objects
-        XElement Serialize(IEnumerable languages);
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// List of Languages to export
+    ///  containing the xml representation of the ILanguage objects
+    XElement Serialize(IEnumerable languages);
 
-        XElement Serialize(ILanguage language);
-        XElement Serialize(ITemplate template);
+    XElement Serialize(ILanguage language);
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// 
-        /// 
-        XElement Serialize(IEnumerable templates);
+    XElement Serialize(ITemplate template);
 
-        XElement Serialize(IMediaType mediaType);
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// 
+    /// 
+    XElement Serialize(IEnumerable templates);
 
-        /// 
-        /// Exports a list of  items to xml as an 
-        /// 
-        /// Macros to export
-        ///  containing the xml representation of the IMacro objects
-        XElement Serialize(IEnumerable macros);
+    XElement Serialize(IMediaType mediaType);
 
-        XElement Serialize(IMacro macro);
-        XElement Serialize(IContentType contentType);
-    }
+    /// 
+    ///     Exports a list of  items to xml as an 
+    /// 
+    /// Macros to export
+    ///  containing the xml representation of the IMacro objects
+    XElement Serialize(IEnumerable macros);
+
+    XElement Serialize(IMacro macro);
+
+    XElement Serialize(IContentType contentType);
 }
diff --git a/src/Umbraco.Core/Services/IExamineIndexCountService.cs b/src/Umbraco.Core/Services/IExamineIndexCountService.cs
index 05c5f7d554..8d85e17e04 100644
--- a/src/Umbraco.Core/Services/IExamineIndexCountService.cs
+++ b/src/Umbraco.Core/Services/IExamineIndexCountService.cs
@@ -1,7 +1,6 @@
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IExamineIndexCountService
 {
-    public interface IExamineIndexCountService
-    {
-        public int GetCount();
-    }
+    public int GetCount();
 }
diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs
index 75f8069f0c..ba75d505ff 100644
--- a/src/Umbraco.Core/Services/IExternalLoginService.cs
+++ b/src/Umbraco.Core/Services/IExternalLoginService.cs
@@ -1,66 +1,63 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Security;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Used to store the external login info
+/// 
+[Obsolete("Use IExternalLoginServiceWithKey. This will be removed in Umbraco 10")]
+public interface IExternalLoginService : IService
 {
     /// 
-    /// Used to store the external login info
+    ///     Returns all user logins assigned
     /// 
-    [Obsolete("Use IExternalLoginServiceWithKey. This will be removed in Umbraco 10")]
-    public interface IExternalLoginService : IService
-    {
-        /// 
-        /// Returns all user logins assigned
-        /// 
-        /// 
-        /// 
-        IEnumerable GetExternalLogins(int userId);
+    /// 
+    /// 
+    IEnumerable GetExternalLogins(int userId);
 
-        /// 
-        /// Returns all user login tokens assigned
-        /// 
-        /// 
-        /// 
-        IEnumerable GetExternalLoginTokens(int userId);
+    /// 
+    ///     Returns all user login tokens assigned
+    /// 
+    /// 
+    /// 
+    IEnumerable GetExternalLoginTokens(int userId);
 
-        /// 
-        /// Returns all logins matching the login info - generally there should only be one but in some cases
-        /// there might be more than one depending on if an administrator has been editing/removing members
-        /// 
-        /// 
-        /// 
-        /// 
-        IEnumerable Find(string loginProvider, string providerKey);
+    /// 
+    ///     Returns all logins matching the login info - generally there should only be one but in some cases
+    ///     there might be more than one depending on if an administrator has been editing/removing members
+    /// 
+    /// 
+    /// 
+    /// 
+    IEnumerable Find(string loginProvider, string providerKey);
 
-        /// 
-        /// Saves the external logins associated with the user
-        /// 
-        /// 
-        /// The user associated with the logins
-        /// 
-        /// 
-        /// 
-        /// This will replace all external login provider information for the user
-        /// 
-        void Save(int userId, IEnumerable logins);
+    /// 
+    ///     Saves the external logins associated with the user
+    /// 
+    /// 
+    ///     The user associated with the logins
+    /// 
+    /// 
+    /// 
+    ///     This will replace all external login provider information for the user
+    /// 
+    void Save(int userId, IEnumerable logins);
 
-        /// 
-        /// Saves the external login tokens associated with the user
-        /// 
-        /// 
-        /// The user associated with the tokens
-        /// 
-        /// 
-        /// 
-        /// This will replace all external login tokens for the user
-        /// 
-        void Save(int userId, IEnumerable tokens);
+    /// 
+    ///     Saves the external login tokens associated with the user
+    /// 
+    /// 
+    ///     The user associated with the tokens
+    /// 
+    /// 
+    /// 
+    ///     This will replace all external login tokens for the user
+    /// 
+    void Save(int userId, IEnumerable tokens);
 
-        /// 
-        /// Deletes all user logins - normally used when a member is deleted
-        /// 
-        /// 
-        void DeleteUserLogins(int userId);
-    }
+    /// 
+    ///     Deletes all user logins - normally used when a member is deleted
+    /// 
+    /// 
+    void DeleteUserLogins(int userId);
 }
diff --git a/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
index bc31f54f8b..54f827c899 100644
--- a/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
+++ b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
@@ -1,54 +1,51 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Security;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IExternalLoginWithKeyService : IService
 {
-    public interface IExternalLoginWithKeyService : IService
-    {
-        /// 
-        /// Returns all user logins assigned
-        /// 
-        IEnumerable GetExternalLogins(Guid userOrMemberKey);
+    /// 
+    ///     Returns all user logins assigned
+    /// 
+    IEnumerable GetExternalLogins(Guid userOrMemberKey);
 
-        /// 
-        /// Returns all user login tokens assigned
-        /// 
-        IEnumerable GetExternalLoginTokens(Guid userOrMemberKey);
+    /// 
+    ///     Returns all user login tokens assigned
+    /// 
+    IEnumerable GetExternalLoginTokens(Guid userOrMemberKey);
 
-        /// 
-        /// Returns all logins matching the login info - generally there should only be one but in some cases
-        /// there might be more than one depending on if an administrator has been editing/removing members
-        /// 
-        IEnumerable Find(string loginProvider, string providerKey);
+    /// 
+    ///     Returns all logins matching the login info - generally there should only be one but in some cases
+    ///     there might be more than one depending on if an administrator has been editing/removing members
+    /// 
+    IEnumerable Find(string loginProvider, string providerKey);
 
-        /// 
-        /// Saves the external logins associated with the user
-        /// 
-        /// 
-        /// The user or member key associated with the logins
-        /// 
-        /// 
-        /// 
-        /// This will replace all external login provider information for the user
-        /// 
-        void Save(Guid userOrMemberKey, IEnumerable logins);
+    /// 
+    ///     Saves the external logins associated with the user
+    /// 
+    /// 
+    ///     The user or member key associated with the logins
+    /// 
+    /// 
+    /// 
+    ///     This will replace all external login provider information for the user
+    /// 
+    void Save(Guid userOrMemberKey, IEnumerable logins);
 
-        /// 
-        /// Saves the external login tokens associated with the user
-        /// 
-        /// 
-        /// The user or member key associated with the logins
-        /// 
-        /// 
-        /// 
-        /// This will replace all external login tokens for the user
-        /// 
-        void Save(Guid userOrMemberKey,IEnumerable tokens);
+    /// 
+    ///     Saves the external login tokens associated with the user
+    /// 
+    /// 
+    ///     The user or member key associated with the logins
+    /// 
+    /// 
+    /// 
+    ///     This will replace all external login tokens for the user
+    /// 
+    void Save(Guid userOrMemberKey, IEnumerable tokens);
 
-        /// 
-        /// Deletes all user logins - normally used when a member is deleted
-        /// 
-        void DeleteUserLogins(Guid userOrMemberKey);
-    }
+    /// 
+    ///     Deletes all user logins - normally used when a member is deleted
+    /// 
+    void DeleteUserLogins(Guid userOrMemberKey);
 }
diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs
index baccd5dedf..3179a4491f 100644
--- a/src/Umbraco.Core/Services/IFileService.cs
+++ b/src/Umbraco.Core/Services/IFileService.cs
@@ -1,307 +1,319 @@
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the File Service, which is an easy access to operations involving  objects like
+///     Scripts, Stylesheets and Templates
+/// 
+public interface IFileService : IService
 {
+    [Obsolete("Please use SnippetCollection.GetPartialViewSnippetNames() or SnippetCollection.GetPartialViewMacroSnippetNames() instead. Scheduled for removal in V12.")]IEnumerable GetPartialViewSnippetNames(params string[] filterNames);
+
+    void CreatePartialViewFolder(string folderPath);
+
+    void CreatePartialViewMacroFolder(string folderPath);
+
+    void DeletePartialViewFolder(string folderPath);
+
+    void DeletePartialViewMacroFolder(string folderPath);
+
     /// 
-    /// Defines the File Service, which is an easy access to operations involving  objects like Scripts, Stylesheets and Templates
+    ///     Gets a list of all  objects
     /// 
-    public interface IFileService : IService
-    {
-        [Obsolete("Please use SnippetCollection.GetPartialViewSnippetNames() or SnippetCollection.GetPartialViewMacroSnippetNames() instead. Scheduled for removal in V12.")]
-        IEnumerable GetPartialViewSnippetNames(params string[] filterNames);
-        void CreatePartialViewFolder(string folderPath);
-        void CreatePartialViewMacroFolder(string folderPath);
-        void DeletePartialViewFolder(string folderPath);
-        void DeletePartialViewMacroFolder(string folderPath);
+    /// An enumerable list of  objects
+    IEnumerable GetPartialViews(params string[] names);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetPartialViews(params string[] names);
+    IPartialView? GetPartialView(string path);
 
-        IPartialView? GetPartialView(string path);
-        IPartialView? GetPartialViewMacro(string path);
-        Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId);
-        Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId);
-        bool DeletePartialView(string path, int? userId = null);
-        bool DeletePartialViewMacro(string path, int? userId = null);
-        Attempt SavePartialView(IPartialView partialView, int? userId = null);
-        Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null);
+    IPartialView? GetPartialViewMacro(string path);
 
-        /// 
-        /// Gets the content of a partial view as a stream.
-        /// 
-        /// The filesystem path to the partial view.
-        /// The content of the partial view.
-        Stream GetPartialViewFileContentStream(string filepath);
+    Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Sets the content of a partial view.
-        /// 
-        /// The filesystem path to the partial view.
-        /// The content of the partial view.
-        void SetPartialViewFileContent(string filepath, Stream content);
+    Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the size of a partial view.
-        /// 
-        /// The filesystem path to the partial view.
-        /// The size of the partial view.
-        long GetPartialViewFileSize(string filepath);
+    bool DeletePartialView(string path, int? userId = null);
 
-        /// 
-        /// Gets the content of a macro partial view as a stream.
-        /// 
-        /// The filesystem path to the macro partial view.
-        /// The content of the macro partial view.
-        Stream GetPartialViewMacroFileContentStream(string filepath);
+    bool DeletePartialViewMacro(string path, int? userId = null);
 
-        /// 
-        /// Sets the content of a macro partial view.
-        /// 
-        /// The filesystem path to the macro partial view.
-        /// The content of the macro partial view.
-        void SetPartialViewMacroFileContent(string filepath, Stream content);
+    Attempt SavePartialView(IPartialView partialView, int? userId = null);
 
-        /// 
-        /// Gets the size of a macro partial view.
-        /// 
-        /// The filesystem path to the macro partial view.
-        /// The size of the macro partial view.
-        long GetPartialViewMacroFileSize(string filepath);
+    Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetStylesheets(params string[] paths);
+    /// 
+    ///     Gets the content of a partial view as a stream.
+    /// 
+    /// The filesystem path to the partial view.
+    /// The content of the partial view.
+    Stream GetPartialViewFileContentStream(string filepath);
 
-        /// 
-        /// Gets a  object by its name
-        /// 
-        /// Path of the stylesheet incl. extension
-        /// A  object
-        IStylesheet? GetStylesheet(string? path);
+    /// 
+    ///     Sets the content of a partial view.
+    /// 
+    /// The filesystem path to the partial view.
+    /// The content of the partial view.
+    void SetPartialViewFileContent(string filepath, Stream content);
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the stylesheet
-        void SaveStylesheet(IStylesheet? stylesheet, int? userId = null);
+    /// 
+    ///     Gets the size of a partial view.
+    /// 
+    /// The filesystem path to the partial view.
+    /// The size of the partial view.
+    long GetPartialViewFileSize(string filepath);
 
-        /// 
-        /// Deletes a stylesheet by its name
-        /// 
-        /// Name incl. extension of the Stylesheet to delete
-        /// Optional id of the user deleting the stylesheet
-        void DeleteStylesheet(string path, int? userId = null);
+    /// 
+    ///     Gets the content of a macro partial view as a stream.
+    /// 
+    /// The filesystem path to the macro partial view.
+    /// The content of the macro partial view.
+    Stream GetPartialViewMacroFileContentStream(string filepath);
 
-        /// 
-        /// Creates a folder for style sheets
-        /// 
-        /// 
-        /// 
-        void CreateStyleSheetFolder(string folderPath);
+    /// 
+    ///     Sets the content of a macro partial view.
+    /// 
+    /// The filesystem path to the macro partial view.
+    /// The content of the macro partial view.
+    void SetPartialViewMacroFileContent(string filepath, Stream content);
 
-        /// 
-        /// Deletes a folder for style sheets
-        /// 
-        /// 
-        void DeleteStyleSheetFolder(string folderPath);
+    /// 
+    ///     Gets the size of a macro partial view.
+    /// 
+    /// The filesystem path to the macro partial view.
+    /// The size of the macro partial view.
+    long GetPartialViewMacroFileSize(string filepath);
 
-        /// 
-        /// Gets the content of a stylesheet as a stream.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The content of the stylesheet.
-        Stream GetStylesheetFileContentStream(string filepath);
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetStylesheets(params string[] paths);
 
-        /// 
-        /// Sets the content of a stylesheet.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The content of the stylesheet.
-        void SetStylesheetFileContent(string filepath, Stream content);
+    /// 
+    ///     Gets a  object by its name
+    /// 
+    /// Path of the stylesheet incl. extension
+    /// A  object
+    IStylesheet? GetStylesheet(string? path);
 
-        /// 
-        /// Gets the size of a stylesheet.
-        /// 
-        /// The filesystem path to the stylesheet.
-        /// The size of the stylesheet.
-        long GetStylesheetFileSize(string filepath);
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the stylesheet
+    void SaveStylesheet(IStylesheet? stylesheet, int? userId = null);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetScripts(params string[] names);
+    /// 
+    ///     Deletes a stylesheet by its name
+    /// 
+    /// Name incl. extension of the Stylesheet to delete
+    /// Optional id of the user deleting the stylesheet
+    void DeleteStylesheet(string path, int? userId = null);
 
-        /// 
-        /// Gets a  object by its name
-        /// 
-        /// Name of the script incl. extension
-        /// A  object
-        IScript? GetScript(string? name);
+    /// 
+    ///     Creates a folder for style sheets
+    /// 
+    /// 
+    /// 
+    void CreateStyleSheetFolder(string folderPath);
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the script
-        void SaveScript(IScript? script, int? userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes a folder for style sheets
+    /// 
+    /// 
+    void DeleteStyleSheetFolder(string folderPath);
 
-        /// 
-        /// Deletes a script by its name
-        /// 
-        /// Name incl. extension of the Script to delete
-        /// Optional id of the user deleting the script
-        void DeleteScript(string path, int? userId = null);
+    /// 
+    ///     Gets the content of a stylesheet as a stream.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The content of the stylesheet.
+    Stream GetStylesheetFileContentStream(string filepath);
 
-        /// 
-        /// Creates a folder for scripts
-        /// 
-        /// 
-        /// 
-        void CreateScriptFolder(string folderPath);
+    /// 
+    ///     Sets the content of a stylesheet.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The content of the stylesheet.
+    void SetStylesheetFileContent(string filepath, Stream content);
 
-        /// 
-        /// Deletes a folder for scripts
-        /// 
-        /// 
-        void DeleteScriptFolder(string folderPath);
+    /// 
+    ///     Gets the size of a stylesheet.
+    /// 
+    /// The filesystem path to the stylesheet.
+    /// The size of the stylesheet.
+    long GetStylesheetFileSize(string filepath);
 
-        /// 
-        /// Gets the content of a script file as a stream.
-        /// 
-        /// The filesystem path to the script.
-        /// The content of the script file.
-        Stream GetScriptFileContentStream(string filepath);
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetScripts(params string[] names);
 
-        /// 
-        /// Sets the content of a script file.
-        /// 
-        /// The filesystem path to the script.
-        /// The content of the script file.
-        void SetScriptFileContent(string filepath, Stream content);
+    /// 
+    ///     Gets a  object by its name
+    /// 
+    /// Name of the script incl. extension
+    /// A  object
+    IScript? GetScript(string? name);
 
-        /// 
-        /// Gets the size of a script file.
-        /// 
-        /// The filesystem path to the script file.
-        /// The size of the script file.
-        long GetScriptFileSize(string filepath);
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the script
+    void SaveScript(IScript? script, int? userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetTemplates(params string[] aliases);
+    /// 
+    ///     Deletes a script by its name
+    /// 
+    /// Name incl. extension of the Script to delete
+    /// Optional id of the user deleting the script
+    void DeleteScript(string path, int? userId = null);
 
-        /// 
-        /// Gets a list of all  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetTemplates(int masterTemplateId);
+    /// 
+    ///     Creates a folder for scripts
+    /// 
+    /// 
+    /// 
+    void CreateScriptFolder(string folderPath);
 
-        /// 
-        /// Gets a  object by its alias.
-        /// 
-        /// The alias of the template.
-        /// The  object matching the alias, or null.
-        ITemplate? GetTemplate(string? alias);
+    /// 
+    ///     Deletes a folder for scripts
+    /// 
+    /// 
+    void DeleteScriptFolder(string folderPath);
 
-        /// 
-        /// Gets a  object by its identifier.
-        /// 
-        /// The identifier of the template.
-        /// The  object matching the identifier, or null.
-        ITemplate? GetTemplate(int id);
+    /// 
+    ///     Gets the content of a script file as a stream.
+    /// 
+    /// The filesystem path to the script.
+    /// The content of the script file.
+    Stream GetScriptFileContentStream(string filepath);
 
-        /// 
-        /// Gets a  object by its guid identifier.
-        /// 
-        /// The guid identifier of the template.
-        /// The  object matching the identifier, or null.
-        ITemplate? GetTemplate(Guid id);
+    /// 
+    ///     Sets the content of a script file.
+    /// 
+    /// The filesystem path to the script.
+    /// The content of the script file.
+    void SetScriptFileContent(string filepath, Stream content);
 
-        /// 
-        /// Gets the template descendants
-        /// 
-        /// 
-        /// 
-        IEnumerable GetTemplateDescendants(int masterTemplateId);
+    /// 
+    ///     Gets the size of a script file.
+    /// 
+    /// The filesystem path to the script file.
+    /// The size of the script file.
+    long GetScriptFileSize(string filepath);
 
-        /// 
-        /// Saves a 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the template
-        void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetTemplates(params string[] aliases);
 
-        /// 
-        /// Creates a template for a content type
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// The template created
-        /// 
-        Attempt?> CreateTemplateForContentType(string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a list of all  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetTemplates(int masterTemplateId);
 
-        ITemplate CreateTemplateWithIdentity(string? name, string? alias, string? content, ITemplate? masterTemplate = null, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  object by its alias.
+    /// 
+    /// The alias of the template.
+    /// The  object matching the alias, or null.
+    ITemplate? GetTemplate(string? alias);
 
-        /// 
-        /// Deletes a template by its alias
-        /// 
-        /// Alias of the  to delete
-        /// Optional id of the user deleting the template
-        void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  object by its identifier.
+    /// 
+    /// The identifier of the template.
+    /// The  object matching the identifier, or null.
+    ITemplate? GetTemplate(int id);
 
-        /// 
-        /// Saves a collection of  objects
-        /// 
-        /// List of  to save
-        /// Optional id of the user
-        void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  object by its guid identifier.
+    /// 
+    /// The guid identifier of the template.
+    /// The  object matching the identifier, or null.
+    ITemplate? GetTemplate(Guid id);
 
-        /// 
-        /// Gets the content of a template as a stream.
-        /// 
-        /// The filesystem path to the template.
-        /// The content of the template.
-        Stream GetTemplateFileContentStream(string filepath);
+    /// 
+    ///     Gets the template descendants
+    /// 
+    /// 
+    /// 
+    IEnumerable GetTemplateDescendants(int masterTemplateId);
 
-        /// 
-        /// Sets the content of a template.
-        /// 
-        /// The filesystem path to the template.
-        /// The content of the template.
-        void SetTemplateFileContent(string filepath, Stream content);
+    /// 
+    ///     Saves a 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the template
+    void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the size of a template.
-        /// 
-        /// The filesystem path to the template.
-        /// The size of the template.
-        long GetTemplateFileSize(string filepath);
+    /// 
+    ///     Creates a template for a content type
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     The template created
+    /// 
+    Attempt?> CreateTemplateForContentType(
+        string contentTypeAlias,
+        string? contentTypeName,
+        int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the content of a macro partial view snippet as a string
-        /// 
-        /// The name of the snippet
-        /// 
-        [Obsolete("Please use SnippetCollection.GetPartialViewMacroSnippetContent instead. Scheduled for removal in V12.")]
+    ITemplate CreateTemplateWithIdentity(string? name, string? alias, string? content, ITemplate? masterTemplate = null, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Deletes a template by its alias
+    /// 
+    /// Alias of the  to delete
+    /// Optional id of the user deleting the template
+    void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Saves a collection of  objects
+    /// 
+    /// List of  to save
+    /// Optional id of the user
+    void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId);
+
+    /// 
+    ///     Gets the content of a template as a stream.
+    /// 
+    /// The filesystem path to the template.
+    /// The content of the template.
+    Stream GetTemplateFileContentStream(string filepath);
+
+    /// 
+    ///     Sets the content of a template.
+    /// 
+    /// The filesystem path to the template.
+    /// The content of the template.
+    void SetTemplateFileContent(string filepath, Stream content);
+
+    /// 
+    ///     Gets the size of a template.
+    /// 
+    /// The filesystem path to the template.
+    /// The size of the template.
+    long GetTemplateFileSize(string filepath);
+
+    /// 
+    ///     Gets the content of a macro partial view snippet as a string
+    /// 
+    /// The name of the snippet
+    /// 
+    [Obsolete("Please use SnippetCollection.GetPartialViewMacroSnippetContent instead. Scheduled for removal in V12.")]
         string GetPartialViewMacroSnippetContent(string snippetName);
 
-        /// 
-        /// Gets the content of a partial view snippet as a string.
-        /// 
-        /// The name of the snippet
-        /// The content of the partial view.
-        [Obsolete("Please use SnippetCollection.GetPartialViewSnippetContent instead. Scheduled for removal in V12.")]
-        string GetPartialViewSnippetContent(string snippetName);
-    }
+    /// 
+    ///     Gets the content of a partial view snippet as a string.
+    /// 
+    /// The name of the snippet
+    /// The content of the partial view.
+    [Obsolete("Please use SnippetCollection.GetPartialViewSnippetContent instead. Scheduled for removal in V12.")]string GetPartialViewSnippetContent(string snippetName);
 }
diff --git a/src/Umbraco.Core/Services/IIconService.cs b/src/Umbraco.Core/Services/IIconService.cs
index 0b215c481c..8aff7e8920 100644
--- a/src/Umbraco.Core/Services/IIconService.cs
+++ b/src/Umbraco.Core/Services/IIconService.cs
@@ -1,23 +1,19 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
-{
-    public interface IIconService
-    {
-        /// 
-        /// Gets the svg string for the icon name found at the global icons path
-        /// 
-        /// 
-        /// 
-        IconModel? GetIcon(string iconName);
+namespace Umbraco.Cms.Core.Services;
 
-        /// 
-        /// Gets a list of all svg icons found at at the global icons path.
-        /// 
-        /// 
-        IReadOnlyDictionary? GetIcons();
-    }
+public interface IIconService
+{
+    /// 
+    ///     Gets the svg string for the icon name found at the global icons path
+    /// 
+    /// 
+    /// 
+    IconModel? GetIcon(string iconName);
+
+    /// 
+    ///     Gets a list of all svg icons found at at the global icons path.
+    /// 
+    /// 
+    IReadOnlyDictionary? GetIcons();
 }
diff --git a/src/Umbraco.Core/Services/IIdKeyMap.cs b/src/Umbraco.Core/Services/IIdKeyMap.cs
index 199ee23813..e85095d41f 100644
--- a/src/Umbraco.Core/Services/IIdKeyMap.cs
+++ b/src/Umbraco.Core/Services/IIdKeyMap.cs
@@ -1,16 +1,20 @@
-using System;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IIdKeyMap
 {
-    public interface IIdKeyMap
-    {
-        Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType);
-        Attempt GetIdForUdi(Udi udi);
-        Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType);
-        Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType);
-        void ClearCache();
-        void ClearCache(int id);
-        void ClearCache(Guid key);
-    }
+    Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType);
+
+    Attempt GetIdForUdi(Udi udi);
+
+    Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType);
+
+    Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType);
+
+    void ClearCache();
+
+    void ClearCache(int id);
+
+    void ClearCache(Guid key);
 }
diff --git a/src/Umbraco.Core/Services/IInstallationService.cs b/src/Umbraco.Core/Services/IInstallationService.cs
index 5b1d28cccc..688c6298bd 100644
--- a/src/Umbraco.Core/Services/IInstallationService.cs
+++ b/src/Umbraco.Core/Services/IInstallationService.cs
@@ -1,9 +1,6 @@
-using System.Threading.Tasks;
+namespace Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Core.Services
+public interface IInstallationService
 {
-    public interface IInstallationService
-    {
-        Task LogInstall(InstallLog installLog);
-    }
+    Task LogInstall(InstallLog installLog);
 }
diff --git a/src/Umbraco.Core/Services/IIpAddressUtilities.cs b/src/Umbraco.Core/Services/IIpAddressUtilities.cs
index 7c68bcfa9f..f6c3717244 100644
--- a/src/Umbraco.Core/Services/IIpAddressUtilities.cs
+++ b/src/Umbraco.Core/Services/IIpAddressUtilities.cs
@@ -1,4 +1,4 @@
-using System.Net;
+using System.Net;
 
 namespace Umbraco.Cms.Core.Services;
 
diff --git a/src/Umbraco.Core/Services/IKeyValueService.cs b/src/Umbraco.Core/Services/IKeyValueService.cs
index 1ebf6e9728..97316911c4 100644
--- a/src/Umbraco.Core/Services/IKeyValueService.cs
+++ b/src/Umbraco.Core/Services/IKeyValueService.cs
@@ -1,45 +1,45 @@
-using System.Collections;
-using System.Collections.Generic;
+namespace Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Core.Services
+/// 
+///     Manages the simplified key/value store.
+/// 
+public interface IKeyValueService
 {
     /// 
-    /// Manages the simplified key/value store.
+    ///     Gets a value.
     /// 
-    public interface IKeyValueService
-    {
-        /// 
-        /// Gets a value.
-        /// 
-        /// Returns null if no value was found for the key.
-        string? GetValue(string key);
+    /// Returns null if no value was found for the key.
+    string? GetValue(string key);
 
-        /// 
-        /// Returns key/value pairs for all keys with the specified prefix.
-        /// 
-        /// 
-        /// 
-        IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix);
+    /// 
+    ///     Returns key/value pairs for all keys with the specified prefix.
+    /// 
+    /// 
+    /// 
+    IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix);
 
-        /// 
-        /// Sets a value.
-        /// 
-        void SetValue(string key, string value);
+    /// 
+    ///     Sets a value.
+    /// 
+    void SetValue(string key, string value);
 
-        /// 
-        /// Sets a value.
-        /// 
-        /// Sets the value to  if the value is ,
-        /// and returns true; otherwise throws an exception. In other words, ensures that the value has not changed
-        /// before setting it.
-        void SetValue(string key, string originValue, string newValue);
+    /// 
+    ///     Sets a value.
+    /// 
+    /// 
+    ///     Sets the value to  if the value is ,
+    ///     and returns true; otherwise throws an exception. In other words, ensures that the value has not changed
+    ///     before setting it.
+    /// 
+    void SetValue(string key, string originValue, string newValue);
 
-        /// 
-        /// Tries to set a value.
-        /// 
-        /// Sets the value to  if the value is ,
-        /// and returns true; otherwise returns false. In other words, ensures that the value has not changed
-        /// before setting it.
-        bool TrySetValue(string key, string originValue, string newValue);
-    }
+    /// 
+    ///     Tries to set a value.
+    /// 
+    /// 
+    ///     Sets the value to  if the value is ,
+    ///     and returns true; otherwise returns false. In other words, ensures that the value has not changed
+    ///     before setting it.
+    /// 
+    bool TrySetValue(string key, string originValue, string newValue);
 }
diff --git a/src/Umbraco.Core/Services/ILocalizationService.cs b/src/Umbraco.Core/Services/ILocalizationService.cs
index eca2a8e070..7a1b1b6fd1 100644
--- a/src/Umbraco.Core/Services/ILocalizationService.cs
+++ b/src/Umbraco.Core/Services/ILocalizationService.cs
@@ -1,171 +1,178 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the Localization Service, which is an easy access to operations involving Languages and Dictionary
+/// 
+public interface ILocalizationService : IService
 {
+    // Possible to-do list:
+    // Import DictionaryItem (?)
+    // RemoveByLanguage (translations)
+    // Add/Set Text (Insert/Update)
+    // Remove Text (in translation)
+
     /// 
-    /// Defines the Localization Service, which is an easy access to operations involving Languages and Dictionary
+    ///     Adds or updates a translation for a dictionary item and language
     /// 
-    public interface ILocalizationService : IService
-    {
-        //Possible to-do list:
-        //Import DictionaryItem (?)
-        //RemoveByLanguage (translations)
-        //Add/Set Text (Insert/Update)
-        //Remove Text (in translation)
+    /// 
+    /// 
+    /// 
+    /// 
+    void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value);
 
-        /// 
-        /// Adds or updates a translation for a dictionary item and language
-        /// 
-        /// 
-        /// 
-        /// 
-        void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value);
+    /// 
+    ///     Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified.
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null);
 
-        /// 
-        /// Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified.
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null);
+    /// 
+    ///     Gets a  by its  id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    IDictionaryItem? GetDictionaryItemById(int id);
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        IDictionaryItem? GetDictionaryItemById(int id);
+    /// 
+    ///     Gets a  by its  id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    IDictionaryItem? GetDictionaryItemById(Guid id);
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        IDictionaryItem? GetDictionaryItemById(Guid id);
+    /// 
+    ///     Gets a  by its key
+    /// 
+    /// Key of the 
+    /// 
+    ///     
+    /// 
+    IDictionaryItem? GetDictionaryItemByKey(string key);
 
-        /// 
-        /// Gets a  by its key
-        /// 
-        /// Key of the 
-        /// 
-        IDictionaryItem? GetDictionaryItemByKey(string key);
+    /// 
+    ///     Gets a list of children for a 
+    /// 
+    /// Id of the parent
+    /// An enumerable list of  objects
+    IEnumerable GetDictionaryItemChildren(Guid parentId);
 
-        /// 
-        /// Gets a list of children for a 
-        /// 
-        /// Id of the parent
-        /// An enumerable list of  objects
-        IEnumerable GetDictionaryItemChildren(Guid parentId);
+    /// 
+    ///     Gets a list of descendants for a 
+    /// 
+    /// Id of the parent, null will return all dictionary items
+    /// An enumerable list of  objects
+    IEnumerable GetDictionaryItemDescendants(Guid? parentId);
 
-        /// 
-        /// Gets a list of descendants for a 
-        /// 
-        /// Id of the parent, null will return all dictionary items
-        /// An enumerable list of  objects
-        IEnumerable GetDictionaryItemDescendants(Guid? parentId);
+    /// 
+    ///     Gets the root/top  objects
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetRootDictionaryItems();
 
-        /// 
-        /// Gets the root/top  objects
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetRootDictionaryItems();
+    /// 
+    ///     Checks if a  with given key exists
+    /// 
+    /// Key of the 
+    /// True if a  exists, otherwise false
+    bool DictionaryItemExists(string key);
 
-        /// 
-        /// Checks if a  with given key exists
-        /// 
-        /// Key of the 
-        /// True if a  exists, otherwise false
-        bool DictionaryItemExists(string key);
+    /// 
+    ///     Saves a  object
+    /// 
+    ///  to save
+    /// Optional id of the user saving the dictionary item
+    void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves a  object
-        /// 
-        ///  to save
-        /// Optional id of the user saving the dictionary item
-        void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes a  object and its related translations
+    ///     as well as its children.
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the dictionary item
+    void Delete(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes a  object and its related translations
-        /// as well as its children.
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the dictionary item
-        void Delete(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a  by its id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    ILanguage? GetLanguageById(int id);
 
-        /// 
-        /// Gets a  by its id
-        /// 
-        /// Id of the 
-        /// 
-        ILanguage? GetLanguageById(int id);
+    /// 
+    ///     Gets a  by its iso code
+    /// 
+    /// Iso Code of the language (ie. en-US)
+    /// 
+    ///     
+    /// 
+    ILanguage? GetLanguageByIsoCode(string? isoCode);
 
-        /// 
-        /// Gets a  by its iso code
-        /// 
-        /// Iso Code of the language (ie. en-US)
-        /// 
-        ILanguage? GetLanguageByIsoCode(string? isoCode);
+    /// 
+    ///     Gets a language identifier from its ISO code.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    int? GetLanguageIdByIsoCode(string isoCode);
 
-        /// 
-        /// Gets a language identifier from its ISO code.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        int? GetLanguageIdByIsoCode(string isoCode);
+    /// 
+    ///     Gets a language ISO code from its identifier.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    string? GetLanguageIsoCodeById(int id);
 
-        /// 
-        /// Gets a language ISO code from its identifier.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        string? GetLanguageIsoCodeById(int id);
+    /// 
+    ///     Gets the default language ISO code.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    string GetDefaultLanguageIsoCode();
 
-        /// 
-        /// Gets the default language ISO code.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        string GetDefaultLanguageIsoCode();
+    /// 
+    ///     Gets the default language identifier.
+    /// 
+    /// 
+    ///     This can be optimized and bypass all deep cloning.
+    /// 
+    int? GetDefaultLanguageId();
 
-        /// 
-        /// Gets the default language identifier.
-        /// 
-        /// 
-        /// This can be optimized and bypass all deep cloning.
-        /// 
-        int? GetDefaultLanguageId();
+    /// 
+    ///     Gets all available languages
+    /// 
+    /// An enumerable list of  objects
+    IEnumerable GetAllLanguages();
 
-        /// 
-        /// Gets all available languages
-        /// 
-        /// An enumerable list of  objects
-        IEnumerable GetAllLanguages();
+    /// 
+    ///     Saves a  object
+    /// 
+    ///  to save
+    /// Optional id of the user saving the language
+    void Save(ILanguage language, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves a  object
-        /// 
-        ///  to save
-        /// Optional id of the user saving the language
-        void Save(ILanguage language, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes a  by removing it and its usages from the db
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the language
+    void Delete(ILanguage language, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes a  by removing it and its usages from the db
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the language
-        void Delete(ILanguage language, int userId = Constants.Security.SuperUserId);
-
-        /// 
-        /// Gets the full dictionary key map.
-        /// 
-        /// The full dictionary key map.
-        Dictionary GetDictionaryItemKeyMap();
-    }
+    /// 
+    ///     Gets the full dictionary key map.
+    /// 
+    /// The full dictionary key map.
+    Dictionary GetDictionaryItemKeyMap();
 }
diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs
index c49a4e6b2f..23e3888ea0 100644
--- a/src/Umbraco.Core/Services/ILocalizedTextService.cs
+++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs
@@ -1,62 +1,62 @@
-using System.Collections.Generic;
 using System.Globalization;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+// TODO: This needs to be merged into one interface in v9, but better yet
+// the Localize method should just the based on area + alias and we should remove
+// the one with the 'key' (the concatenated area/alias) to ensure that we never use that again.
+
+/// 
+///     The entry point to localize any key in the text storage source for a given culture
+/// 
+/// 
+///     This class is created to be as simple as possible so that it can be replaced very easily,
+///     all other methods are extension methods that simply call the one underlying method in this class
+/// 
+public interface ILocalizedTextService
 {
-    // TODO: This needs to be merged into one interface in v9, but better yet
-    // the Localize method should just the based on area + alias and we should remove
-    // the one with the 'key' (the concatenated area/alias) to ensure that we never use that again.
+    /// 
+    ///     Localize a key with variables
+    /// 
+    /// 
+    /// 
+    /// 
+    /// This can be null
+    /// 
+    string Localize(string? area, string? alias, CultureInfo? culture, IDictionary? tokens = null);
 
     /// 
-    /// The entry point to localize any key in the text storage source for a given culture
+    ///     Returns all key/values in storage for the given culture
     /// 
+    /// 
+    IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture);
+
+    /// 
+    ///     Returns all key/values in storage for the given culture
+    /// 
+    /// 
+    IDictionary GetAllStoredValues(CultureInfo culture);
+
+    /// 
+    ///     Returns a list of all currently supported cultures
+    /// 
+    /// 
+    IEnumerable GetSupportedCultures();
+
+    /// 
+    ///     Tries to resolve a full 4 letter culture from a 2 letter culture name
+    /// 
+    /// 
+    ///     The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be
+    ///     returned
+    /// 
+    /// 
     /// 
-    /// This class is created to be as simple as possible so that it can be replaced very easily,
-    /// all other methods are extension methods that simply call the one underlying method in this class
+    ///     TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since
+    ///     that
+    ///     is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this
+    ///     attempts
+    ///     to resolve the full culture if possible.
     /// 
-    public interface ILocalizedTextService
-    {
-        /// 
-        /// Localize a key with variables
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This can be null
-        /// 
-        string Localize(string? area, string? alias, CultureInfo? culture, IDictionary? tokens = null);
-
-
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        /// 
-        IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture);
-
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        /// 
-        IDictionary GetAllStoredValues(CultureInfo culture);
-
-        /// 
-        /// Returns a list of all currently supported cultures
-        /// 
-        /// 
-        IEnumerable GetSupportedCultures();
-
-        /// 
-        /// Tries to resolve a full 4 letter culture from a 2 letter culture name
-        /// 
-        /// 
-        /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned
-        /// 
-        /// 
-        /// 
-        /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that
-        /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts
-        /// to resolve the full culture if possible.
-        /// 
-        CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture);
-    }
+    CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture);
 }
diff --git a/src/Umbraco.Core/Services/IMacroService.cs b/src/Umbraco.Core/Services/IMacroService.cs
index a75547dd6d..141b278d93 100644
--- a/src/Umbraco.Core/Services/IMacroService.cs
+++ b/src/Umbraco.Core/Services/IMacroService.cs
@@ -1,57 +1,53 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the MacroService, which is an easy access to operations involving 
+/// 
+public interface IMacroService : IService
 {
     /// 
-    /// Defines the MacroService, which is an easy access to operations involving 
+    ///     Gets an  object by its alias
     /// 
-    public interface IMacroService : IService
-    {
+    /// Alias to retrieve an  for
+    /// An  object
+    IMacro? GetByAlias(string alias);
 
-        /// 
-        /// Gets an  object by its alias
-        /// 
-        /// Alias to retrieve an  for
-        /// An  object
-        IMacro? GetByAlias(string alias);
+    IEnumerable GetAll();
 
-        IEnumerable GetAll();
+    IEnumerable GetAll(params int[] ids);
 
-        IEnumerable GetAll(params int[] ids);
+    IEnumerable GetAll(params Guid[] ids);
 
-        IEnumerable GetAll(params Guid[] ids);
+    IMacro? GetById(int id);
 
-        IMacro? GetById(int id);
+    IMacro? GetById(Guid id);
 
-        IMacro? GetById(Guid id);
+    /// 
+    ///     Deletes an 
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the macro
+    void Delete(IMacro macro, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes an 
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the macro
-        void Delete(IMacro macro, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Saves an 
+    /// 
+    ///  to save
+    /// Optional id of the user saving the macro
+    void Save(IMacro macro, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves an 
-        /// 
-        ///  to save
-        /// Optional id of the user saving the macro
-        void Save(IMacro macro, int userId = Constants.Security.SuperUserId);
+    ///// 
+    ///// Gets a list all available  plugins
+    ///// 
+    ///// An enumerable list of  objects
+    // IEnumerable GetMacroPropertyTypes();
 
-        ///// 
-        ///// Gets a list all available  plugins
-        ///// 
-        ///// An enumerable list of  objects
-        //IEnumerable GetMacroPropertyTypes();
-
-        ///// 
-        ///// Gets an  by its alias
-        ///// 
-        ///// Alias to retrieve an  for
-        ///// An  object
-        //IMacroPropertyType GetMacroPropertyTypeByAlias(string alias);
-    }
+    ///// 
+    ///// Gets an  by its alias
+    ///// 
+    ///// Alias to retrieve an  for
+    ///// An  object
+    // IMacroPropertyType GetMacroPropertyTypeByAlias(string alias);
 }
diff --git a/src/Umbraco.Core/Services/IMacroWithAliasService.cs b/src/Umbraco.Core/Services/IMacroWithAliasService.cs
index 6e72777bfa..508168b877 100644
--- a/src/Umbraco.Core/Services/IMacroWithAliasService.cs
+++ b/src/Umbraco.Core/Services/IMacroWithAliasService.cs
@@ -1,17 +1,14 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+[Obsolete("This interface will be merged with IMacroService in Umbraco 11")]
+public interface IMacroWithAliasService : IMacroService
 {
-    [Obsolete("This interface will be merged with IMacroService in Umbraco 11")]
-    public interface IMacroWithAliasService : IMacroService
-    {
-        /// 
-        /// Gets a list of available  objects by alias.
-        /// 
-        /// Optional array of aliases to limit the results
-        /// An enumerable list of  objects
-        IEnumerable GetAll(params string[] aliases);
-    }
+    /// 
+    ///     Gets a list of available  objects by alias.
+    /// 
+    /// Optional array of aliases to limit the results
+    /// An enumerable list of  objects
+    IEnumerable GetAll(params string[] aliases);
 }
diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs
index fe14bdda0f..86440b1119 100644
--- a/src/Umbraco.Core/Services/IMediaService.cs
+++ b/src/Umbraco.Core/Services/IMediaService.cs
@@ -1,363 +1,387 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the Media Service, which is an easy access to operations involving 
+/// 
+public interface IMediaService : IContentServiceBase
 {
-        /// 
-    /// Defines the Media Service, which is an easy access to operations involving 
+    int CountNotTrashed(string? contentTypeAlias = null);
+
+    int Count(string? mediaTypeAlias = null);
+
+    int CountChildren(int parentId, string? mediaTypeAlias = null);
+
+    int CountDescendants(int parentId, string? mediaTypeAlias = null);
+
+    IEnumerable GetByIds(IEnumerable ids);
+
+    IEnumerable GetByIds(IEnumerable ids);
+
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
     /// 
-    public interface IMediaService : IContentServiceBase
-    {
-        int CountNotTrashed(string? contentTypeAlias = null);
-        int Count(string? mediaTypeAlias = null);
-        int CountChildren(int parentId, string? mediaTypeAlias = null);
-        int CountDescendants(int parentId, string? mediaTypeAlias = null);
+    /// 
+    ///     Note that using this method will simply return a new IMedia without any identity
+    ///     as it has not yet been persisted. It is intended as a shortcut to creating new media objects
+    ///     that does not invoke a save operation against the database.
+    /// 
+    /// Name of the Media object
+    /// Id of Parent for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        IEnumerable GetByIds(IEnumerable ids);
-        IEnumerable GetByIds(IEnumerable ids);
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
+    /// 
+    /// 
+    ///     Note that using this method will simply return a new IMedia without any identity
+    ///     as it has not yet been persisted. It is intended as a shortcut to creating new media objects
+    ///     that does not invoke a save operation against the database.
+    /// 
+    /// Name of the Media object
+    /// Id of Parent for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// Note that using this method will simply return a new IMedia without any identity
-        /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects
-        /// that does not invoke a save operation against the database.
-        /// 
-        /// Name of the Media object
-        /// Id of Parent for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
+    /// 
+    /// 
+    ///     Note that using this method will simply return a new IMedia without any identity
+    ///     as it has not yet been persisted. It is intended as a shortcut to creating new media objects
+    ///     that does not invoke a save operation against the database.
+    /// 
+    /// Name of the Media object
+    /// Parent  for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// Note that using this method will simply return a new IMedia without any identity
-        /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects
-        /// that does not invoke a save operation against the database.
-        /// 
-        /// Name of the Media object
-        /// Id of Parent for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets an  object by Id
+    /// 
+    /// Id of the Content to retrieve
+    /// 
+    ///     
+    /// 
+    IMedia? GetById(int id);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// Note that using this method will simply return a new IMedia without any identity
-        /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects
-        /// that does not invoke a save operation against the database.
-        /// 
-        /// Name of the Media object
-        /// Parent  for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a collection of  objects by Parent Id
+    /// 
+    /// Id of the Parent to retrieve Children from
+    /// Page number
+    /// Page size
+    /// Total records query would return without paging
+    /// Field to order by
+    /// Direction to order by
+    /// Flag to indicate when ordering by system field
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
 
-        /// 
-        /// Gets an  object by Id
-        /// 
-        /// Id of the Content to retrieve
-        /// 
-        IMedia? GetById(int id);
+    /// 
+    ///     Gets a collection of  objects by Parent Id
+    /// 
+    /// Id of the Parent to retrieve Descendants from
+    /// Page number
+    /// Page size
+    /// Total records query would return without paging
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
 
-        /// 
-        /// Gets a collection of  objects by Parent Id
-        /// 
-        /// Id of the Parent to retrieve Children from
-        /// Page number
-        /// Page size
-        /// Total records query would return without paging
-        /// Field to order by
-        /// Direction to order by
-        /// Flag to indicate when ordering by system field
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets paged documents of a content
+    /// 
+    /// The page number.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Search text filter.
+    /// Ordering infos.
+    IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null);
 
-        /// 
-        /// Gets a collection of  objects by Parent Id
-        /// 
-        /// Id of the Parent to retrieve Descendants from
-        /// Page number
-        /// Page size
-        /// Total records query would return without paging
-        /// 
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets paged documents for specified content types
+    /// 
+    /// The page number.
+    /// The page number.
+    /// The page size.
+    /// Total number of documents.
+    /// Search text filter.
+    /// Ordering infos.
+    IEnumerable GetPagedOfTypes(
+        int[] contentTypeIds,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        /// 
-        /// Gets paged documents of a content
-        /// 
-        /// The page number.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Search text filter.
-        /// Ordering infos.
-        IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets a collection of  objects, which reside at the first level / root
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetRootMedia();
 
-        /// 
-        /// Gets paged documents for specified content types
-        /// 
-        /// The page number.
-        /// The page number.
-        /// The page size.
-        /// Total number of documents.
-        /// Search text filter.
-        /// Ordering infos.
-        IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Gets a collection of an  objects, which resides in the Recycle Bin
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetPagedMediaInRecycleBin(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        IQuery? filter = null,
+        Ordering? ordering = null);
 
-        /// 
-        /// Gets a collection of  objects, which reside at the first level / root
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetRootMedia();
+    /// 
+    ///     Moves an  object to a new location
+    /// 
+    /// The  to move
+    /// Id of the Media's new Parent
+    /// Id of the User moving the Media
+    /// True if moving succeeded, otherwise False
+    Attempt Move(IMedia media, int parentId, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets a collection of an  objects, which resides in the Recycle Bin
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null);
+    /// 
+    ///     Deletes an  object by moving it to the Recycle Bin
+    /// 
+    /// The  to delete
+    /// Id of the User deleting the Media
+    Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Moves an  object to a new location
-        /// 
-        /// The  to move
-        /// Id of the Media's new Parent
-        /// Id of the User moving the Media
-        /// True if moving succeeded, otherwise False
-        Attempt Move(IMedia media, int parentId, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Empties the Recycle Bin by deleting all  that resides in the bin
+    /// 
+    /// Optional Id of the User emptying the Recycle Bin
+    OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes an  object by moving it to the Recycle Bin
-        /// 
-        /// The  to delete
-        /// Id of the User deleting the Media
-        Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Returns true if there is any media in the recycle bin
+    /// 
+    bool RecycleBinSmells();
 
-        /// 
-        /// Empties the Recycle Bin by deleting all  that resides in the bin
-        /// 
-        /// Optional Id of the User emptying the Recycle Bin
-        OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Deletes all media of specified type. All children of deleted media is moved to Recycle Bin.
+    /// 
+    /// This needs extra care and attention as its potentially a dangerous and extensive operation
+    /// Id of the 
+    /// Optional Id of the user deleting Media
+    void DeleteMediaOfType(int mediaTypeId, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Returns true if there is any media in the recycle bin
-        /// 
-        bool RecycleBinSmells();
+    /// 
+    ///     Deletes all media of the specified types. All Descendants of deleted media that is not of these types is moved to
+    ///     Recycle Bin.
+    /// 
+    /// This needs extra care and attention as its potentially a dangerous and extensive operation
+    /// Ids of the s
+    /// Optional Id of the user issuing the delete operation
+    void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin.
-        /// 
-        /// This needs extra care and attention as its potentially a dangerous and extensive operation
-        /// Id of the 
-        /// Optional Id of the user deleting Media
-        void DeleteMediaOfType(int mediaTypeId, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Permanently deletes an  object
+    /// 
+    /// 
+    ///     Please note that this method will completely remove the Media from the database,
+    ///     but current not from the file system.
+    /// 
+    /// The  to delete
+    /// Id of the User deleting the Media
+    Attempt Delete(IMedia media, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Deletes all media of the specified types. All Descendants of deleted media that is not of these types is moved to Recycle Bin.
-        /// 
-        /// This needs extra care and attention as its potentially a dangerous and extensive operation
-        /// Ids of the s
-        /// Optional Id of the user issuing the delete operation
-        void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Saves a single  object
+    /// 
+    /// The  to save
+    /// Id of the User saving the Media
+    Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Permanently deletes an  object
-        /// 
-        /// 
-        /// Please note that this method will completely remove the Media from the database,
-        /// but current not from the file system.
-        /// 
-        /// The  to delete
-        /// Id of the User deleting the Media
-        Attempt Delete(IMedia media, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Saves a collection of  objects
+    /// 
+    /// Collection of  to save
+    /// Id of the User saving the Media
+    Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Saves a single  object
-        /// 
-        /// The  to save
-        /// Id of the User saving the Media
-        Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets an  object by its 'UniqueId'
+    /// 
+    /// Guid key of the Media to retrieve
+    /// 
+    ///     
+    /// 
+    IMedia? GetById(Guid key);
 
-        /// 
-        /// Saves a collection of  objects
-        /// 
-        /// Collection of  to save
-        /// Id of the User saving the Media
-        Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a collection of  objects by Level
+    /// 
+    /// The level to retrieve Media from
+    /// An Enumerable list of  objects
+    IEnumerable? GetByLevel(int level);
 
-        /// 
-        /// Gets an  object by its 'UniqueId'
-        /// 
-        /// Guid key of the Media to retrieve
-        /// 
-        IMedia? GetById(Guid key);
+    /// 
+    ///     Gets a specific version of an  item.
+    /// 
+    /// Id of the version to retrieve
+    /// An  item
+    IMedia? GetVersion(int versionId);
 
-        /// 
-        /// Gets a collection of  objects by Level
-        /// 
-        /// The level to retrieve Media from
-        /// An Enumerable list of  objects
-        IEnumerable? GetByLevel(int level);
+    /// 
+    ///     Gets a collection of an  objects versions by Id
+    /// 
+    /// 
+    /// An Enumerable list of  objects
+    IEnumerable GetVersions(int id);
 
-        /// 
-        /// Gets a specific version of an  item.
-        /// 
-        /// Id of the version to retrieve
-        /// An  item
-        IMedia? GetVersion(int versionId);
+    /// 
+    ///     Checks whether an  item has any children
+    /// 
+    /// Id of the 
+    /// True if the media has any children otherwise False
+    bool HasChildren(int id);
 
-        /// 
-        /// Gets a collection of an  objects versions by Id
-        /// 
-        /// 
-        /// An Enumerable list of  objects
-        IEnumerable GetVersions(int id);
+    /// 
+    ///     Permanently deletes versions from an  object prior to a specific date.
+    /// 
+    /// Id of the  object to delete versions from
+    /// Latest version date
+    /// Optional Id of the User deleting versions of a Content object
+    void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Checks whether an  item has any children
-        /// 
-        /// Id of the 
-        /// True if the media has any children otherwise False
-        bool HasChildren(int id);
+    /// 
+    ///     Permanently deletes specific version(s) from an  object.
+    /// 
+    /// Id of the  object to delete a version from
+    /// Id of the version to delete
+    /// Boolean indicating whether to delete versions prior to the versionId
+    /// Optional Id of the User deleting versions of a Content object
+    void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Permanently deletes versions from an  object prior to a specific date.
-        /// 
-        /// Id of the  object to delete versions from
-        /// Latest version date
-        /// Optional Id of the User deleting versions of a Content object
-        void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets an  object from the path stored in the 'umbracoFile' property.
+    /// 
+    /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg)
+    /// 
+    ///     
+    /// 
+    IMedia? GetMediaByPath(string mediaPath);
 
-        /// 
-        /// Permanently deletes specific version(s) from an  object.
-        /// 
-        /// Id of the  object to delete a version from
-        /// Id of the version to delete
-        /// Boolean indicating whether to delete versions prior to the versionId
-        /// Optional Id of the User deleting versions of a Content object
-        void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets a collection of  objects, which are ancestors of the current media.
+    /// 
+    /// Id of the  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    IEnumerable GetAncestors(int id);
 
-        /// 
-        /// Gets an  object from the path stored in the 'umbracoFile' property.
-        /// 
-        /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg)
-        /// 
-        IMedia? GetMediaByPath(string mediaPath);
+    /// 
+    ///     Gets a collection of  objects, which are ancestors of the current media.
+    /// 
+    ///  to retrieve ancestors for
+    /// An Enumerable list of  objects
+    IEnumerable GetAncestors(IMedia media);
 
-        /// 
-        /// Gets a collection of  objects, which are ancestors of the current media.
-        /// 
-        /// Id of the  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        IEnumerable GetAncestors(int id);
+    /// 
+    ///     Gets the parent of the current media as an  item.
+    /// 
+    /// Id of the  to retrieve the parent from
+    /// Parent  object
+    IMedia? GetParent(int id);
 
-        /// 
-        /// Gets a collection of  objects, which are ancestors of the current media.
-        /// 
-        ///  to retrieve ancestors for
-        /// An Enumerable list of  objects
-        IEnumerable GetAncestors(IMedia media);
+    /// 
+    ///     Gets the parent of the current media as an  item.
+    /// 
+    ///  to retrieve the parent from
+    /// Parent  object
+    IMedia? GetParent(IMedia media);
 
-        /// 
-        /// Gets the parent of the current media as an  item.
-        /// 
-        /// Id of the  to retrieve the parent from
-        /// Parent  object
-        IMedia? GetParent(int id);
+    /// 
+    ///     Sorts a collection of  objects by updating the SortOrder according
+    ///     to the ordering of items in the passed in .
+    /// 
+    /// 
+    /// 
+    /// True if sorting succeeded, otherwise False
+    bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Gets the parent of the current media as an  item.
-        /// 
-        ///  to retrieve the parent from
-        /// Parent  object
-        IMedia? GetParent(IMedia media);
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
+    /// 
+    /// 
+    ///     This method returns an  object that has been persisted to the database
+    ///     and therefor has an identity.
+    /// 
+    /// Name of the Media object
+    /// Parent  for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Sorts a collection of  objects by updating the SortOrder according
-        /// to the ordering of items in the passed in .
-        /// 
-        /// 
-        /// 
-        /// True if sorting succeeded, otherwise False
-        bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Creates an  object using the alias of the 
+    ///     that this Media should based on.
+    /// 
+    /// 
+    ///     This method returns an  object that has been persisted to the database
+    ///     and therefor has an identity.
+    /// 
+    /// Name of the Media object
+    /// Id of Parent for the new Media item
+    /// Alias of the 
+    /// Optional id of the user creating the media item
+    /// 
+    ///     
+    /// 
+    IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// This method returns an  object that has been persisted to the database
-        /// and therefor has an identity.
-        /// 
-        /// Name of the Media object
-        /// Parent  for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Gets the content of a media as a stream.
+    /// 
+    /// The filesystem path to the media.
+    /// The content of the media.
+    Stream GetMediaFileContentStream(string filepath);
 
-        /// 
-        /// Creates an  object using the alias of the 
-        /// that this Media should based on.
-        /// 
-        /// 
-        /// This method returns an  object that has been persisted to the database
-        /// and therefor has an identity.
-        /// 
-        /// Name of the Media object
-        /// Id of Parent for the new Media item
-        /// Alias of the 
-        /// Optional id of the user creating the media item
-        /// 
-        IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Sets the content of a media.
+    /// 
+    /// The filesystem path to the media.
+    /// The content of the media.
+    void SetMediaFileContent(string filepath, Stream content);
 
-        /// 
-        /// Gets the content of a media as a stream.
-        /// 
-        /// The filesystem path to the media.
-        /// The content of the media.
-        Stream GetMediaFileContentStream(string filepath);
+    /// 
+    ///     Deletes a media file.
+    /// 
+    /// The filesystem path to the media.
+    void DeleteMediaFile(string filepath);
 
-        /// 
-        /// Sets the content of a media.
-        /// 
-        /// The filesystem path to the media.
-        /// The content of the media.
-        void SetMediaFileContent(string filepath, Stream content);
-
-        /// 
-        /// Deletes a media file.
-        /// 
-        /// The filesystem path to the media.
-        void DeleteMediaFile(string filepath);
-
-        /// 
-        /// Gets the size of a media.
-        /// 
-        /// The filesystem path to the media.
-        /// The size of the media.
-        long GetMediaFileSize(string filepath);
-    }
+    /// 
+    ///     Gets the size of a media.
+    /// 
+    /// The filesystem path to the media.
+    /// The size of the media.
+    long GetMediaFileSize(string filepath);
 }
diff --git a/src/Umbraco.Core/Services/IMediaTypeService.cs b/src/Umbraco.Core/Services/IMediaTypeService.cs
index e00d86613b..a00b9ae5c6 100644
--- a/src/Umbraco.Core/Services/IMediaTypeService.cs
+++ b/src/Umbraco.Core/Services/IMediaTypeService.cs
@@ -1,10 +1,10 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Manages  objects.
+/// 
+public interface IMediaTypeService : IContentTypeBaseService
 {
-    /// 
-    /// Manages  objects.
-    /// 
-    public interface IMediaTypeService : IContentTypeBaseService
-    { }
 }
diff --git a/src/Umbraco.Core/Services/IMemberGroupService.cs b/src/Umbraco.Core/Services/IMemberGroupService.cs
index 9b8c4a8d53..24cc6845ad 100644
--- a/src/Umbraco.Core/Services/IMemberGroupService.cs
+++ b/src/Umbraco.Core/Services/IMemberGroupService.cs
@@ -1,17 +1,20 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IMemberGroupService : IService
 {
-    public interface IMemberGroupService : IService
-    {
-        IEnumerable GetAll();
-        IMemberGroup? GetById(int id);
-        IMemberGroup? GetById(Guid id);
-        IEnumerable GetByIds(IEnumerable ids);
-        IMemberGroup? GetByName(string? name);
-        void Save(IMemberGroup memberGroup);
-        void Delete(IMemberGroup memberGroup);
-    }
+    IEnumerable GetAll();
+
+    IMemberGroup? GetById(int id);
+
+    IMemberGroup? GetById(Guid id);
+
+    IEnumerable GetByIds(IEnumerable ids);
+
+    IMemberGroup? GetByName(string? name);
+
+    void Save(IMemberGroup memberGroup);
+
+    void Delete(IMemberGroup memberGroup);
 }
diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs
index d6e0480091..ec600efab7 100644
--- a/src/Umbraco.Core/Services/IMemberService.cs
+++ b/src/Umbraco.Core/Services/IMemberService.cs
@@ -1,206 +1,280 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the MemberService, which is an easy access to operations involving (umbraco) members.
+/// 
+public interface IMemberService : IMembershipMemberService
 {
     /// 
-    /// Defines the MemberService, which is an easy access to operations involving (umbraco) members.
+    ///     Gets a list of paged  objects
     /// 
-    public interface IMemberService : IMembershipMemberService
-    {
-        /// 
-        /// Gets a list of paged  objects
-        /// 
-        /// An  can be of type  
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// Field to order by
-        /// Direction to order by
-        /// 
-        /// Search text filter
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection, string? memberTypeAlias = null, string filter = "");
+    /// An  can be of type  
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// Field to order by
+    /// Direction to order by
+    /// 
+    /// Search text filter
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAll(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        string orderBy,
+        Direction orderDirection,
+        string? memberTypeAlias = null,
+        string filter = "");
 
-        /// 
-        /// Gets a list of paged  objects
-        /// 
-        /// An  can be of type  
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// Field to order by
-        /// Direction to order by
-        /// Flag to indicate when ordering by system field
-        /// 
-        /// Search text filter
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection, bool orderBySystemField, string? memberTypeAlias, string filter);
+    /// 
+    ///     Gets a list of paged  objects
+    /// 
+    /// An  can be of type  
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// Field to order by
+    /// Direction to order by
+    /// Flag to indicate when ordering by system field
+    /// 
+    /// Search text filter
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAll(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        string orderBy,
+        Direction orderDirection,
+        bool orderBySystemField,
+        string? memberTypeAlias,
+        string filter);
 
-        /// 
-        /// Creates an  object without persisting it
-        /// 
-        /// This method is convenient for when you need to add properties to a new Member
-        /// before persisting it in order to limit the amount of times its saved.
-        /// Also note that the returned  will not have an Id until its saved.
-        /// Username of the Member to create
-        /// Email of the Member to create
-        /// Name of the Member to create
-        /// Alias of the MemberType the Member should be based on
-        /// 
-        IMember CreateMember(string username, string email, string name, string memberTypeAlias);
+    /// 
+    ///     Creates an  object without persisting it
+    /// 
+    /// 
+    ///     This method is convenient for when you need to add properties to a new Member
+    ///     before persisting it in order to limit the amount of times its saved.
+    ///     Also note that the returned  will not have an Id until its saved.
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    /// Name of the Member to create
+    /// Alias of the MemberType the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMember(string username, string email, string name, string memberTypeAlias);
 
-        /// 
-        /// Creates an  object without persisting it
-        /// 
-        /// This method is convenient for when you need to add properties to a new Member
-        /// before persisting it in order to limit the amount of times its saved.
-        /// Also note that the returned  will not have an Id until its saved.
-        /// Username of the Member to create
-        /// Email of the Member to create
-        /// Name of the Member to create
-        /// MemberType the Member should be based on
-        /// 
-        IMember CreateMember(string username, string email, string name, IMemberType memberType);
+    /// 
+    ///     Creates an  object without persisting it
+    /// 
+    /// 
+    ///     This method is convenient for when you need to add properties to a new Member
+    ///     before persisting it in order to limit the amount of times its saved.
+    ///     Also note that the returned  will not have an Id until its saved.
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    /// Name of the Member to create
+    /// MemberType the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMember(string username, string email, string name, IMemberType memberType);
 
-        /// 
-        /// Creates and persists a Member
-        /// 
-        /// Using this method will persist the Member object before its returned
-        /// meaning that it will have an Id available (unlike the CreateMember method)
-        /// Username of the Member to create
-        /// Email of the Member to create
-        /// Name of the Member to create
-        /// Alias of the MemberType the Member should be based on
-        /// 
-        IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias);
+    /// 
+    ///     Creates and persists a Member
+    /// 
+    /// 
+    ///     Using this method will persist the Member object before its returned
+    ///     meaning that it will have an Id available (unlike the CreateMember method)
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    /// Name of the Member to create
+    /// Alias of the MemberType the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias);
 
-        /// 
-        /// Creates and persists a Member
-        /// 
-        /// Using this method will persist the Member object before its returned
-        /// meaning that it will have an Id available (unlike the CreateMember method)
-        /// Username of the Member to create
-        /// Email of the Member to create
-        /// Name of the Member to create
-        /// MemberType the Member should be based on
-        /// 
-        IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
+    /// 
+    ///     Creates and persists a Member
+    /// 
+    /// 
+    ///     Using this method will persist the Member object before its returned
+    ///     meaning that it will have an Id available (unlike the CreateMember method)
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    /// Name of the Member to create
+    /// MemberType the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
 
-        /// 
-        /// Gets the count of Members by an optional MemberType alias
-        /// 
-        /// If no alias is supplied then the count for all Member will be returned
-        /// Optional alias for the MemberType when counting number of Members
-        ///  with number of Members
-        int Count(string? memberTypeAlias = null);
+    /// 
+    ///     Gets the count of Members by an optional MemberType alias
+    /// 
+    /// If no alias is supplied then the count for all Member will be returned
+    /// Optional alias for the MemberType when counting number of Members
+    ///  with number of Members
+    int Count(string? memberTypeAlias = null);
 
-        /// 
-        /// Checks if a Member with the id exists
-        /// 
-        /// Id of the Member
-        /// True if the Member exists otherwise False
-        bool Exists(int id);
+    /// 
+    ///     Checks if a Member with the id exists
+    /// 
+    /// Id of the Member
+    /// True if the Member exists otherwise False
+    bool Exists(int id);
 
-        /// 
-        /// Gets a Member by the unique key
-        /// 
-        /// The guid key corresponds to the unique id in the database
-        /// and the user id in the membership provider.
-        ///  Id
-        /// 
-        IMember? GetByKey(Guid id);
+    /// 
+    ///     Gets a Member by the unique key
+    /// 
+    /// 
+    ///     The guid key corresponds to the unique id in the database
+    ///     and the user id in the membership provider.
+    /// 
+    ///  Id
+    /// 
+    ///     
+    /// 
+    IMember? GetByKey(Guid id);
 
-        /// 
-        /// Gets a Member by its integer id
-        /// 
-        ///  Id
-        /// 
-        IMember? GetById(int id);
+    /// 
+    ///     Gets a Member by its integer id
+    /// 
+    ///  Id
+    /// 
+    ///     
+    /// 
+    IMember? GetById(int id);
 
-        /// 
-        /// Gets all Members for the specified MemberType alias
-        /// 
-        /// Alias of the MemberType
-        /// 
-        IEnumerable GetMembersByMemberType(string memberTypeAlias);
+    /// 
+    ///     Gets all Members for the specified MemberType alias
+    /// 
+    /// Alias of the MemberType
+    /// 
+    ///     
+    /// 
+    IEnumerable GetMembersByMemberType(string memberTypeAlias);
 
-        /// 
-        /// Gets all Members for the MemberType id
-        /// 
-        /// Id of the MemberType
-        /// 
-        IEnumerable GetMembersByMemberType(int memberTypeId);
+    /// 
+    ///     Gets all Members for the MemberType id
+    /// 
+    /// Id of the MemberType
+    /// 
+    ///     
+    /// 
+    IEnumerable GetMembersByMemberType(int memberTypeId);
 
-        /// 
-        /// Gets all Members within the specified MemberGroup name
-        /// 
-        /// Name of the MemberGroup
-        /// 
-        IEnumerable GetMembersByGroup(string memberGroupName);
+    /// 
+    ///     Gets all Members within the specified MemberGroup name
+    /// 
+    /// Name of the MemberGroup
+    /// 
+    ///     
+    /// 
+    IEnumerable GetMembersByGroup(string memberGroupName);
 
-        /// 
-        /// Gets all Members with the ids specified
-        /// 
-        /// If no Ids are specified all Members will be retrieved
-        /// Optional list of Member Ids
-        /// 
-        IEnumerable GetAllMembers(params int[] ids);
+    /// 
+    ///     Gets all Members with the ids specified
+    /// 
+    /// If no Ids are specified all Members will be retrieved
+    /// Optional list of Member Ids
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAllMembers(params int[] ids);
 
-        /// 
-        /// Delete Members of the specified MemberType id
-        /// 
-        /// Id of the MemberType
-        void DeleteMembersOfType(int memberTypeId);
+    /// 
+    ///     Delete Members of the specified MemberType id
+    /// 
+    /// Id of the MemberType
+    void DeleteMembersOfType(int memberTypeId);
 
-        /// 
-        /// Finds Members based on their display name
-        /// 
-        /// Display name to match
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
+    /// 
+    ///     Finds Members based on their display name
+    /// 
+    /// Display name to match
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable FindMembersByDisplayName(
+        string displayNameToMatch,
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        /// 
-        /// Gets a list of Members based on a property search
-        /// 
-        /// Alias of the PropertyType to search for
-        ///  Value to match
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact);
+    /// 
+    ///     Gets a list of Members based on a property search
+    /// 
+    /// Alias of the PropertyType to search for
+    ///  Value to match
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable? GetMembersByPropertyValue(
+        string propertyTypeAlias,
+        string value,
+        StringPropertyMatchType matchType = StringPropertyMatchType.Exact);
 
-        /// 
-        /// Gets a list of Members based on a property search
-        /// 
-        /// Alias of the PropertyType to search for
-        ///  Value to match
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
+    /// 
+    ///     Gets a list of Members based on a property search
+    /// 
+    /// Alias of the PropertyType to search for
+    ///  Value to match
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
 
-        /// 
-        /// Gets a list of Members based on a property search
-        /// 
-        /// Alias of the PropertyType to search for
-        ///  Value to match
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, bool value);
+    /// 
+    ///     Gets a list of Members based on a property search
+    /// 
+    /// Alias of the PropertyType to search for
+    ///  Value to match
+    /// 
+    ///     
+    /// 
+    IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, bool value);
 
-        /// 
-        /// Gets a list of Members based on a property search
-        /// 
-        /// Alias of the PropertyType to search for
-        ///  Value to match
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
-    }
+    /// 
+    ///     Gets a list of Members based on a property search
+    /// 
+    /// Alias of the PropertyType to search for
+    ///  Value to match
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
 }
diff --git a/src/Umbraco.Core/Services/IMemberTypeService.cs b/src/Umbraco.Core/Services/IMemberTypeService.cs
index 4a52438d5e..6a70e620a1 100644
--- a/src/Umbraco.Core/Services/IMemberTypeService.cs
+++ b/src/Umbraco.Core/Services/IMemberTypeService.cs
@@ -1,12 +1,11 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Manages  objects.
+/// 
+public interface IMemberTypeService : IContentTypeBaseService
 {
-    /// 
-    /// Manages  objects.
-    /// 
-    public interface IMemberTypeService : IContentTypeBaseService
-    {
-        string GetDefault();
-    }
+    string GetDefault();
 }
diff --git a/src/Umbraco.Core/Services/IMembershipMemberService.cs b/src/Umbraco.Core/Services/IMembershipMemberService.cs
index 94dbbf3da9..dc96535f8b 100644
--- a/src/Umbraco.Core/Services/IMembershipMemberService.cs
+++ b/src/Umbraco.Core/Services/IMembershipMemberService.cs
@@ -1,171 +1,204 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines part of the MemberService, which is specific to methods used by the membership provider.
+/// 
+/// 
+///     Idea is to have this as an isolated interface so that it can be easily 'replaced' in the membership provider
+///     implementation.
+/// 
+public interface IMembershipMemberService : IMembershipMemberService, IMembershipRoleService
 {
     /// 
-    /// Defines part of the MemberService, which is specific to methods used by the membership provider.
+    ///     Creates and persists a new Member
+    /// 
+    /// Username of the Member to create
+    /// Email of the Member to create
+    ///  which the Member should be based on
+    /// 
+    ///     
+    /// 
+    IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType);
+}
+
+/// 
+///     Defines part of the UserService/MemberService, which is specific to methods used by the membership provider.
+///     The generic type is restricted to . The implementation of this interface  uses
+///     either  for the MemberService or  for the UserService.
+/// 
+/// 
+///     Idea is to have this as an isolated interface so that it can be easily 'replaced' in the membership provider
+///     implementation.
+/// 
+public interface IMembershipMemberService : IService
+    where T : class, IMembershipUser
+{
+    /// 
+    ///     Gets the total number of Members or Users based on the count type
     /// 
     /// 
-    /// Idea is to have this as an isolated interface so that it can be easily 'replaced' in the membership provider implementation.
+    ///     The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any
+    ///     members
+    ///     that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact
+    ///     science
+    ///     but that is how MS have made theirs so we'll follow that principal.
     /// 
-    public interface IMembershipMemberService : IMembershipMemberService, IMembershipRoleService
-    {
-        /// 
-        /// Creates and persists a new Member
-        /// 
-        /// Username of the Member to create
-        /// Email of the Member to create
-        ///  which the Member should be based on
-        /// 
-        IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType);
-    }
+    ///  to count by
+    ///  with number of Members or Users for passed in type
+    int GetCount(MemberCountType countType);
 
     /// 
-    /// Defines part of the UserService/MemberService, which is specific to methods used by the membership provider.
-    /// The generic type is restricted to . The implementation of this interface  uses
-    /// either  for the MemberService or  for the UserService.
+    ///     Checks if a Member with the username exists
     /// 
+    /// Username to check
+    /// True if the Member exists otherwise False
+    bool Exists(string username);
+
+    /// 
+    ///     Creates and persists a new 
+    /// 
+    /// An  can be of type  or 
+    /// Username of the  to create
+    /// Email of the  to create
+    /// 
+    ///     This value should be the encoded/encrypted/hashed value for the password that will be
+    ///     stored in the database
+    /// 
+    /// Alias of the Type
+    /// 
+    ///     
+    /// 
+    T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias);
+
+    /// 
+    ///     Creates and persists a new 
+    /// 
+    /// An  can be of type  or 
+    /// Username of the  to create
+    /// Email of the  to create
+    /// 
+    ///     This value should be the encoded/encrypted/hashed value for the password that will be
+    ///     stored in the database
+    /// 
+    /// Alias of the Type
+    /// IsApproved of the  to create
+    /// 
+    ///     
+    /// 
+    T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved);
+
+    /// 
+    ///     Gets an  by its provider key
+    /// 
+    /// An  can be of type  or 
+    /// Id to use for retrieval
+    /// 
+    ///     
+    /// 
+    T? GetByProviderKey(object id);
+
+    /// 
+    ///     Get an  by email
+    /// 
+    /// An  can be of type  or 
+    /// Email to use for retrieval
+    /// 
+    ///     
+    /// 
+    T? GetByEmail(string email);
+
+    /// 
+    ///     Get an  by username
+    /// 
+    /// An  can be of type  or 
+    /// Username to use for retrieval
+    /// 
+    ///     
+    /// 
+    T? GetByUsername(string? username);
+
+    /// 
+    ///     Deletes an 
+    /// 
+    /// An  can be of type  or 
+    ///  or  to Delete
+    void Delete(T membershipUser);
+
+    /// 
+    ///     Sets the last login date for the member if they are found by username
+    /// 
+    /// 
+    /// 
     /// 
-    /// Idea is to have this as an isolated interface so that it can be easily 'replaced' in the membership provider implementation.
+    ///     This is a specialized method because whenever a member logs in, the membership provider requires us to set the
+    ///     'online' which requires
+    ///     updating their login date. This operation must be fast and cannot use database locks which is fine if we are only
+    ///     executing a single query
+    ///     for this data since there won't be any other data contention issues.
     /// 
-    public interface IMembershipMemberService : IService
-        where T : class, IMembershipUser
-    {
-        /// 
-        /// Gets the total number of Members or Users based on the count type
-        /// 
-        /// 
-        /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members
-        /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science
-        /// but that is how MS have made theirs so we'll follow that principal.
-        /// 
-        ///  to count by
-        ///  with number of Members or Users for passed in type
-        int GetCount(MemberCountType countType);
+    void SetLastLogin(string username, DateTime date);
 
-        /// 
-        /// Checks if a Member with the username exists
-        /// 
-        /// Username to check
-        /// True if the Member exists otherwise False
-        bool Exists(string username);
+    /// 
+    ///     Saves an 
+    /// 
+    /// An  can be of type  or 
+    ///  or  to Save
+    void Save(T entity);
 
-        /// 
-        /// Creates and persists a new 
-        /// 
-        /// An  can be of type  or 
-        /// Username of the  to create
-        /// Email of the  to create
-        /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database
-        /// Alias of the Type
-        /// 
-        T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias);
+    /// 
+    ///     Saves a list of  objects
+    /// 
+    /// An  can be of type  or 
+    ///  to save
+    void Save(IEnumerable entities);
 
-        /// 
-        /// Creates and persists a new 
-        /// 
-        /// An  can be of type  or 
-        /// Username of the  to create
-        /// Email of the  to create
-        /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database
-        /// Alias of the Type
-        /// IsApproved of the  to create
-        /// 
-        T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved);
+    /// 
+    ///     Finds a list of  objects by a partial email string
+    /// 
+    /// An  can be of type  or 
+    /// Partial email string to match
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        /// 
-        /// Gets an  by its provider key
-        /// 
-        /// An  can be of type  or 
-        /// Id to use for retrieval
-        /// 
-        T? GetByProviderKey(object id);
+    /// 
+    ///     Finds a list of  objects by a partial username
+    /// 
+    /// An  can be of type  or 
+    /// Partial username to match
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// 
+    ///     The type of match to make as . Default is
+    ///     
+    /// 
+    /// 
+    ///     
+    /// 
+    IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        /// 
-        /// Get an  by email
-        /// 
-        /// An  can be of type  or 
-        /// Email to use for retrieval
-        /// 
-        T? GetByEmail(string email);
-
-        /// 
-        /// Get an  by username
-        /// 
-        /// An  can be of type  or 
-        /// Username to use for retrieval
-        /// 
-        T? GetByUsername(string? username);
-
-        /// 
-        /// Deletes an 
-        /// 
-        /// An  can be of type  or 
-        ///  or  to Delete
-        void Delete(T membershipUser);
-
-        /// 
-        /// Sets the last login date for the member if they are found by username
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This is a specialized method because whenever a member logs in, the membership provider requires us to set the 'online' which requires
-        /// updating their login date. This operation must be fast and cannot use database locks which is fine if we are only executing a single query
-        /// for this data since there won't be any other data contention issues.
-        /// 
-        void SetLastLogin(string username, DateTime date);
-
-        /// 
-        /// Saves an 
-        /// 
-        /// An  can be of type  or 
-        ///  or  to Save
-        void Save(T entity);
-
-        /// 
-        /// Saves a list of  objects
-        /// 
-        /// An  can be of type  or 
-        ///  to save
-        void Save(IEnumerable entities);
-
-        /// 
-        /// Finds a list of  objects by a partial email string
-        /// 
-        /// An  can be of type  or 
-        /// Partial email string to match
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
-
-        /// 
-        /// Finds a list of  objects by a partial username
-        /// 
-        /// An  can be of type  or 
-        /// Partial username to match
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// The type of match to make as . Default is 
-        /// 
-        IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
-
-        /// 
-        /// Gets a list of paged  objects
-        /// 
-        /// An  can be of type  or 
-        /// Current page index
-        /// Size of the page
-        /// Total number of records found (out)
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords);
-    }
+    /// 
+    ///     Gets a list of paged  objects
+    /// 
+    /// An  can be of type  or 
+    /// Current page index
+    /// Size of the page
+    /// Total number of records found (out)
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords);
 }
diff --git a/src/Umbraco.Core/Services/IMembershipRoleService.cs b/src/Umbraco.Core/Services/IMembershipRoleService.cs
index 5c62a84973..538ae4fb8c 100644
--- a/src/Umbraco.Core/Services/IMembershipRoleService.cs
+++ b/src/Umbraco.Core/Services/IMembershipRoleService.cs
@@ -1,52 +1,49 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IMembershipRoleService
+    where T : class, IMembershipUser
 {
-    public interface IMembershipRoleService
-        where T : class, IMembershipUser
-    {
-        void AddRole(string roleName);
+    void AddRole(string roleName);
 
-        IEnumerable GetAllRoles();
+    IEnumerable GetAllRoles();
 
-        IEnumerable GetAllRoles(int memberId);
+    IEnumerable GetAllRoles(int memberId);
 
-        IEnumerable GetAllRoles(string username);
+    IEnumerable GetAllRoles(string username);
 
-        IEnumerable GetAllRolesIds();
+    IEnumerable GetAllRolesIds();
 
-        IEnumerable GetAllRolesIds(int memberId);
+    IEnumerable GetAllRolesIds(int memberId);
 
-        IEnumerable GetAllRolesIds(string username);
+    IEnumerable GetAllRolesIds(string username);
 
-        IEnumerable GetMembersInRole(string roleName);
+    IEnumerable GetMembersInRole(string roleName);
 
-        IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
+    IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith);
 
-        bool DeleteRole(string roleName, bool throwIfBeingUsed);
+    bool DeleteRole(string roleName, bool throwIfBeingUsed);
 
-        void AssignRole(string username, string roleName);
+    void AssignRole(string username, string roleName);
 
-        void AssignRoles(string[] usernames, string[] roleNames);
+    void AssignRoles(string[] usernames, string[] roleNames);
 
-        void DissociateRole(string username, string roleName);
+    void DissociateRole(string username, string roleName);
 
-        void DissociateRoles(string[] usernames, string[] roleNames);
+    void DissociateRoles(string[] usernames, string[] roleNames);
 
-        void AssignRole(int memberId, string roleName);
+    void AssignRole(int memberId, string roleName);
 
-        void AssignRoles(int[] memberIds, string[] roleNames);
+    void AssignRoles(int[] memberIds, string[] roleNames);
 
-        void DissociateRole(int memberId, string roleName);
+    void DissociateRole(int memberId, string roleName);
 
-        void DissociateRoles(int[] memberIds, string[] roleNames);
+    void DissociateRoles(int[] memberIds, string[] roleNames);
 
-        void ReplaceRoles(string[] usernames, string[] roleNames);
+    void ReplaceRoles(string[] usernames, string[] roleNames);
 
-        void ReplaceRoles(int[] memberIds, string[] roleNames);
-
-    }
+    void ReplaceRoles(int[] memberIds, string[] roleNames);
 }
diff --git a/src/Umbraco.Core/Services/IMembershipUserService.cs b/src/Umbraco.Core/Services/IMembershipUserService.cs
index a2aca2821e..7a8dc2023f 100644
--- a/src/Umbraco.Core/Services/IMembershipUserService.cs
+++ b/src/Umbraco.Core/Services/IMembershipUserService.cs
@@ -1,24 +1,27 @@
-using Umbraco.Cms.Core.Models.Membership;
+using Umbraco.Cms.Core.Models.Membership;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines part of the UserService, which is specific to methods used by the membership provider.
+/// 
+/// 
+///     Idea is to have this is an isolated interface so that it can be easily 'replaced' in the membership provider impl.
+/// 
+public interface IMembershipUserService : IMembershipMemberService
 {
     /// 
-    /// Defines part of the UserService, which is specific to methods used by the membership provider.
+    ///     Creates and persists a new User
     /// 
     /// 
-    /// Idea is to have this is an isolated interface so that it can be easily 'replaced' in the membership provider impl.
+    ///     The user will be saved in the database and returned with an Id.
+    ///     This method is convenient when you need to perform operations, which needs the
+    ///     Id of the user once its been created.
     /// 
-    public interface IMembershipUserService : IMembershipMemberService
-    {
-        /// 
-        /// Creates and persists a new User
-        /// 
-        /// The user will be saved in the database and returned with an Id.
-        /// This method is convenient when you need to perform operations, which needs the
-        /// Id of the user once its been created.
-        /// Username of the User to create
-        /// Email of the User to create
-        /// 
-        IUser CreateUserWithIdentity(string username, string email);
-    }
+    /// Username of the User to create
+    /// Email of the User to create
+    /// 
+    ///     
+    /// 
+    IUser CreateUserWithIdentity(string username, string email);
 }
diff --git a/src/Umbraco.Core/Services/IMetricsConsentService.cs b/src/Umbraco.Core/Services/IMetricsConsentService.cs
index e55cfd71d0..72f3ebe873 100644
--- a/src/Umbraco.Core/Services/IMetricsConsentService.cs
+++ b/src/Umbraco.Core/Services/IMetricsConsentService.cs
@@ -1,11 +1,10 @@
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IMetricsConsentService
 {
-    public interface IMetricsConsentService
-    {
-        TelemetryLevel GetConsentLevel();
+    TelemetryLevel GetConsentLevel();
 
-        void SetConsentLevel(TelemetryLevel telemetryLevel);
-    }
+    void SetConsentLevel(TelemetryLevel telemetryLevel);
 }
diff --git a/src/Umbraco.Core/Services/INodeCountService.cs b/src/Umbraco.Core/Services/INodeCountService.cs
index 50d91c1512..d442a7199f 100644
--- a/src/Umbraco.Core/Services/INodeCountService.cs
+++ b/src/Umbraco.Core/Services/INodeCountService.cs
@@ -1,10 +1,8 @@
-using System;
+namespace Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Core.Services
+public interface INodeCountService
 {
-    public interface INodeCountService
-    {
-        int GetNodeCount(Guid nodeType);
-        int GetMediaCount();
-    }
+    int GetNodeCount(Guid nodeType);
+
+    int GetMediaCount();
 }
diff --git a/src/Umbraco.Core/Services/INotificationService.cs b/src/Umbraco.Core/Services/INotificationService.cs
index cf65b1aa67..8472333d19 100644
--- a/src/Umbraco.Core/Services/INotificationService.cs
+++ b/src/Umbraco.Core/Services/INotificationService.cs
@@ -1,89 +1,92 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Models.Membership;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface INotificationService : IService
 {
-    public interface INotificationService : IService
-    {
-        /// 
-        /// Sends the notifications for the specified user regarding the specified nodes and action.
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        void SendNotifications(IUser operatingUser, IEnumerable entities, string? action, string? actionName, Uri siteUri,
-                               Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
-                               Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody);
+    /// 
+    ///     Sends the notifications for the specified user regarding the specified nodes and action.
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    void SendNotifications(
+        IUser operatingUser,
+        IEnumerable entities,
+        string? action,
+        string? actionName,
+        Uri siteUri,
+        Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
+        Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody);
 
-        /// 
-        /// Gets the notifications for the user
-        /// 
-        /// 
-        /// 
-        IEnumerable? GetUserNotifications(IUser user);
+    /// 
+    ///     Gets the notifications for the user
+    /// 
+    /// 
+    /// 
+    IEnumerable? GetUserNotifications(IUser user);
 
-        /// 
-        /// Gets the notifications for the user based on the specified node path
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's parent (ancestors)
-        /// 
-        IEnumerable? GetUserNotifications(IUser? user, string path);
+    /// 
+    ///     Gets the notifications for the user based on the specified node path
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     Notifications are inherited from the parent so any child node will also have notifications assigned based on it's
+    ///     parent (ancestors)
+    /// 
+    IEnumerable? GetUserNotifications(IUser? user, string path);
 
-        /// 
-        /// Returns the notifications for an entity
-        /// 
-        /// 
-        /// 
-        IEnumerable? GetEntityNotifications(IEntity entity);
+    /// 
+    ///     Returns the notifications for an entity
+    /// 
+    /// 
+    /// 
+    IEnumerable? GetEntityNotifications(IEntity entity);
 
-        /// 
-        /// Deletes notifications by entity
-        /// 
-        /// 
-        void DeleteNotifications(IEntity entity);
+    /// 
+    ///     Deletes notifications by entity
+    /// 
+    /// 
+    void DeleteNotifications(IEntity entity);
 
-        /// 
-        /// Deletes notifications by user
-        /// 
-        /// 
-        void DeleteNotifications(IUser user);
+    /// 
+    ///     Deletes notifications by user
+    /// 
+    /// 
+    void DeleteNotifications(IUser user);
 
-        /// 
-        /// Delete notifications by user and entity
-        /// 
-        /// 
-        /// 
-        void DeleteNotifications(IUser user, IEntity entity);
+    /// 
+    ///     Delete notifications by user and entity
+    /// 
+    /// 
+    /// 
+    void DeleteNotifications(IUser user, IEntity entity);
 
-        /// 
-        /// Sets the specific notifications for the user and entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This performs a full replace
-        /// 
-        IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions);
+    /// 
+    ///     Sets the specific notifications for the user and entity
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This performs a full replace
+    /// 
+    IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions);
 
-        /// 
-        /// Creates a new notification
-        /// 
-        /// 
-        /// 
-        /// The action letter - note: this is a string for future compatibility
-        /// 
-        Notification CreateNotification(IUser user, IEntity entity, string action);
-    }
+    /// 
+    ///     Creates a new notification
+    /// 
+    /// 
+    /// 
+    /// The action letter - note: this is a string for future compatibility
+    /// 
+    Notification CreateNotification(IUser user, IEntity entity, string action);
 }
diff --git a/src/Umbraco.Core/Services/IPackagingService.cs b/src/Umbraco.Core/Services/IPackagingService.cs
index 8429898354..40f39628be 100644
--- a/src/Umbraco.Core/Services/IPackagingService.cs
+++ b/src/Umbraco.Core/Services/IPackagingService.cs
@@ -1,63 +1,59 @@
-using System.Collections.Generic;
-using System.IO;
 using System.Xml.Linq;
 using Umbraco.Cms.Core.Models.Packaging;
 using Umbraco.Cms.Core.Packaging;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IPackagingService : IService
 {
-    public interface IPackagingService : IService
-    {
-        /// 
-        /// Returns a  result from an umbraco package file (zip)
-        /// 
-        /// 
-        /// 
-        CompiledPackage GetCompiledPackageInfo(XDocument packageXml);
+    /// 
+    ///     Returns a  result from an umbraco package file (zip)
+    /// 
+    /// 
+    /// 
+    CompiledPackage GetCompiledPackageInfo(XDocument packageXml);
 
-        /// 
-        /// Installs the data, entities, objects contained in an umbraco package file (zip)
-        /// 
-        /// 
-        /// 
-        InstallationSummary InstallCompiledPackageData(FileInfo packageXmlFile, int userId = Constants.Security.SuperUserId);
+    /// 
+    ///     Installs the data, entities, objects contained in an umbraco package file (zip)
+    /// 
+    /// 
+    /// 
+    InstallationSummary InstallCompiledPackageData(FileInfo packageXmlFile, int userId = Constants.Security.SuperUserId);
 
-        InstallationSummary InstallCompiledPackageData(XDocument? packageXml, int userId = Constants.Security.SuperUserId);
+    InstallationSummary InstallCompiledPackageData(XDocument? packageXml, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Returns the advertised installed packages
-        /// 
-        /// 
-        IEnumerable GetAllInstalledPackages();
+    /// 
+    ///     Returns the advertised installed packages
+    /// 
+    /// 
+    IEnumerable GetAllInstalledPackages();
 
-        InstalledPackage? GetInstalledPackageByName(string packageName);
+    InstalledPackage? GetInstalledPackageByName(string packageName);
 
-        /// 
-        /// Returns the created packages
-        /// 
-        /// 
-        IEnumerable GetAllCreatedPackages();
+    /// 
+    ///     Returns the created packages
+    /// 
+    /// 
+    IEnumerable GetAllCreatedPackages();
 
-        /// 
-        /// Returns a created package by id
-        /// 
-        /// 
-        /// 
-        PackageDefinition? GetCreatedPackageById(int id);
+    /// 
+    ///     Returns a created package by id
+    /// 
+    /// 
+    /// 
+    PackageDefinition? GetCreatedPackageById(int id);
 
-        void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId);
+    void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId);
 
-        /// 
-        /// Persists a package definition to storage
-        /// 
-        /// 
-        bool SaveCreatedPackage(PackageDefinition definition);
+    /// 
+    ///     Persists a package definition to storage
+    /// 
+    /// 
+    bool SaveCreatedPackage(PackageDefinition definition);
 
-        /// 
-        /// Creates the package file and returns it's physical path
-        /// 
-        /// 
-        string ExportCreatedPackage(PackageDefinition definition);
-
-    }
+    /// 
+    ///     Creates the package file and returns it's physical path
+    /// 
+    /// 
+    string ExportCreatedPackage(PackageDefinition definition);
 }
diff --git a/src/Umbraco.Core/Services/IPropertyValidationService.cs b/src/Umbraco.Core/Services/IPropertyValidationService.cs
index c2b8824340..e854d0f7f5 100644
--- a/src/Umbraco.Core/Services/IPropertyValidationService.cs
+++ b/src/Umbraco.Core/Services/IPropertyValidationService.cs
@@ -1,39 +1,37 @@
-using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.PropertyEditors;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IPropertyValidationService
 {
-    public interface IPropertyValidationService
-    {
-        /// 
-        /// Validates the content item's properties pass validation rules
-        /// 
-        bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact);
+    /// 
+    ///     Validates the content item's properties pass validation rules
+    /// 
+    bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact);
 
-        /// 
-        /// Gets a value indicating whether the property has valid values.
-        /// 
-        bool IsPropertyValid(IProperty property, string culture = "*", string segment = "*");
+    /// 
+    ///     Gets a value indicating whether the property has valid values.
+    /// 
+    bool IsPropertyValid(IProperty property, string culture = "*", string segment = "*");
 
-        /// 
-        /// Validates a property value.
-        /// 
-        IEnumerable ValidatePropertyValue(
-            IDataEditor editor,
-            IDataType dataType,
-            object? postedValue,
-            bool isRequired,
-            string? validationRegExp,
-            string? isRequiredMessage,
-            string? validationRegExpMessage);
+    /// 
+    ///     Validates a property value.
+    /// 
+    IEnumerable ValidatePropertyValue(
+        IDataEditor editor,
+        IDataType dataType,
+        object? postedValue,
+        bool isRequired,
+        string? validationRegExp,
+        string? isRequiredMessage,
+        string? validationRegExpMessage);
 
-        /// 
-        /// Validates a property value.
-        /// 
-        IEnumerable ValidatePropertyValue(
-            IPropertyType propertyType,
-            object? postedValue);
-    }
+    /// 
+    ///     Validates a property value.
+    /// 
+    IEnumerable ValidatePropertyValue(
+        IPropertyType propertyType,
+        object? postedValue);
 }
diff --git a/src/Umbraco.Core/Services/IPublicAccessService.cs b/src/Umbraco.Core/Services/IPublicAccessService.cs
index 96d8ca5d1b..fb4f080e03 100644
--- a/src/Umbraco.Core/Services/IPublicAccessService.cs
+++ b/src/Umbraco.Core/Services/IPublicAccessService.cs
@@ -1,73 +1,69 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IPublicAccessService : IService
 {
-    public interface IPublicAccessService : IService
-    {
+    /// 
+    ///     Gets all defined entries and associated rules
+    /// 
+    /// 
+    IEnumerable GetAll();
 
-        /// 
-        /// Gets all defined entries and associated rules
-        /// 
-        /// 
-        IEnumerable GetAll();
+    /// 
+    ///     Gets the entry defined for the content item's path
+    /// 
+    /// 
+    /// Returns null if no entry is found
+    PublicAccessEntry? GetEntryForContent(IContent content);
 
-        /// 
-        /// Gets the entry defined for the content item's path
-        /// 
-        /// 
-        /// Returns null if no entry is found
-        PublicAccessEntry? GetEntryForContent(IContent content);
+    /// 
+    ///     Gets the entry defined for the content item based on a content path
+    /// 
+    /// 
+    /// Returns null if no entry is found
+    PublicAccessEntry? GetEntryForContent(string contentPath);
 
-        /// 
-        /// Gets the entry defined for the content item based on a content path
-        /// 
-        /// 
-        /// Returns null if no entry is found
-        PublicAccessEntry? GetEntryForContent(string contentPath);
+    /// 
+    ///     Returns true if the content has an entry for it's path
+    /// 
+    /// 
+    /// 
+    Attempt IsProtected(IContent content);
 
-        /// 
-        /// Returns true if the content has an entry for it's path
-        /// 
-        /// 
-        /// 
-        Attempt IsProtected(IContent content);
+    /// 
+    ///     Returns true if the content has an entry based on a content path
+    /// 
+    /// 
+    /// 
+    Attempt IsProtected(string contentPath);
 
-        /// 
-        /// Returns true if the content has an entry based on a content path
-        /// 
-        /// 
-        /// 
-        Attempt IsProtected(string contentPath);
+    /// 
+    ///     Adds a rule if the entry doesn't already exist
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt?> AddRule(IContent content, string ruleType, string ruleValue);
 
-        /// 
-        /// Adds a rule if the entry doesn't already exist
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        Attempt?> AddRule(IContent content, string ruleType, string ruleValue);
+    /// 
+    ///     Removes a rule
+    /// 
+    /// 
+    /// 
+    /// 
+    Attempt RemoveRule(IContent content, string ruleType, string ruleValue);
 
-        /// 
-        /// Removes a rule
-        /// 
-        /// 
-        /// 
-        /// 
-        Attempt RemoveRule(IContent content, string ruleType, string ruleValue);
+    /// 
+    ///     Saves the entry
+    /// 
+    /// 
+    Attempt Save(PublicAccessEntry entry);
 
-        /// 
-        /// Saves the entry
-        /// 
-        /// 
-        Attempt Save(PublicAccessEntry entry);
-
-        /// 
-        /// Deletes the entry and all associated rules
-        /// 
-        /// 
-        Attempt Delete(PublicAccessEntry entry);
-
-    }
+    /// 
+    ///     Deletes the entry and all associated rules
+    /// 
+    /// 
+    Attempt Delete(PublicAccessEntry entry);
 }
diff --git a/src/Umbraco.Core/Services/IRedirectUrlService.cs b/src/Umbraco.Core/Services/IRedirectUrlService.cs
index 3c061db466..9da327a600 100644
--- a/src/Umbraco.Core/Services/IRedirectUrlService.cs
+++ b/src/Umbraco.Core/Services/IRedirectUrlService.cs
@@ -1,95 +1,91 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+/// 
+public interface IRedirectUrlService : IService
 {
     /// 
-    ///
+    ///     Registers a redirect URL.
     /// 
-    public interface IRedirectUrlService : IService
-    {
-        /// 
-        /// Registers a redirect URL.
-        /// 
-        /// The Umbraco URL route.
-        /// The content unique key.
-        /// The culture.
-        /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo.
-        void Register(string url, Guid contentKey, string? culture = null);
+    /// The Umbraco URL route.
+    /// The content unique key.
+    /// The culture.
+    /// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo.
+    void Register(string url, Guid contentKey, string? culture = null);
 
-        /// 
-        /// Deletes all redirect URLs for a given content.
-        /// 
-        /// The content unique key.
-        void DeleteContentRedirectUrls(Guid contentKey);
+    /// 
+    ///     Deletes all redirect URLs for a given content.
+    /// 
+    /// The content unique key.
+    void DeleteContentRedirectUrls(Guid contentKey);
 
-        /// 
-        /// Deletes a redirect URL.
-        /// 
-        /// The redirect URL to delete.
-        void Delete(IRedirectUrl redirectUrl);
+    /// 
+    ///     Deletes a redirect URL.
+    /// 
+    /// The redirect URL to delete.
+    void Delete(IRedirectUrl redirectUrl);
 
-        /// 
-        /// Deletes a redirect URL.
-        /// 
-        /// The redirect URL identifier.
-        void Delete(Guid id);
+    /// 
+    ///     Deletes a redirect URL.
+    /// 
+    /// The redirect URL identifier.
+    void Delete(Guid id);
 
-        /// 
-        /// Deletes all redirect URLs.
-        /// 
-        void DeleteAll();
+    /// 
+    ///     Deletes all redirect URLs.
+    /// 
+    void DeleteAll();
 
-        /// 
-        /// Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
-        /// 
-        /// The Umbraco redirect URL route.
-        /// The most recent redirect URLs corresponding to the route.
-        IRedirectUrl? GetMostRecentRedirectUrl(string url);
+    /// 
+    ///     Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
+    /// 
+    /// The Umbraco redirect URL route.
+    /// The most recent redirect URLs corresponding to the route.
+    IRedirectUrl? GetMostRecentRedirectUrl(string url);
 
-        /// 
-        /// Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
-        /// 
-        /// The Umbraco redirect URL route.
-        /// The culture of the request.
-        /// The most recent redirect URLs corresponding to the route.
-        IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture);
+    /// 
+    ///     Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route.
+    /// 
+    /// The Umbraco redirect URL route.
+    /// The culture of the request.
+    /// The most recent redirect URLs corresponding to the route.
+    IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture);
 
-        /// 
-        /// Gets all redirect URLs for a content item.
-        /// 
-        /// The content unique key.
-        /// All redirect URLs for the content item.
-        IEnumerable GetContentRedirectUrls(Guid contentKey);
+    /// 
+    ///     Gets all redirect URLs for a content item.
+    /// 
+    /// The content unique key.
+    /// All redirect URLs for the content item.
+    IEnumerable GetContentRedirectUrls(Guid contentKey);
 
-        /// 
-        /// Gets all redirect URLs.
-        /// 
-        /// The page index.
-        /// The page size.
-        /// The total count of redirect URLs.
-        /// The redirect URLs.
-        IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total);
+    /// 
+    ///     Gets all redirect URLs.
+    /// 
+    /// The page index.
+    /// The page size.
+    /// The total count of redirect URLs.
+    /// The redirect URLs.
+    IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total);
 
-        /// 
-        /// Gets all redirect URLs below a given content item.
-        /// 
-        /// The content unique identifier.
-        /// The page index.
-        /// The page size.
-        /// The total count of redirect URLs.
-        /// The redirect URLs.
-        IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total);
+    /// 
+    ///     Gets all redirect URLs below a given content item.
+    /// 
+    /// The content unique identifier.
+    /// The page index.
+    /// The page size.
+    /// The total count of redirect URLs.
+    /// The redirect URLs.
+    IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total);
 
-        /// 
-        /// Searches for all redirect URLs that contain a given search term in their URL property.
-        /// 
-        /// The term to search for.
-        /// The page index.
-        /// The page size.
-        /// The total count of redirect URLs.
-        /// The redirect URLs.
-        IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total);
-    }
+    /// 
+    ///     Searches for all redirect URLs that contain a given search term in their URL property.
+    /// 
+    /// The term to search for.
+    /// The page index.
+    /// The page size.
+    /// The total count of redirect URLs.
+    /// The redirect URLs.
+    IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total);
 }
diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs
index a0825611f7..6f8fa9b75a 100644
--- a/src/Umbraco.Core/Services/IRelationService.cs
+++ b/src/Umbraco.Core/Services/IRelationService.cs
@@ -1,357 +1,353 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Entities;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IRelationService : IService
 {
-    public interface IRelationService : IService
-    {
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// A  object
-        IRelation? GetById(int id);
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// A  object
+    IRelation? GetById(int id);
 
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// A  object
-        IRelationType? GetRelationTypeById(int id);
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// A  object
+    IRelationType? GetRelationTypeById(int id);
 
-        /// 
-        /// Gets a  by its Id
-        /// 
-        /// Id of the 
-        /// A  object
-        IRelationType? GetRelationTypeById(Guid id);
+    /// 
+    ///     Gets a  by its Id
+    /// 
+    /// Id of the 
+    /// A  object
+    IRelationType? GetRelationTypeById(Guid id);
 
-        /// 
-        /// Gets a  by its Alias
-        /// 
-        /// Alias of the 
-        /// A  object
-        IRelationType? GetRelationTypeByAlias(string alias);
+    /// 
+    ///     Gets a  by its Alias
+    /// 
+    /// Alias of the 
+    /// A  object
+    IRelationType? GetRelationTypeByAlias(string alias);
 
-        /// 
-        /// Gets all  objects
-        /// 
-        /// Optional array of integer ids to return relations for
-        /// An enumerable list of  objects
-        IEnumerable GetAllRelations(params int[] ids);
+    /// 
+    ///     Gets all  objects
+    /// 
+    /// Optional array of integer ids to return relations for
+    /// An enumerable list of  objects
+    IEnumerable GetAllRelations(params int[] ids);
 
-        /// 
-        /// Gets all  objects by their 
-        /// 
-        ///  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetAllRelationsByRelationType(IRelationType relationType);
+    /// 
+    ///     Gets all  objects by their 
+    /// 
+    ///  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetAllRelationsByRelationType(IRelationType relationType);
 
-        /// 
-        /// Gets all  objects by their 's Id
-        /// 
-        /// Id of the  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetAllRelationsByRelationType(int relationTypeId);
+    /// 
+    ///     Gets all  objects by their 's Id
+    /// 
+    /// Id of the  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetAllRelationsByRelationType(int relationTypeId);
 
-        /// 
-        /// Gets all  objects
-        /// 
-        /// Optional array of integer ids to return relationtypes for
-        /// An enumerable list of  objects
-        IEnumerable GetAllRelationTypes(params int[] ids);
+    /// 
+    ///     Gets all  objects
+    /// 
+    /// Optional array of integer ids to return relationtypes for
+    /// An enumerable list of  objects
+    IEnumerable GetAllRelationTypes(params int[] ids);
 
-        /// 
-        /// Gets a list of  objects by their parent Id
-        /// 
-        /// Id of the parent to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetByParentId(int id);
+    /// 
+    ///     Gets a list of  objects by their parent Id
+    /// 
+    /// Id of the parent to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetByParentId(int id);
 
-        /// 
-        /// Gets a list of  objects by their parent Id
-        /// 
-        /// Id of the parent to retrieve relations for
-        /// Alias of the type of relation to retrieve
-        /// An enumerable list of  objects
-        IEnumerable? GetByParentId(int id, string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by their parent Id
+    /// 
+    /// Id of the parent to retrieve relations for
+    /// Alias of the type of relation to retrieve
+    /// An enumerable list of  objects
+    IEnumerable? GetByParentId(int id, string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by their parent entity
-        /// 
-        /// Parent Entity to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetByParent(IUmbracoEntity parent);
+    /// 
+    ///     Gets a list of  objects by their parent entity
+    /// 
+    /// Parent Entity to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetByParent(IUmbracoEntity parent);
 
-        /// 
-        /// Gets a list of  objects by their parent entity
-        /// 
-        /// Parent Entity to retrieve relations for
-        /// Alias of the type of relation to retrieve
-        /// An enumerable list of  objects
-        IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by their parent entity
+    /// 
+    /// Parent Entity to retrieve relations for
+    /// Alias of the type of relation to retrieve
+    /// An enumerable list of  objects
+    IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by their child Id
-        /// 
-        /// Id of the child to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByChildId(int id);
+    /// 
+    ///     Gets a list of  objects by their child Id
+    /// 
+    /// Id of the child to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByChildId(int id);
 
-        /// 
-        /// Gets a list of  objects by their child Id
-        /// 
-        /// Id of the child to retrieve relations for
-        /// Alias of the type of relation to retrieve
-        /// An enumerable list of  objects
-        IEnumerable GetByChildId(int id, string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by their child Id
+    /// 
+    /// Id of the child to retrieve relations for
+    /// Alias of the type of relation to retrieve
+    /// An enumerable list of  objects
+    IEnumerable GetByChildId(int id, string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by their child Entity
-        /// 
-        /// Child Entity to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByChild(IUmbracoEntity child);
+    /// 
+    ///     Gets a list of  objects by their child Entity
+    /// 
+    /// Child Entity to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByChild(IUmbracoEntity child);
 
-        /// 
-        /// Gets a list of  objects by their child Entity
-        /// 
-        /// Child Entity to retrieve relations for
-        /// Alias of the type of relation to retrieve
-        /// An enumerable list of  objects
-        IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by their child Entity
+    /// 
+    /// Child Entity to retrieve relations for
+    /// Alias of the type of relation to retrieve
+    /// An enumerable list of  objects
+    IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by their child or parent Id.
-        /// Using this method will get you all relations regards of it being a child or parent relation.
-        /// 
-        /// Id of the child or parent to retrieve relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByParentOrChildId(int id);
+    /// 
+    ///     Gets a list of  objects by their child or parent Id.
+    ///     Using this method will get you all relations regards of it being a child or parent relation.
+    /// 
+    /// Id of the child or parent to retrieve relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByParentOrChildId(int id);
 
-        IEnumerable GetByParentOrChildId(int id, string relationTypeAlias);
+    IEnumerable GetByParentOrChildId(int id, string relationTypeAlias);
 
-        /// 
-        /// Gets a relation by the unique combination of parentId, childId and relationType.
-        /// 
-        /// The id of the parent item.
-        /// The id of the child item.
-        /// The RelationType.
-        /// The relation or null
-        IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType);
+    /// 
+    ///     Gets a relation by the unique combination of parentId, childId and relationType.
+    /// 
+    /// The id of the parent item.
+    /// The id of the child item.
+    /// The RelationType.
+    /// The relation or null
+    IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType);
 
-        /// 
-        /// Gets a list of  objects by the Name of the 
-        /// 
-        /// Name of the  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByRelationTypeName(string relationTypeName);
+    /// 
+    ///     Gets a list of  objects by the Name of the 
+    /// 
+    /// Name of the  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByRelationTypeName(string relationTypeName);
 
-        /// 
-        /// Gets a list of  objects by the Alias of the 
-        /// 
-        /// Alias of the  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable GetByRelationTypeAlias(string relationTypeAlias);
+    /// 
+    ///     Gets a list of  objects by the Alias of the 
+    /// 
+    /// Alias of the  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable GetByRelationTypeAlias(string relationTypeAlias);
 
-        /// 
-        /// Gets a list of  objects by the Id of the 
-        /// 
-        /// Id of the  to retrieve Relations for
-        /// An enumerable list of  objects
-        IEnumerable? GetByRelationTypeId(int relationTypeId);
+    /// 
+    ///     Gets a list of  objects by the Id of the 
+    /// 
+    /// Id of the  to retrieve Relations for
+    /// An enumerable list of  objects
+    IEnumerable? GetByRelationTypeId(int relationTypeId);
 
-        /// 
-        /// Gets a paged result of 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null);
+    /// 
+    ///     Gets a paged result of 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null);
 
-        /// 
-        /// Gets the Child object from a Relation as an 
-        /// 
-        /// Relation to retrieve child object from
-        /// An 
-        IUmbracoEntity? GetChildEntityFromRelation(IRelation relation);
+    /// 
+    ///     Gets the Child object from a Relation as an 
+    /// 
+    /// Relation to retrieve child object from
+    /// An 
+    IUmbracoEntity? GetChildEntityFromRelation(IRelation relation);
 
-        /// 
-        /// Gets the Parent object from a Relation as an 
-        /// 
-        /// Relation to retrieve parent object from
-        /// An 
-        IUmbracoEntity? GetParentEntityFromRelation(IRelation relation);
+    /// 
+    ///     Gets the Parent object from a Relation as an 
+    /// 
+    /// Relation to retrieve parent object from
+    /// An 
+    IUmbracoEntity? GetParentEntityFromRelation(IRelation relation);
 
-        /// 
-        /// Gets the Parent and Child objects from a Relation as a "/> with .
-        /// 
-        /// Relation to retrieve parent and child object from
-        /// Returns a Tuple with Parent (item1) and Child (item2)
-        Tuple? GetEntitiesFromRelation(IRelation relation);
+    /// 
+    ///     Gets the Parent and Child objects from a Relation as a "/> with .
+    /// 
+    /// Relation to retrieve parent and child object from
+    /// Returns a Tuple with Parent (item1) and Child (item2)
+    Tuple? GetEntitiesFromRelation(IRelation relation);
 
-        /// 
-        /// Gets the Child objects from a list of Relations as a list of  objects.
-        /// 
-        /// List of relations to retrieve child objects from
-        /// An enumerable list of 
-        IEnumerable GetChildEntitiesFromRelations(IEnumerable relations);
+    /// 
+    ///     Gets the Child objects from a list of Relations as a list of  objects.
+    /// 
+    /// List of relations to retrieve child objects from
+    /// An enumerable list of 
+    IEnumerable GetChildEntitiesFromRelations(IEnumerable relations);
 
-        /// 
-        /// Gets the Parent objects from a list of Relations as a list of  objects.
-        /// 
-        /// List of relations to retrieve parent objects from
-        /// An enumerable list of 
-        IEnumerable GetParentEntitiesFromRelations(IEnumerable relations);
+    /// 
+    ///     Gets the Parent objects from a list of Relations as a list of  objects.
+    /// 
+    /// List of relations to retrieve parent objects from
+    /// An enumerable list of 
+    IEnumerable GetParentEntitiesFromRelations(IEnumerable relations);
 
-        /// 
-        /// Returns paged parent entities for a related child id
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// An enumerable list of 
-        IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
+    /// 
+    ///     Returns paged parent entities for a related child id
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// An enumerable list of 
+    IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
 
-        /// 
-        /// Returns paged child entities for a related parent id
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// An enumerable list of 
-        IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
+    /// 
+    ///     Returns paged child entities for a related parent id
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// An enumerable list of 
+    IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
 
-        /// 
-        /// Gets the Parent and Child objects from a list of Relations as a list of  objects.
-        /// 
-        /// List of relations to retrieve parent and child objects from
-        /// An enumerable list of  with 
-        IEnumerable> GetEntitiesFromRelations(IEnumerable relations);
+    /// 
+    ///     Gets the Parent and Child objects from a list of Relations as a list of  objects.
+    /// 
+    /// List of relations to retrieve parent and child objects from
+    /// An enumerable list of  with 
+    IEnumerable> GetEntitiesFromRelations(IEnumerable relations);
 
-        /// 
-        /// Relates two objects by their entity Ids.
-        /// 
-        /// Id of the parent
-        /// Id of the child
-        /// The type of relation to create
-        /// The created 
-        IRelation Relate(int parentId, int childId, IRelationType relationType);
+    /// 
+    ///     Relates two objects by their entity Ids.
+    /// 
+    /// Id of the parent
+    /// Id of the child
+    /// The type of relation to create
+    /// The created 
+    IRelation Relate(int parentId, int childId, IRelationType relationType);
 
-        /// 
-        /// Relates two objects that are based on the  interface.
-        /// 
-        /// Parent entity
-        /// Child entity
-        /// The type of relation to create
-        /// The created 
-        IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType);
+    /// 
+    ///     Relates two objects that are based on the  interface.
+    /// 
+    /// Parent entity
+    /// Child entity
+    /// The type of relation to create
+    /// The created 
+    IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType);
 
-        /// 
-        /// Relates two objects by their entity Ids.
-        /// 
-        /// Id of the parent
-        /// Id of the child
-        /// Alias of the type of relation to create
-        /// The created 
-        IRelation Relate(int parentId, int childId, string relationTypeAlias);
+    /// 
+    ///     Relates two objects by their entity Ids.
+    /// 
+    /// Id of the parent
+    /// Id of the child
+    /// Alias of the type of relation to create
+    /// The created 
+    IRelation Relate(int parentId, int childId, string relationTypeAlias);
 
-        /// 
-        /// Relates two objects that are based on the  interface.
-        /// 
-        /// Parent entity
-        /// Child entity
-        /// Alias of the type of relation to create
-        /// The created 
-        IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias);
+    /// 
+    ///     Relates two objects that are based on the  interface.
+    /// 
+    /// Parent entity
+    /// Child entity
+    /// Alias of the type of relation to create
+    /// The created 
+    IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias);
 
-        /// 
-        /// Checks whether any relations exists for the passed in .
-        /// 
-        ///  to check for relations
-        /// Returns True if any relations exists for the given , otherwise False
-        bool HasRelations(IRelationType relationType);
+    /// 
+    ///     Checks whether any relations exists for the passed in .
+    /// 
+    ///  to check for relations
+    /// 
+    ///     Returns True if any relations exists for the given , otherwise False
+    /// 
+    bool HasRelations(IRelationType relationType);
 
-        /// 
-        /// Checks whether any relations exists for the passed in Id.
-        /// 
-        /// Id of an object to check relations for
-        /// Returns True if any relations exists with the given Id, otherwise False
-        bool IsRelated(int id);
+    /// 
+    ///     Checks whether any relations exists for the passed in Id.
+    /// 
+    /// Id of an object to check relations for
+    /// Returns True if any relations exists with the given Id, otherwise False
+    bool IsRelated(int id);
 
-        /// 
-        /// Checks whether two items are related
-        /// 
-        /// Id of the Parent relation
-        /// Id of the Child relation
-        /// Returns True if any relations exists with the given Ids, otherwise False
-        bool AreRelated(int parentId, int childId);
+    /// 
+    ///     Checks whether two items are related
+    /// 
+    /// Id of the Parent relation
+    /// Id of the Child relation
+    /// Returns True if any relations exists with the given Ids, otherwise False
+    bool AreRelated(int parentId, int childId);
 
-        /// 
-        /// Checks whether two items are related
-        /// 
-        /// Parent entity
-        /// Child entity
-        /// Returns True if any relations exist between the entities, otherwise False
-        bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child);
+    /// 
+    ///     Checks whether two items are related
+    /// 
+    /// Parent entity
+    /// Child entity
+    /// Returns True if any relations exist between the entities, otherwise False
+    bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child);
 
-        /// 
-        /// Checks whether two items are related
-        /// 
-        /// Parent entity
-        /// Child entity
-        /// Alias of the type of relation to create
-        /// Returns True if any relations exist between the entities, otherwise False
-        bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias);
+    /// 
+    ///     Checks whether two items are related
+    /// 
+    /// Parent entity
+    /// Child entity
+    /// Alias of the type of relation to create
+    /// Returns True if any relations exist between the entities, otherwise False
+    bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias);
 
-        /// 
-        /// Checks whether two items are related
-        /// 
-        /// Id of the Parent relation
-        /// Id of the Child relation
-        /// Alias of the type of relation to create
-        /// Returns True if any relations exist between the entities, otherwise False
-        bool AreRelated(int parentId, int childId, string relationTypeAlias);
+    /// 
+    ///     Checks whether two items are related
+    /// 
+    /// Id of the Parent relation
+    /// Id of the Child relation
+    /// Alias of the type of relation to create
+    /// Returns True if any relations exist between the entities, otherwise False
+    bool AreRelated(int parentId, int childId, string relationTypeAlias);
 
-        /// 
-        /// Saves a 
-        /// 
-        /// Relation to save
-        void Save(IRelation relation);
+    /// 
+    ///     Saves a 
+    /// 
+    /// Relation to save
+    void Save(IRelation relation);
 
-        void Save(IEnumerable relations);
+    void Save(IEnumerable relations);
 
-        /// 
-        /// Saves a 
-        /// 
-        /// RelationType to Save
-        void Save(IRelationType relationType);
+    /// 
+    ///     Saves a 
+    /// 
+    /// RelationType to Save
+    void Save(IRelationType relationType);
 
-        /// 
-        /// Deletes a 
-        /// 
-        /// Relation to Delete
-        void Delete(IRelation relation);
+    /// 
+    ///     Deletes a 
+    /// 
+    /// Relation to Delete
+    void Delete(IRelation relation);
 
-        /// 
-        /// Deletes a 
-        /// 
-        /// RelationType to Delete
-        void Delete(IRelationType relationType);
+    /// 
+    ///     Deletes a 
+    /// 
+    /// RelationType to Delete
+    void Delete(IRelationType relationType);
 
-        /// 
-        /// Deletes all  objects based on the passed in 
-        /// 
-        ///  to Delete Relations for
-        void DeleteRelationsOfType(IRelationType relationType);
-
-
-
-    }
+    /// 
+    ///     Deletes all  objects based on the passed in 
+    /// 
+    ///  to Delete Relations for
+    void DeleteRelationsOfType(IRelationType relationType);
 }
diff --git a/src/Umbraco.Core/Services/IRuntime.cs b/src/Umbraco.Core/Services/IRuntime.cs
index caa430ce1f..53ac51f585 100644
--- a/src/Umbraco.Core/Services/IRuntime.cs
+++ b/src/Umbraco.Core/Services/IRuntime.cs
@@ -1,22 +1,19 @@
-using System.Threading;
-using System.Threading.Tasks;
 using Microsoft.Extensions.Hosting;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the Umbraco runtime.
+/// 
+public interface IRuntime : IHostedService
 {
     /// 
-    /// Defines the Umbraco runtime.
+    ///     Gets the runtime state.
     /// 
-    public interface IRuntime : IHostedService
-    {
-        /// 
-        /// Gets the runtime state.
-        /// 
-        IRuntimeState State { get; }
+    IRuntimeState State { get; }
 
-        /// 
-        /// Stops and Starts the runtime using the original cancellation token.
-        /// 
-        Task RestartAsync();
-    }
+    /// 
+    ///     Stops and Starts the runtime using the original cancellation token.
+    /// 
+    Task RestartAsync();
 }
diff --git a/src/Umbraco.Core/Services/IRuntimeState.cs b/src/Umbraco.Core/Services/IRuntimeState.cs
index 3c765a0748..a576671010 100644
--- a/src/Umbraco.Core/Services/IRuntimeState.cs
+++ b/src/Umbraco.Core/Services/IRuntimeState.cs
@@ -1,65 +1,62 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Exceptions;
 using Umbraco.Cms.Core.Semver;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents the state of the Umbraco runtime.
+/// 
+public interface IRuntimeState
 {
     /// 
-    /// Represents the state of the Umbraco runtime.
+    ///     Gets the version of the executing code.
     /// 
-    public interface IRuntimeState
-    {
-        /// 
-        /// Gets the version of the executing code.
-        /// 
-        Version Version { get; }
+    Version Version { get; }
 
-        /// 
-        /// Gets the version comment of the executing code.
-        /// 
-        string VersionComment { get; }
+    /// 
+    ///     Gets the version comment of the executing code.
+    /// 
+    string VersionComment { get; }
 
-        /// 
-        /// Gets the semantic version of the executing code.
-        /// 
-        SemVersion SemanticVersion { get; }
+    /// 
+    ///     Gets the semantic version of the executing code.
+    /// 
+    SemVersion SemanticVersion { get; }
 
-        /// 
-        /// Gets the runtime level of execution.
-        /// 
-        RuntimeLevel Level { get; }
+    /// 
+    ///     Gets the runtime level of execution.
+    /// 
+    RuntimeLevel Level { get; }
 
-        /// 
-        /// Gets the reason for the runtime level of execution.
-        /// 
-        RuntimeLevelReason Reason { get; }
+    /// 
+    ///     Gets the reason for the runtime level of execution.
+    /// 
+    RuntimeLevelReason Reason { get; }
 
-        /// 
-        /// Gets the current migration state.
-        /// 
-        string? CurrentMigrationState { get; }
+    /// 
+    ///     Gets the current migration state.
+    /// 
+    string? CurrentMigrationState { get; }
 
-        /// 
-        /// Gets the final migration state.
-        /// 
-        string? FinalMigrationState { get; }
+    /// 
+    ///     Gets the final migration state.
+    /// 
+    string? FinalMigrationState { get; }
 
-        /// 
-        /// Gets the exception that caused the boot to fail.
-        /// 
-        BootFailedException? BootFailedException { get; }
+    /// 
+    ///     Gets the exception that caused the boot to fail.
+    /// 
+    BootFailedException? BootFailedException { get; }
 
-        /// 
-        /// Determines the runtime level.
-        /// 
-        void DetermineRuntimeLevel();
+    /// 
+    ///     Returns any state data that was collected during startup
+    /// 
+    IReadOnlyDictionary StartupState { get; }
 
-        void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception? bootFailedException = null);
+    /// 
+    ///     Determines the runtime level.
+    /// 
+    void DetermineRuntimeLevel();
 
-        /// 
-        /// Returns any state data that was collected during startup
-        /// 
-        IReadOnlyDictionary StartupState { get; }
-    }
+    void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception? bootFailedException = null);
 }
diff --git a/src/Umbraco.Core/Services/ISectionService.cs b/src/Umbraco.Core/Services/ISectionService.cs
index ded733963b..515896cafc 100644
--- a/src/Umbraco.Core/Services/ISectionService.cs
+++ b/src/Umbraco.Core/Services/ISectionService.cs
@@ -1,27 +1,25 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Sections;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface ISectionService
 {
-    public interface ISectionService
-    {
-        /// 
-        /// The cache storage for all applications
-        /// 
-        IEnumerable GetSections();
+    /// 
+    ///     The cache storage for all applications
+    /// 
+    IEnumerable GetSections();
 
-        /// 
-        /// Get the user group's allowed sections
-        /// 
-        /// 
-        /// 
-        IEnumerable GetAllowedSections(int userId);
+    /// 
+    ///     Get the user group's allowed sections
+    /// 
+    /// 
+    /// 
+    IEnumerable GetAllowedSections(int userId);
 
-        /// 
-        /// Gets the application by its alias.
-        /// 
-        /// The application alias.
-        /// 
-        ISection? GetByAlias(string appAlias);
-    }
+    /// 
+    ///     Gets the application by its alias.
+    /// 
+    /// The application alias.
+    /// 
+    ISection? GetByAlias(string appAlias);
 }
diff --git a/src/Umbraco.Core/Services/IServerRegistrationService.cs b/src/Umbraco.Core/Services/IServerRegistrationService.cs
index e469de9a06..4a08492079 100644
--- a/src/Umbraco.Core/Services/IServerRegistrationService.cs
+++ b/src/Umbraco.Core/Services/IServerRegistrationService.cs
@@ -1,57 +1,58 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Sync;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IServerRegistrationService
 {
-    public interface IServerRegistrationService
-    {
-        /// 
-        /// Touches a server to mark it as active; deactivate stale servers.
-        /// 
-        /// The server URL.
-        /// The time after which a server is considered stale.
-        void TouchServer(string serverAddress, TimeSpan staleTimeout);
+    /// 
+    ///     Touches a server to mark it as active; deactivate stale servers.
+    /// 
+    /// The server URL.
+    /// The time after which a server is considered stale.
+    void TouchServer(string serverAddress, TimeSpan staleTimeout);
 
-        /// 
-        /// Deactivates a server.
-        /// 
-        /// The server unique identity.
-        void DeactiveServer(string serverIdentity);
+    /// 
+    ///     Deactivates a server.
+    /// 
+    /// The server unique identity.
+    void DeactiveServer(string serverIdentity);
 
-        /// 
-        /// Deactivates stale servers.
-        /// 
-        /// The time after which a server is considered stale.
-        void DeactiveStaleServers(TimeSpan staleTimeout);
+    /// 
+    ///     Deactivates stale servers.
+    /// 
+    /// The time after which a server is considered stale.
+    void DeactiveStaleServers(TimeSpan staleTimeout);
 
-        /// 
-        /// Return all active servers.
-        /// 
-        /// A value indicating whether to force-refresh the cache.
-        /// All active servers.
-        /// By default this method will rely on the repository's cache, which is updated each
-        /// time the current server is touched, and the period depends on the configuration. Use the
-        ///  parameter to force a cache refresh and reload active servers
-        /// from the database.
-        IEnumerable? GetActiveServers(bool refresh = false);
+    /// 
+    ///     Return all active servers.
+    /// 
+    /// A value indicating whether to force-refresh the cache.
+    /// All active servers.
+    /// 
+    ///     By default this method will rely on the repository's cache, which is updated each
+    ///     time the current server is touched, and the period depends on the configuration. Use the
+    ///      parameter to force a cache refresh and reload active servers
+    ///     from the database.
+    /// 
+    IEnumerable? GetActiveServers(bool refresh = false);
 
-        /// 
-        /// Return all servers (active and inactive).
-        /// 
-        /// A value indicating whether to force-refresh the cache.
-        /// All servers.
-        /// By default this method will rely on the repository's cache, which is updated each
-        /// time the current server is touched, and the period depends on the configuration. Use the
-        ///  parameter to force a cache refresh and reload all servers
-        /// from the database.
-        IEnumerable GetServers(bool refresh = false);
+    /// 
+    ///     Return all servers (active and inactive).
+    /// 
+    /// A value indicating whether to force-refresh the cache.
+    /// All servers.
+    /// 
+    ///     By default this method will rely on the repository's cache, which is updated each
+    ///     time the current server is touched, and the period depends on the configuration. Use the
+    ///      parameter to force a cache refresh and reload all servers
+    ///     from the database.
+    /// 
+    IEnumerable GetServers(bool refresh = false);
 
-        /// 
-        /// Gets the role of the current server.
-        /// 
-        /// The role of the current server.
-        ServerRole GetCurrentServerRole();
-    }
+    /// 
+    ///     Gets the role of the current server.
+    /// 
+    /// The role of the current server.
+    ServerRole GetCurrentServerRole();
 }
diff --git a/src/Umbraco.Core/Services/IService.cs b/src/Umbraco.Core/Services/IService.cs
index 6ca00a8dbe..3147b34a56 100644
--- a/src/Umbraco.Core/Services/IService.cs
+++ b/src/Umbraco.Core/Services/IService.cs
@@ -1,10 +1,8 @@
-namespace Umbraco.Cms.Core.Services
-{
-    /// 
-    /// Marker interface for services, which is used to store difference services in a list or dictionary
-    /// 
-    public interface IService
-    {
+namespace Umbraco.Cms.Core.Services;
 
-    }
+/// 
+///     Marker interface for services, which is used to store difference services in a list or dictionary
+/// 
+public interface IService
+{
 }
diff --git a/src/Umbraco.Core/Services/ITagService.cs b/src/Umbraco.Core/Services/ITagService.cs
index 70c4ba81b6..5e2f164a35 100644
--- a/src/Umbraco.Core/Services/ITagService.cs
+++ b/src/Umbraco.Core/Services/ITagService.cs
@@ -1,98 +1,95 @@
-using System;
-using System.Collections.Generic;
-using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Tag service to query for tags in the tags db table. The tags returned are only relevant for published content &
+///     saved media or members
+/// 
+/// 
+///     If there is unpublished content with tags, those tags will not be contained.
+///     This service does not contain methods to query for content, media or members based on tags, those methods will be added
+///     to the content, media and member services respectively.
+/// 
+public interface ITagService : IService
 {
     /// 
-    /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members
+    ///     Gets a tagged entity.
     /// 
-    /// 
-    /// If there is unpublished content with tags, those tags will not be contained.
-    ///
-    /// This service does not contain methods to query for content, media or members based on tags, those methods will be added
-    /// to the content, media and member services respectively.
-    /// 
-    public interface ITagService : IService
-    {
-        /// 
-        /// Gets a tagged entity.
-        /// 
-        TaggedEntity? GetTaggedEntityById(int id);
+    TaggedEntity? GetTaggedEntityById(int id);
 
-        /// 
-        /// Gets a tagged entity.
-        /// 
-        TaggedEntity? GetTaggedEntityByKey(Guid key);
+    /// 
+    ///     Gets a tagged entity.
+    /// 
+    TaggedEntity? GetTaggedEntityByKey(Guid key);
 
-        /// 
-        /// Gets all documents tagged with any tag in the specified group.
-        /// 
-        IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null);
+    /// 
+    ///     Gets all documents tagged with any tag in the specified group.
+    /// 
+    IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null);
 
-        /// 
-        /// Gets all documents tagged with the specified tag.
-        /// 
-        IEnumerable GetTaggedContentByTag(string tag, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all documents tagged with the specified tag.
+    /// 
+    IEnumerable GetTaggedContentByTag(string tag, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all media tagged with any tag in the specified group.
-        /// 
-        IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null);
+    /// 
+    ///     Gets all media tagged with any tag in the specified group.
+    /// 
+    IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null);
 
-        /// 
-        /// Gets all media tagged with the specified tag.
-        /// 
-        IEnumerable GetTaggedMediaByTag(string tag, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all media tagged with the specified tag.
+    /// 
+    IEnumerable GetTaggedMediaByTag(string tag, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all members tagged with any tag in the specified group.
-        /// 
-        IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null);
+    /// 
+    ///     Gets all members tagged with any tag in the specified group.
+    /// 
+    IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null);
 
-        /// 
-        /// Gets all members tagged with the specified tag.
-        /// 
-        IEnumerable GetTaggedMembersByTag(string tag, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all members tagged with the specified tag.
+    /// 
+    IEnumerable GetTaggedMembersByTag(string tag, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags.
-        /// 
-        IEnumerable GetAllTags(string? group = null, string? culture = null);
+    /// 
+    ///     Gets all tags.
+    /// 
+    IEnumerable GetAllTags(string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all document tags.
-        /// 
-        IEnumerable GetAllContentTags(string? group = null, string? culture = null);
+    /// 
+    ///     Gets all document tags.
+    /// 
+    IEnumerable GetAllContentTags(string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all media tags.
-        /// 
-        IEnumerable GetAllMediaTags(string? group = null, string? culture = null);
+    /// 
+    ///     Gets all media tags.
+    /// 
+    IEnumerable GetAllMediaTags(string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all member tags.
-        /// 
-        IEnumerable GetAllMemberTags(string? group = null, string? culture = null);
+    /// 
+    ///     Gets all member tags.
+    /// 
+    IEnumerable GetAllMemberTags(string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags attached to an entity via a property.
-        /// 
-        IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all tags attached to an entity via a property.
+    /// 
+    IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags attached to an entity.
-        /// 
-        IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all tags attached to an entity.
+    /// 
+    IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags attached to an entity via a property.
-        /// 
-        IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null);
+    /// 
+    ///     Gets all tags attached to an entity via a property.
+    /// 
+    IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null);
 
-        /// 
-        /// Gets all tags attached to an entity.
-        /// 
-        IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null);
-    }
+    /// 
+    ///     Gets all tags attached to an entity.
+    /// 
+    IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null);
 }
diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
index dea99c0f6d..16b953c35a 100644
--- a/src/Umbraco.Core/Services/ITrackedReferencesService.cs
+++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
@@ -1,38 +1,46 @@
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface ITrackedReferencesService
 {
-    public interface ITrackedReferencesService
-    {
-        /// 
-        /// Gets a paged result of items which are in relation with the current item.
-        /// Basically, shows the items which depend on the current item.
-        /// 
-        /// The identifier of the entity to retrieve relations for.
-        /// The page index.
-        /// The page size.
-        /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
-        /// A paged result of  objects.
-        PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+    /// 
+    ///     Gets a paged result of items which are in relation with the current item.
+    ///     Basically, shows the items which depend on the current item.
+    /// 
+    /// The identifier of the entity to retrieve relations for.
+    /// The page index.
+    /// The page size.
+    /// 
+    ///     A boolean indicating whether to filter only the RelationTypes which are
+    ///     dependencies (isDependency field is set to true).
+    /// 
+    /// A paged result of  objects.
+    PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency);
 
-        /// 
-        /// Gets a paged result of the descending items that have any references, given a parent id.
-        /// 
-        /// The unique identifier of the parent to retrieve descendants for.
-        /// The page index.
-        /// The page size.
-        /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
-        /// A paged result of  objects.
-        PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+    /// 
+    ///     Gets a paged result of the descending items that have any references, given a parent id.
+    /// 
+    /// The unique identifier of the parent to retrieve descendants for.
+    /// The page index.
+    /// The page size.
+    /// 
+    ///     A boolean indicating whether to filter only the RelationTypes which are
+    ///     dependencies (isDependency field is set to true).
+    /// 
+    /// A paged result of  objects.
+    PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency);
 
-        /// 
-        /// Gets a paged result of items used in any kind of relation from selected integer ids.
-        /// 
-        /// The identifiers of the entities to check for relations.
-        /// The page index.
-        /// The page size.
-        /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
-        /// A paged result of  objects.
-        PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
-    }
+    /// 
+    ///     Gets a paged result of items used in any kind of relation from selected integer ids.
+    /// 
+    /// The identifiers of the entities to check for relations.
+    /// The page index.
+    /// The page size.
+    /// 
+    ///     A boolean indicating whether to filter only the RelationTypes which are
+    ///     dependencies (isDependency field is set to true).
+    /// 
+    /// A paged result of  objects.
+    PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
 }
diff --git a/src/Umbraco.Core/Services/ITreeService.cs b/src/Umbraco.Core/Services/ITreeService.cs
index b67e36e15b..d61fca066a 100644
--- a/src/Umbraco.Core/Services/ITreeService.cs
+++ b/src/Umbraco.Core/Services/ITreeService.cs
@@ -1,32 +1,30 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Trees;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents a service which manages section trees.
+/// 
+public interface ITreeService
 {
     /// 
-    /// Represents a service which manages section trees.
+    ///     Gets a tree.
     /// 
-    public interface ITreeService
-    {
-        /// 
-        /// Gets a tree.
-        /// 
-        /// The tree alias.
-        Tree? GetByAlias(string treeAlias);
+    /// The tree alias.
+    Tree? GetByAlias(string treeAlias);
 
-        /// 
-        /// Gets all trees.
-        /// 
-        IEnumerable GetAll(TreeUse use = TreeUse.Main);
+    /// 
+    ///     Gets all trees.
+    /// 
+    IEnumerable GetAll(TreeUse use = TreeUse.Main);
 
-        /// 
-        /// Gets all trees for a section.
-        /// 
-        IEnumerable GetBySection(string sectionAlias, TreeUse use = TreeUse.Main);
+    /// 
+    ///     Gets all trees for a section.
+    /// 
+    IEnumerable GetBySection(string sectionAlias, TreeUse use = TreeUse.Main);
 
-        /// 
-        /// Gets all trees for a section, grouped.
-        /// 
-        IDictionary> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main);
-    }
+    /// 
+    ///     Gets all trees for a section, grouped.
+    /// 
+    IDictionary> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main);
 }
diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs
index 30b221742c..d0509a9283 100644
--- a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs
+++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs
@@ -1,69 +1,66 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Service handling 2FA logins.
+/// 
+public interface ITwoFactorLoginService : IService
 {
     /// 
-    /// Service handling 2FA logins.
+    ///     Deletes all user logins - normally used when a member is deleted.
     /// 
-    public interface ITwoFactorLoginService : IService
-    {
-        /// 
-        /// Deletes all user logins - normally used when a member is deleted.
-        /// 
-        Task DeleteUserLoginsAsync(Guid userOrMemberKey);
+    Task DeleteUserLoginsAsync(Guid userOrMemberKey);
 
-        /// 
-        /// Checks whether 2FA is enabled for the user or member with the specified key.
-        /// 
-        Task IsTwoFactorEnabledAsync(Guid userOrMemberKey);
+    /// 
+    ///     Checks whether 2FA is enabled for the user or member with the specified key.
+    /// 
+    Task IsTwoFactorEnabledAsync(Guid userOrMemberKey);
 
-        /// 
-        /// Gets the secret for user or member and a specific provider.
-        /// 
-        Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName);
+    /// 
+    ///     Gets the secret for user or member and a specific provider.
+    /// 
+    Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName);
 
-        /// 
-        /// Gets the setup info for a specific user or member and a specific provider.
-        /// 
-        /// 
-        /// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by the provider.
-        /// 
-        Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName);
+    /// 
+    ///     Gets the setup info for a specific user or member and a specific provider.
+    /// 
+    /// 
+    ///     The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by
+    ///     the provider.
+    /// 
+    Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName);
 
-        /// 
-        /// Gets all registered providers names.
-        /// 
-        IEnumerable GetAllProviderNames();
+    /// 
+    ///     Gets all registered providers names.
+    /// 
+    IEnumerable GetAllProviderNames();
 
-        /// 
-        /// Disables the 2FA provider with the specified provider name for the specified user or member.
-        /// 
-        Task DisableAsync(Guid userOrMemberKey, string providerName);
+    /// 
+    ///     Disables the 2FA provider with the specified provider name for the specified user or member.
+    /// 
+    Task DisableAsync(Guid userOrMemberKey, string providerName);
 
-        /// 
-        /// Validates the setup of the provider using the secret and code.
-        /// 
-        bool ValidateTwoFactorSetup(string providerName, string secret, string code);
+    /// 
+    ///     Validates the setup of the provider using the secret and code.
+    /// 
+    bool ValidateTwoFactorSetup(string providerName, string secret, string code);
 
-        /// 
-        /// Saves the 2FA login information.
-        /// 
-        Task SaveAsync(TwoFactorLogin twoFactorLogin);
+    /// 
+    ///     Saves the 2FA login information.
+    /// 
+    Task SaveAsync(TwoFactorLogin twoFactorLogin);
 
-        /// 
-        /// Gets all the enabled 2FA providers for the user or member with the specified key.
-        /// 
-        Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey);
-    }
-
-    [Obsolete("This will be merged into ITwoFactorLoginService in Umbraco 11")]
-    public interface ITwoFactorLoginService2 : ITwoFactorLoginService
-    {
-        Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code);
-
-        Task ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code);
-    }
+    /// 
+    ///     Gets all the enabled 2FA providers for the user or member with the specified key.
+    /// 
+    Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey);
+}
+
+[Obsolete("This will be merged into ITwoFactorLoginService in Umbraco 11")]
+public interface ITwoFactorLoginService2 : ITwoFactorLoginService
+{
+    Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code);
+
+    Task ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code);
 }
diff --git a/src/Umbraco.Core/Services/IUpgradeService.cs b/src/Umbraco.Core/Services/IUpgradeService.cs
index 2e0f2a5f17..2f1e65f00a 100644
--- a/src/Umbraco.Core/Services/IUpgradeService.cs
+++ b/src/Umbraco.Core/Services/IUpgradeService.cs
@@ -1,10 +1,8 @@
-using System.Threading.Tasks;
 using Umbraco.Cms.Core.Semver;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IUpgradeService
 {
-    public interface IUpgradeService
-    {
-        Task CheckUpgrade(SemVersion version);
-    }
+    Task CheckUpgrade(SemVersion version);
 }
diff --git a/src/Umbraco.Core/Services/IUsageInformationService.cs b/src/Umbraco.Core/Services/IUsageInformationService.cs
index c6b2c68702..1d4caaa526 100644
--- a/src/Umbraco.Core/Services/IUsageInformationService.cs
+++ b/src/Umbraco.Core/Services/IUsageInformationService.cs
@@ -1,10 +1,8 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IUsageInformationService
 {
-    public interface IUsageInformationService
-    {
-        IEnumerable? GetDetailed();
-    }
+    IEnumerable? GetDetailed();
 }
diff --git a/src/Umbraco.Core/Services/IUserDataService.cs b/src/Umbraco.Core/Services/IUserDataService.cs
index e63ee3f697..0bb1d10cc4 100644
--- a/src/Umbraco.Core/Services/IUserDataService.cs
+++ b/src/Umbraco.Core/Services/IUserDataService.cs
@@ -1,10 +1,8 @@
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public interface IUserDataService
 {
-    public interface IUserDataService
-    {
-        IEnumerable GetUserData();
-    }
+    IEnumerable GetUserData();
 }
diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs
index 9a63fcf0ad..40a3fbd899 100644
--- a/src/Umbraco.Core/Services/IUserService.cs
+++ b/src/Umbraco.Core/Services/IUserService.cs
@@ -1,256 +1,289 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Persistence.Querying;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Defines the UserService, which is an easy access to operations involving  and eventually
+///     Users.
+/// 
+public interface IUserService : IMembershipUserService
 {
     /// 
-    /// Defines the UserService, which is an easy access to operations involving  and eventually Users.
+    ///     Creates a database entry for starting a new login session for a user
     /// 
-    public interface IUserService : IMembershipUserService
-    {
-        /// 
-        /// Creates a database entry for starting a new login session for a user
-        /// 
-        /// 
-        /// 
-        /// 
-        Guid CreateLoginSession(int userId, string requestingIpAddress);
+    /// 
+    /// 
+    /// 
+    Guid CreateLoginSession(int userId, string requestingIpAddress);
 
-        /// 
-        /// Validates that a user login session is valid/current and hasn't been closed
-        /// 
-        /// 
-        /// 
-        /// 
-        bool ValidateLoginSession(int userId, Guid sessionId);
+    /// 
+    ///     Validates that a user login session is valid/current and hasn't been closed
+    /// 
+    /// 
+    /// 
+    /// 
+    bool ValidateLoginSession(int userId, Guid sessionId);
 
-        /// 
-        /// Removes the session's validity
-        /// 
-        /// 
-        void ClearLoginSession(Guid sessionId);
+    /// 
+    ///     Removes the session's validity
+    /// 
+    /// 
+    void ClearLoginSession(Guid sessionId);
 
-        /// 
-        /// Removes all valid sessions for the user
-        /// 
-        /// 
-        int ClearLoginSessions(int userId);
+    /// 
+    ///     Removes all valid sessions for the user
+    /// 
+    /// 
+    int ClearLoginSessions(int userId);
 
-        /// 
-        /// This is basically facets of UserStates key = state, value = count
-        /// 
-        IDictionary GetUserStates();
+    /// 
+    ///     This is basically facets of UserStates key = state, value = count
+    /// 
+    IDictionary GetUserStates();
 
-        /// 
-        /// Get paged users
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// A filter to only include user that belong to these user groups
-        /// 
-        /// 
-        /// A filter to only include users that do not belong to these user groups
-        /// 
-        /// 
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection,
-            UserState[]? userState = null,
-            string[]? includeUserGroups = null,
-            string[]? excludeUserGroups = null,
-            IQuery? filter = null);
+    /// 
+    ///     Get paged users
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     A filter to only include user that belong to these user groups
+    /// 
+    /// 
+    ///     A filter to only include users that do not belong to these user groups
+    /// 
+    /// 
+    /// 
+    IEnumerable GetAll(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        string orderBy,
+        Direction orderDirection,
+        UserState[]? userState = null,
+        string[]? includeUserGroups = null,
+        string[]? excludeUserGroups = null,
+        IQuery? filter = null);
 
-        /// 
-        /// Get paged users
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// A filter to only include user that belong to these user groups
-        /// 
-        /// 
-        /// 
-        IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection,
-            UserState[]? userState = null,
-            string[]? userGroups = null,
-            string? filter = null);
+    /// 
+    ///     Get paged users
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     A filter to only include user that belong to these user groups
+    /// 
+    /// 
+    /// 
+    IEnumerable GetAll(
+        long pageIndex,
+        int pageSize,
+        out long totalRecords,
+        string orderBy,
+        Direction orderDirection,
+        UserState[]? userState = null,
+        string[]? userGroups = null,
+        string? filter = null);
 
-        /// 
-        /// Deletes or disables a User
-        /// 
-        ///  to delete
-        /// True to permanently delete the user, False to disable the user
-        void Delete(IUser user, bool deletePermanently);
+    /// 
+    ///     Deletes or disables a User
+    /// 
+    ///  to delete
+    /// True to permanently delete the user, False to disable the user
+    void Delete(IUser user, bool deletePermanently);
 
-        /// 
-        /// Gets an IProfile by User Id.
-        /// 
-        /// Id of the User to retrieve
-        /// 
-        IProfile? GetProfileById(int id);
+    /// 
+    ///     Gets an IProfile by User Id.
+    /// 
+    /// Id of the User to retrieve
+    /// 
+    ///     
+    /// 
+    IProfile? GetProfileById(int id);
 
-        /// 
-        /// Gets a profile by username
-        /// 
-        /// Username
-        /// 
-        IProfile? GetProfileByUserName(string username);
+    /// 
+    ///     Gets a profile by username
+    /// 
+    /// Username
+    /// 
+    ///     
+    /// 
+    IProfile? GetProfileByUserName(string username);
 
-        /// 
-        /// Gets a user by Id
-        /// 
-        /// Id of the user to retrieve
-        /// 
-        IUser? GetUserById(int id);
+    /// 
+    ///     Gets a user by Id
+    /// 
+    /// Id of the user to retrieve
+    /// 
+    ///     
+    /// 
+    IUser? GetUserById(int id);
 
-        /// 
-        /// Gets a users by Id
-        /// 
-        /// Ids of the users to retrieve
-        /// 
-        IEnumerable GetUsersById(params int[]? ids);
+    /// 
+    ///     Gets a users by Id
+    /// 
+    /// Ids of the users to retrieve
+    /// 
+    ///     
+    /// 
+    IEnumerable GetUsersById(params int[]? ids);
 
-        /// 
-        /// Removes a specific section from all user groups
-        /// 
-        /// This is useful when an entire section is removed from config
-        /// Alias of the section to remove
-        void DeleteSectionFromAllUserGroups(string sectionAlias);
+    /// 
+    ///     Removes a specific section from all user groups
+    /// 
+    /// This is useful when an entire section is removed from config
+    /// Alias of the section to remove
+    void DeleteSectionFromAllUserGroups(string sectionAlias);
 
-        /// 
-        /// Get explicitly assigned permissions for a user and optional node ids
-        /// 
-        /// If no permissions are found for a particular entity then the user's default permissions will be applied
-        /// User to retrieve permissions for
-        /// Specifying nothing will return all user permissions for all nodes that have explicit permissions defined
-        /// An enumerable list of 
-        /// 
-        /// This will return the default permissions for the user's groups for node ids that don't have explicitly defined permissions
-        /// 
-        EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds);
+    /// 
+    ///     Get explicitly assigned permissions for a user and optional node ids
+    /// 
+    /// If no permissions are found for a particular entity then the user's default permissions will be applied
+    /// User to retrieve permissions for
+    /// 
+    ///     Specifying nothing will return all user permissions for all nodes that have explicit permissions
+    ///     defined
+    /// 
+    /// An enumerable list of 
+    /// 
+    ///     This will return the default permissions for the user's groups for node ids that don't have explicitly defined
+    ///     permissions
+    /// 
+    EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds);
 
-        /// 
-        /// Get explicitly assigned permissions for groups and optional node Ids
-        /// 
-        /// 
-        /// 
-        ///     Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set
-        /// 
-        /// Specifying nothing will return all permissions for all nodes
-        /// An enumerable list of 
-        EntityPermissionCollection GetPermissions(IUserGroup?[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds);
+    /// 
+    ///     Get explicitly assigned permissions for groups and optional node Ids
+    /// 
+    /// 
+    /// 
+    ///     Flag indicating if we want to include the default group permissions for each result if there are not explicit
+    ///     permissions set
+    /// 
+    /// Specifying nothing will return all permissions for all nodes
+    /// An enumerable list of 
+    EntityPermissionCollection GetPermissions(IUserGroup?[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds);
 
-        /// 
-        /// Gets the implicit/inherited permissions for the user for the given path
-        /// 
-        /// User to check permissions for
-        /// Path to check permissions for
-        EntityPermissionSet GetPermissionsForPath(IUser? user, string? path);
+    /// 
+    ///     Gets the implicit/inherited permissions for the user for the given path
+    /// 
+    /// User to check permissions for
+    /// Path to check permissions for
+    EntityPermissionSet GetPermissionsForPath(IUser? user, string? path);
 
-        /// 
-        /// Gets the permissions for the provided groups and path
-        /// 
-        /// 
-        /// Path to check permissions for
-        /// 
-        ///     Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set
-        /// 
-        EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false);
+    /// 
+    ///     Gets the permissions for the provided groups and path
+    /// 
+    /// 
+    /// Path to check permissions for
+    /// 
+    ///     Flag indicating if we want to include the default group permissions for each result if there are not explicit
+    ///     permissions set
+    /// 
+    EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false);
 
-        /// 
-        /// Replaces the same permission set for a single group to any number of entities
-        /// 
-        /// Id of the group
-        /// 
-        /// Permissions as enumerable list of ,
-        /// if no permissions are specified then all permissions for this node are removed for this group
-        /// 
-        /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed.
-        /// If no 'entityIds' are specified all permissions will be removed for the specified group.
-        void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds);
+    /// 
+    ///     Replaces the same permission set for a single group to any number of entities
+    /// 
+    /// Id of the group
+    /// 
+    ///     Permissions as enumerable list of ,
+    ///     if no permissions are specified then all permissions for this node are removed for this group
+    /// 
+    /// 
+    ///     Specify the nodes to replace permissions for. If nothing is specified all permissions are
+    ///     removed.
+    /// 
+    /// If no 'entityIds' are specified all permissions will be removed for the specified group.
+    void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds);
 
-        /// 
-        /// Assigns the same permission set for a single user group to any number of entities
-        /// 
-        /// Id of the group
-        /// 
-        /// Specify the nodes to replace permissions for
-        void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds);
+    /// 
+    ///     Assigns the same permission set for a single user group to any number of entities
+    /// 
+    /// Id of the group
+    /// 
+    /// Specify the nodes to replace permissions for
+    void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds);
 
-        /// 
-        /// Gets a list of  objects associated with a given group
-        /// 
-        /// Id of group
-        /// 
-        IEnumerable GetAllInGroup(int? groupId);
+    /// 
+    ///     Gets a list of  objects associated with a given group
+    /// 
+    /// Id of group
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAllInGroup(int? groupId);
 
-        /// 
-        /// Gets a list of  objects not associated with a given group
-        /// 
-        /// Id of group
-        /// 
-        IEnumerable GetAllNotInGroup(int groupId);
+    /// 
+    ///     Gets a list of  objects not associated with a given group
+    /// 
+    /// Id of group
+    /// 
+    ///     
+    /// 
+    IEnumerable GetAllNotInGroup(int groupId);
 
-        IEnumerable GetNextUsers(int id, int count);
+    IEnumerable GetNextUsers(int id, int count);
 
-        #region User groups
+    #region User groups
 
-        /// 
-        /// Gets all UserGroups or those specified as parameters
-        /// 
-        /// Optional Ids of UserGroups to retrieve
-        /// An enumerable list of 
-        IEnumerable GetAllUserGroups(params int[] ids);
+    /// 
+    ///     Gets all UserGroups or those specified as parameters
+    /// 
+    /// Optional Ids of UserGroups to retrieve
+    /// An enumerable list of 
+    IEnumerable GetAllUserGroups(params int[] ids);
 
-        /// 
-        /// Gets a UserGroup by its Alias
-        /// 
-        /// Alias of the UserGroup to retrieve
-        /// 
-        IEnumerable GetUserGroupsByAlias(params string[] alias);
+    /// 
+    ///     Gets a UserGroup by its Alias
+    /// 
+    /// Alias of the UserGroup to retrieve
+    /// 
+    ///     
+    /// 
+    IEnumerable GetUserGroupsByAlias(params string[] alias);
 
-        /// 
-        /// Gets a UserGroup by its Alias
-        /// 
-        /// Name of the UserGroup to retrieve
-        /// 
-        IUserGroup? GetUserGroupByAlias(string name);
+    /// 
+    ///     Gets a UserGroup by its Alias
+    /// 
+    /// Name of the UserGroup to retrieve
+    /// 
+    ///     
+    /// 
+    IUserGroup? GetUserGroupByAlias(string name);
 
-        /// 
-        /// Gets a UserGroup by its Id
-        /// 
-        /// Id of the UserGroup to retrieve
-        /// 
-        IUserGroup? GetUserGroupById(int id);
+    /// 
+    ///     Gets a UserGroup by its Id
+    /// 
+    /// Id of the UserGroup to retrieve
+    /// 
+    ///     
+    /// 
+    IUserGroup? GetUserGroupById(int id);
 
-        /// 
-        /// Saves a UserGroup
-        /// 
-        /// UserGroup to save
-        /// 
-        /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in
-        /// than all users will be removed from this group and only these users will be added
-        /// 
-        void Save(IUserGroup userGroup, int[]? userIds = null);
+    /// 
+    ///     Saves a UserGroup
+    /// 
+    /// UserGroup to save
+    /// 
+    ///     If null than no changes are made to the users who are assigned to this group, however if a value is passed in
+    ///     than all users will be removed from this group and only these users will be added
+    /// 
+    void Save(IUserGroup userGroup, int[]? userIds = null);
 
-        /// 
-        /// Deletes a UserGroup
-        /// 
-        /// UserGroup to delete
-        void DeleteUserGroup(IUserGroup userGroup);
+    /// 
+    ///     Deletes a UserGroup
+    /// 
+    /// UserGroup to delete
+    void DeleteUserGroup(IUserGroup userGroup);
 
-        #endregion
-    }
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/IdKeyMap.cs b/src/Umbraco.Core/Services/IdKeyMap.cs
index 00acb7ad04..7aa746ae27 100644
--- a/src/Umbraco.Core/Services/IdKeyMap.cs
+++ b/src/Umbraco.Core/Services/IdKeyMap.cs
@@ -1,79 +1,56 @@
-using System;
 using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Threading;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class IdKeyMap : IIdKeyMap, IDisposable
 {
-    public class IdKeyMap : IIdKeyMap,IDisposable
+    private readonly ICoreScopeProvider _scopeProvider;
+    private readonly IIdKeyMapRepository _idKeyMapRepository;
+    private readonly ReaderWriterLockSlim _locker = new();
+
+    private readonly Dictionary> _id2Key = new();
+    private readonly Dictionary> _key2Id = new();
+
+    // note - for pure read-only we might want to *not* enforce a transaction?
+
+    // notes
+    //
+    // - this class assumes that the id/guid map is unique; that is, if an id and a guid map
+    //   to each other, then the id will never map to another guid, and the guid will never map
+    //   to another id
+    //
+    // - cache is cleared by MediaCacheRefresher, UnpublishedPageCacheRefresher, and other
+    //   refreshers - because id/guid map is unique, we only clear to avoid leaking memory, 'cos
+    //   we don't risk caching obsolete values - and only when actually deleting
+    //
+    // - we do NOT prefetch anything from database
+    //
+    // - NuCache maintains its own id/guid map for content & media items
+    //   it does *not* populate the idk map, because it directly uses its own map
+    //   still, it provides mappers so that the idk map can benefit from them
+    //   which means there will be some double-caching at some point ??
+    //
+    // - when a request comes in:
+    //   if the idkMap already knows about the map, it returns the value
+    //   else it tries the published cache via mappers
+    //   else it hits the database
+    private readonly ConcurrentDictionary id2key, Func key2id)>
+        _dictionary
+            = new();
+
+    public IdKeyMap(ICoreScopeProvider scopeProvider, IIdKeyMapRepository idKeyMapRepository)
     {
-        private readonly ICoreScopeProvider _scopeProvider;
-        private readonly IIdKeyMapRepository _idKeyMapRepository;
-        private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
+        _scopeProvider = scopeProvider;
+        _idKeyMapRepository = idKeyMapRepository;
+    }
 
-        private readonly Dictionary> _id2Key = new Dictionary>();
-        private readonly Dictionary> _key2Id = new Dictionary>();
+    private bool _disposedValue;
 
-        public IdKeyMap(ICoreScopeProvider scopeProvider, IIdKeyMapRepository idKeyMapRepository)
-        {
-            _scopeProvider = scopeProvider;
-            _idKeyMapRepository = idKeyMapRepository;
-        }
-
-        // note - for pure read-only we might want to *not* enforce a transaction?
-
-        // notes
-        //
-        // - this class assumes that the id/guid map is unique; that is, if an id and a guid map
-        //   to each other, then the id will never map to another guid, and the guid will never map
-        //   to another id
-        //
-        // - cache is cleared by MediaCacheRefresher, UnpublishedPageCacheRefresher, and other
-        //   refreshers - because id/guid map is unique, we only clear to avoid leaking memory, 'cos
-        //   we don't risk caching obsolete values - and only when actually deleting
-        //
-        // - we do NOT prefetch anything from database
-        //
-        // - NuCache maintains its own id/guid map for content & media items
-        //   it does *not* populate the idk map, because it directly uses its own map
-        //   still, it provides mappers so that the idk map can benefit from them
-        //   which means there will be some double-caching at some point ??
-        //
-        // - when a request comes in:
-        //   if the idkMap already knows about the map, it returns the value
-        //   else it tries the published cache via mappers
-        //   else it hits the database
-
-        private readonly ConcurrentDictionary id2key, Func key2id)> _dictionary
-            = new ConcurrentDictionary id2key, Func key2id)>();
-        private bool _disposedValue;
-
-        public void SetMapper(UmbracoObjectTypes umbracoObjectType, Func id2key, Func key2id)
-        {
-            _dictionary[umbracoObjectType] = (id2key, key2id);
-        }
-
-        internal void Populate(IEnumerable<(int id, Guid key)> pairs, UmbracoObjectTypes umbracoObjectType)
-        {
-            try
-            {
-                _locker.EnterWriteLock();
-                foreach (var pair in pairs)
-                {
-
-                    _id2Key[pair.id] = new TypedId(pair.key, umbracoObjectType);
-                    _key2Id[pair.key] = new TypedId(pair.id, umbracoObjectType);
-                }
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
-            }
-        }
+    public void SetMapper(UmbracoObjectTypes umbracoObjectType, Func id2key, Func key2id) =>
+        _dictionary[umbracoObjectType] = (id2key, key2id);
 
 #if POPULATE_FROM_DATABASE
         private void PopulateLocked()
@@ -85,7 +62,8 @@ namespace Umbraco.Cms.Core.Services
             {
                 // populate content and media items
                 var types = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media };
-                var values = scope.Database.Query("SELECT id, uniqueId, nodeObjectType FROM umbracoNode WHERE nodeObjectType IN @types", new { types });
+                var values =
+ scope.Database.Query("SELECT id, uniqueId, nodeObjectType FROM umbracoNode WHERE nodeObjectType IN @types", new { types });
                 foreach (var value in values)
                 {
                     var umbracoObjectType = ObjectTypes.GetUmbracoObjectType(value.NodeObjectType);
@@ -135,21 +113,27 @@ namespace Umbraco.Cms.Core.Services
         }
 #endif
 
-        public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType)
-        {
-            bool empty;
+    public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType)
+    {
+        bool empty;
 
-            try
+        try
+        {
+            _locker.EnterReadLock();
+            if (_key2Id.TryGetValue(key, out TypedId id) && id.UmbracoObjectType == umbracoObjectType)
             {
-                _locker.EnterReadLock();
-                if (_key2Id.TryGetValue(key, out var id) && id.UmbracoObjectType == umbracoObjectType) return Attempt.Succeed(id.Id);
-                empty = _key2Id.Count == 0;
+                return Attempt.Succeed(id.Id);
             }
-            finally
+
+            empty = _key2Id.Count == 0;
+        }
+        finally
+        {
+            if (_locker.IsReadLockHeld)
             {
-                if (_locker.IsReadLockHeld)
-                    _locker.ExitReadLock();
+                _locker.ExitReadLock();
             }
+        }
 
 #if POPULATE_FROM_DATABASE
             // if cache is empty and looking for a document or a media,
@@ -158,77 +142,115 @@ namespace Umbraco.Cms.Core.Services
                 return PopulateAndGetIdForKey(key, umbracoObjectType);
 #endif
 
-            // optimize for read speed: reading database outside a lock means that we could read
-            // multiple times, but we don't lock the cache while accessing the database = better
+        // optimize for read speed: reading database outside a lock means that we could read
+        // multiple times, but we don't lock the cache while accessing the database = better
+        int? val = null;
 
-            int? val = null;
-
-            if (_dictionary.TryGetValue(umbracoObjectType, out var mappers))
-                if ((val = mappers.key2id(key)) == default(int)) val = null;
-
-            if (val == null)
+        if (_dictionary.TryGetValue(umbracoObjectType, out (Func id2key, Func key2id) mappers))
+        {
+            if ((val = mappers.key2id(key)) == default(int))
             {
-                using (var scope = _scopeProvider.CreateCoreScope())
-                {
-                    val = _idKeyMapRepository.GetIdForKey(key, umbracoObjectType);
-                    scope.Complete();
-                }
+                val = null;
             }
-
-            if (val == null) return Attempt.Fail();
-
-            // cache reservations, when something is saved this cache is cleared anyways
-            //if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
-            //    Attempt.Succeed(val.Value);
-
-            try
-            {
-                _locker.EnterWriteLock();
-                _id2Key[val.Value] = new TypedId(key, umbracoObjectType);
-                _key2Id[key] = new TypedId(val.Value, umbracoObjectType);
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
-            }
-
-            return Attempt.Succeed(val.Value);
         }
 
-        public Attempt GetIdForUdi(Udi udi)
+        if (val == null)
         {
-            var guidUdi = udi as GuidUdi;
-            if (guidUdi == null)
-                return Attempt.Fail();
-
-            var umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(guidUdi.EntityType);
-            return GetIdForKey(guidUdi.Guid, umbracoType);
+            using (ICoreScope scope = _scopeProvider.CreateCoreScope())
+            {
+                val = _idKeyMapRepository.GetIdForKey(key, umbracoObjectType);
+                scope.Complete();
+            }
         }
 
-        public Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType)
+        if (val == null)
         {
-            var keyAttempt = GetKeyForId(id, umbracoObjectType);
-            return keyAttempt.Success
-                ? Attempt.Succeed(new GuidUdi(UdiEntityTypeHelper.FromUmbracoObjectType(umbracoObjectType), keyAttempt.Result))
-                : Attempt.Fail();
+            return Attempt.Fail();
         }
 
-        public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType)
+        // cache reservations, when something is saved this cache is cleared anyways
+        // if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
+        //    Attempt.Succeed(val.Value);
+        try
         {
-            bool empty;
+            _locker.EnterWriteLock();
+            _id2Key[val.Value] = new TypedId(key, umbracoObjectType);
+            _key2Id[key] = new TypedId(val.Value, umbracoObjectType);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
+            {
+                _locker.ExitWriteLock();
+            }
+        }
 
-            try
+        return Attempt.Succeed(val.Value);
+    }
+
+    internal void Populate(IEnumerable<(int id, Guid key)> pairs, UmbracoObjectTypes umbracoObjectType)
+    {
+        try
+        {
+            _locker.EnterWriteLock();
+            foreach ((int id, Guid key) in pairs)
             {
-                _locker.EnterReadLock();
-                if (_id2Key.TryGetValue(id, out var key) && key.UmbracoObjectType == umbracoObjectType) return Attempt.Succeed(key.Id);
-                empty = _id2Key.Count == 0;
+                _id2Key[id] = new TypedId(key, umbracoObjectType);
+                _key2Id[key] = new TypedId(id, umbracoObjectType);
             }
-            finally
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                if (_locker.IsReadLockHeld)
-                    _locker.ExitReadLock();
+                _locker.ExitWriteLock();
             }
+        }
+    }
+
+    public Attempt GetIdForUdi(Udi udi)
+    {
+        var guidUdi = udi as GuidUdi;
+        if (guidUdi == null)
+        {
+            return Attempt.Fail();
+        }
+
+        UmbracoObjectTypes umbracoType = UdiEntityTypeHelper.ToUmbracoObjectType(guidUdi.EntityType);
+        return GetIdForKey(guidUdi.Guid, umbracoType);
+    }
+
+    public Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType)
+    {
+        Attempt keyAttempt = GetKeyForId(id, umbracoObjectType);
+        return keyAttempt.Success
+            ? Attempt.Succeed(new GuidUdi(
+                UdiEntityTypeHelper.FromUmbracoObjectType(umbracoObjectType),
+                keyAttempt.Result))
+            : Attempt.Fail();
+    }
+
+    public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType)
+    {
+        bool empty;
+
+        try
+        {
+            _locker.EnterReadLock();
+            if (_id2Key.TryGetValue(id, out TypedId key) && key.UmbracoObjectType == umbracoObjectType)
+            {
+                return Attempt.Succeed(key.Id);
+            }
+
+            empty = _id2Key.Count == 0;
+        }
+        finally
+        {
+            if (_locker.IsReadLockHeld)
+            {
+                _locker.ExitReadLock();
+            }
+        }
 
 #if POPULATE_FROM_DATABASE
             // if cache is empty and looking for a document or a media,
@@ -237,133 +259,156 @@ namespace Umbraco.Cms.Core.Services
                 return PopulateAndGetKeyForId(id, umbracoObjectType);
 #endif
 
-            // optimize for read speed: reading database outside a lock means that we could read
-            // multiple times, but we don't lock the cache while accessing the database = better
+        // optimize for read speed: reading database outside a lock means that we could read
+        // multiple times, but we don't lock the cache while accessing the database = better
+        Guid? val = null;
 
-            Guid? val = null;
-
-            if (_dictionary.TryGetValue(umbracoObjectType, out var mappers))
-                if ((val = mappers.id2key(id)) == default(Guid)) val = null;
-
-            if (val == null)
-            {
-                using (var scope = _scopeProvider.CreateCoreScope())
-                {
-                    val = _idKeyMapRepository.GetIdForKey(id, umbracoObjectType);
-                    scope.Complete();
-                }
-            }
-
-            if (val == null) return Attempt.Fail();
-
-            // cache reservations, when something is saved this cache is cleared anyways
-            //if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
-            //    Attempt.Succeed(val.Value);
-
-            try
-            {
-                _locker.EnterWriteLock();
-                _id2Key[id] = new TypedId(val.Value, umbracoObjectType);
-                _key2Id[val.Value] = new TypedId(id, umbracoObjectType);
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
-            }
-
-            return Attempt.Succeed(val.Value);
-        }
-
-        // invoked on UnpublishedPageCacheRefresher.RefreshAll
-        // anything else will use the id-specific overloads
-        public void ClearCache()
+        if (_dictionary.TryGetValue(umbracoObjectType, out (Func id2key, Func key2id) mappers))
         {
-            try
+            if ((val = mappers.id2key(id)) == default(Guid))
             {
-                _locker.EnterWriteLock();
-                _id2Key.Clear();
-                _key2Id.Clear();
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                val = null;
             }
         }
 
-        public void ClearCache(int id)
+        if (val == null)
         {
-            try
+            using (ICoreScope scope = _scopeProvider.CreateCoreScope())
             {
-                _locker.EnterWriteLock();
-                if (_id2Key.TryGetValue(id, out var key) == false) return;
-                _id2Key.Remove(id);
-                _key2Id.Remove(key.Id);
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                val = _idKeyMapRepository.GetIdForKey(id, umbracoObjectType);
+                scope.Complete();
             }
         }
 
-        public void ClearCache(Guid key)
+        if (val == null)
         {
-            try
+            return Attempt.Fail();
+        }
+
+        // cache reservations, when something is saved this cache is cleared anyways
+        // if (umbracoObjectType == UmbracoObjectTypes.IdReservation)
+        //    Attempt.Succeed(val.Value);
+        try
+        {
+            _locker.EnterWriteLock();
+            _id2Key[id] = new TypedId(val.Value, umbracoObjectType);
+            _key2Id[val.Value] = new TypedId(id, umbracoObjectType);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                _locker.EnterWriteLock();
-                if (_key2Id.TryGetValue(key, out var id) == false) return;
-                _id2Key.Remove(id.Id);
-                _key2Id.Remove(key);
-            }
-            finally
-            {
-                if (_locker.IsWriteLockHeld)
-                    _locker.ExitWriteLock();
+                _locker.ExitWriteLock();
             }
         }
 
-        // ReSharper disable ClassNeverInstantiated.Local
-        // ReSharper disable UnusedAutoPropertyAccessor.Local
-        private class TypedIdDto
-        {
-            public int Id { get; set; }
-            public Guid UniqueId { get; set; }
-            public Guid NodeObjectType { get; set; }
-        }
-        // ReSharper restore ClassNeverInstantiated.Local
-        // ReSharper restore UnusedAutoPropertyAccessor.Local
+        return Attempt.Succeed(val.Value);
+    }
 
-        private struct TypedId
+    // invoked on UnpublishedPageCacheRefresher.RefreshAll
+    // anything else will use the id-specific overloads
+    public void ClearCache()
+    {
+        try
         {
-            public TypedId(T id, UmbracoObjectTypes umbracoObjectType)
+            _locker.EnterWriteLock();
+            _id2Key.Clear();
+            _key2Id.Clear();
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
             {
-                UmbracoObjectType = umbracoObjectType;
-                Id = id;
+                _locker.ExitWriteLock();
             }
-
-            public UmbracoObjectTypes UmbracoObjectType { get; }
-
-            public T Id { get; }
-        }
-
-        protected virtual void Dispose(bool disposing)
-        {
-            if (!_disposedValue)
-            {
-                if (disposing)
-                {
-                    _locker.Dispose();
-                }
-                _disposedValue = true;
-            }
-        }
-
-        public void Dispose()
-        {
-            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
-            Dispose(disposing: true);
         }
     }
+
+    public void ClearCache(int id)
+    {
+        try
+        {
+            _locker.EnterWriteLock();
+            if (_id2Key.TryGetValue(id, out TypedId key) == false)
+            {
+                return;
+            }
+
+            _id2Key.Remove(id);
+            _key2Id.Remove(key.Id);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
+            {
+                _locker.ExitWriteLock();
+            }
+        }
+    }
+
+    public void ClearCache(Guid key)
+    {
+        try
+        {
+            _locker.EnterWriteLock();
+            if (_key2Id.TryGetValue(key, out TypedId id) == false)
+            {
+                return;
+            }
+
+            _id2Key.Remove(id.Id);
+            _key2Id.Remove(key);
+        }
+        finally
+        {
+            if (_locker.IsWriteLockHeld)
+            {
+                _locker.ExitWriteLock();
+            }
+        }
+    }
+
+    protected virtual void Dispose(bool disposing)
+    {
+        if (!_disposedValue)
+        {
+            if (disposing)
+            {
+                _locker.Dispose();
+            }
+
+            _disposedValue = true;
+        }
+    }
+
+    // ReSharper restore ClassNeverInstantiated.Local
+    // ReSharper restore UnusedAutoPropertyAccessor.Local
+    private struct TypedId
+    {
+        public TypedId(T id, UmbracoObjectTypes umbracoObjectType)
+        {
+            UmbracoObjectType = umbracoObjectType;
+            Id = id;
+        }
+
+        public UmbracoObjectTypes UmbracoObjectType { get; }
+
+        public T Id { get; }
+    }
+
+    // ReSharper disable ClassNeverInstantiated.Local
+    // ReSharper disable UnusedAutoPropertyAccessor.Local
+    private class TypedIdDto
+    {
+        public int Id { get; set; }
+
+        public Guid UniqueId { get; set; }
+
+        public Guid NodeObjectType { get; set; }
+    }
+
+    public void Dispose() =>
+
+        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+        Dispose(true);
 }
diff --git a/src/Umbraco.Core/Services/InstallationService.cs b/src/Umbraco.Core/Services/InstallationService.cs
index eb1632be8a..00bd00aa91 100644
--- a/src/Umbraco.Core/Services/InstallationService.cs
+++ b/src/Umbraco.Core/Services/InstallationService.cs
@@ -1,20 +1,14 @@
-using System.Threading.Tasks;
 using Umbraco.Cms.Core.Persistence.Repositories;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class InstallationService : IInstallationService
 {
-    public class InstallationService : IInstallationService
-    {
-        private readonly IInstallationRepository _installationRepository;
+    private readonly IInstallationRepository _installationRepository;
 
-        public InstallationService(IInstallationRepository installationRepository)
-        {
-            _installationRepository = installationRepository;
-        }
+    public InstallationService(IInstallationRepository installationRepository) =>
+        _installationRepository = installationRepository;
 
-        public async Task LogInstall(InstallLog installLog)
-        {
-            await _installationRepository.SaveInstallLogAsync(installLog);
-        }
-    }
+    public async Task LogInstall(InstallLog installLog) =>
+        await _installationRepository.SaveInstallLogAsync(installLog);
 }
diff --git a/src/Umbraco.Core/Services/KeyValueService.cs b/src/Umbraco.Core/Services/KeyValueService.cs
index 834c0d3116..0a38e3c284 100644
--- a/src/Umbraco.Core/Services/KeyValueService.cs
+++ b/src/Umbraco.Core/Services/KeyValueService.cs
@@ -1,97 +1,91 @@
-using System;
-using System.Collections.Generic;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+internal class KeyValueService : IKeyValueService
 {
-    internal class KeyValueService : IKeyValueService
+    private readonly IKeyValueRepository _repository;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public KeyValueService(ICoreScopeProvider scopeProvider, IKeyValueRepository repository)
     {
-        private readonly ICoreScopeProvider _scopeProvider;
-        private readonly IKeyValueRepository _repository;
+        _scopeProvider = scopeProvider;
+        _repository = repository;
+    }
 
-        public KeyValueService(ICoreScopeProvider scopeProvider, IKeyValueRepository repository)
+    /// 
+    public string? GetValue(string key)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
         {
-            _scopeProvider = scopeProvider;
-            _repository = repository;
-        }
-
-        /// 
-        public string? GetValue(string key)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _repository.Get(key)?.Value;
-            }
-        }
-
-        /// 
-        public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _repository.FindByKeyPrefix(keyPrefix);
-            }
-        }
-
-        /// 
-        public void SetValue(string key, string value)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Cms.Core.Constants.Locks.KeyValues);
-
-                var keyValue = _repository.Get(key);
-                if (keyValue == null)
-                {
-                    keyValue = new KeyValue
-                    {
-                        Identifier = key,
-                        Value = value,
-                        UpdateDate = DateTime.Now,
-                    };
-                }
-                else
-                {
-                    keyValue.Value = value;
-                    keyValue.UpdateDate = DateTime.Now;
-                }
-
-                _repository.Save(keyValue);
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        public void SetValue(string key, string originValue, string newValue)
-        {
-            if (!TrySetValue(key, originValue, newValue))
-                throw new InvalidOperationException("Could not set the value.");
-        }
-
-        /// 
-        public bool TrySetValue(string key, string originalValue, string newValue)
-        {
-            using (var scope = _scopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Cms.Core.Constants.Locks.KeyValues);
-
-                var keyValue = _repository.Get(key);
-                if (keyValue == null || keyValue.Value != originalValue)
-                {
-                    return false;
-                }
-
-                keyValue.Value = newValue;
-                keyValue.UpdateDate = DateTime.Now;
-                _repository.Save(keyValue);
-
-                scope.Complete();
-            }
-
-            return true;
+            return _repository.Get(key)?.Value;
         }
     }
+
+    /// 
+    public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _repository.FindByKeyPrefix(keyPrefix);
+        }
+    }
+
+    /// 
+    public void SetValue(string key, string value)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.KeyValues);
+
+            IKeyValue? keyValue = _repository.Get(key);
+            if (keyValue == null)
+            {
+                keyValue = new KeyValue { Identifier = key, Value = value, UpdateDate = DateTime.Now };
+            }
+            else
+            {
+                keyValue.Value = value;
+                keyValue.UpdateDate = DateTime.Now;
+            }
+
+            _repository.Save(keyValue);
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    public void SetValue(string key, string originValue, string newValue)
+    {
+        if (!TrySetValue(key, originValue, newValue))
+        {
+            throw new InvalidOperationException("Could not set the value.");
+        }
+    }
+
+    /// 
+    public bool TrySetValue(string key, string originalValue, string newValue)
+    {
+        using (ICoreScope scope = _scopeProvider.CreateCoreScope())
+        {
+            scope.WriteLock(Constants.Locks.KeyValues);
+
+            IKeyValue? keyValue = _repository.Get(key);
+            if (keyValue == null || keyValue.Value != originalValue)
+            {
+                return false;
+            }
+
+            keyValue.Value = newValue;
+            keyValue.UpdateDate = DateTime.Now;
+            _repository.Save(keyValue);
+
+            scope.Complete();
+        }
+
+        return true;
+    }
 }
diff --git a/src/Umbraco.Core/Services/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs
index 262697c935..3046ddafb5 100644
--- a/src/Umbraco.Core/Services/LocalizationService.cs
+++ b/src/Umbraco.Core/Services/LocalizationService.cs
@@ -1,178 +1,193 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Persistence.Querying;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
-{
-    /// 
-    /// Represents the Localization Service, which is an easy access to operations involving  and 
-    /// 
-    internal class LocalizationService : RepositoryService, ILocalizationService
-    {
-        private readonly IDictionaryRepository _dictionaryRepository;
-        private readonly ILanguageRepository _languageRepository;
-        private readonly IAuditRepository _auditRepository;
+namespace Umbraco.Cms.Core.Services;
 
-        public LocalizationService(
-            ICoreScopeProvider provider,
-            ILoggerFactory loggerFactory,
-            IEventMessagesFactory eventMessagesFactory,
-            IDictionaryRepository dictionaryRepository,
-            IAuditRepository auditRepository,
-            ILanguageRepository languageRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+/// 
+///     Represents the Localization Service, which is an easy access to operations involving  and
+///     
+/// 
+internal class LocalizationService : RepositoryService, ILocalizationService
+{
+    private readonly IAuditRepository _auditRepository;
+    private readonly IDictionaryRepository _dictionaryRepository;
+    private readonly ILanguageRepository _languageRepository;
+
+    public LocalizationService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IDictionaryRepository dictionaryRepository,
+        IAuditRepository auditRepository,
+        ILanguageRepository languageRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
+    {
+        _dictionaryRepository = dictionaryRepository;
+        _auditRepository = auditRepository;
+        _languageRepository = languageRepository;
+    }
+
+    /// 
+    ///     Adds or updates a translation for a dictionary item and language
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This does not save the item, that needs to be done explicitly
+    /// 
+    public void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value)
+    {
+        if (item == null)
         {
-            _dictionaryRepository = dictionaryRepository;
-            _auditRepository = auditRepository;
-            _languageRepository = languageRepository;
+            throw new ArgumentNullException(nameof(item));
         }
 
-        /// 
-        /// Adds or updates a translation for a dictionary item and language
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This does not save the item, that needs to be done explicitly
-        /// 
-        public void AddOrUpdateDictionaryValue(IDictionaryItem item, ILanguage? language, string value)
+        if (language == null)
         {
-            if (item == null) throw new ArgumentNullException(nameof(item));
-            if (language == null) throw new ArgumentNullException(nameof(language));
+            throw new ArgumentNullException(nameof(language));
+        }
 
-            var existing = item.Translations?.FirstOrDefault(x => x.Language?.Id == language.Id);
-            if (existing != null)
+        IDictionaryTranslation? existing = item.Translations?.FirstOrDefault(x => x.Language?.Id == language.Id);
+        if (existing != null)
+        {
+            existing.Value = value;
+        }
+        else
+        {
+            if (item.Translations is not null)
             {
-                existing.Value = value;
+                item.Translations = new List(item.Translations)
+                {
+                    new DictionaryTranslation(language, value),
+                };
             }
             else
             {
-                if (item.Translations is not null)
-                {
-                    item.Translations = new List(item.Translations)
-                    {
-                        new DictionaryTranslation(language, value)
-                    };
-                }
-                else
-                {
-                    item.Translations = new List
-                    {
-                        new DictionaryTranslation(language, value)
-                    };
-                }
+                item.Translations = new List { new DictionaryTranslation(language, value) };
             }
         }
+    }
 
-        /// 
-        /// Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified.
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null)
+    /// 
+    ///     Creates and saves a new dictionary item and assigns a value to all languages if defaultValue is specified.
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public IDictionaryItem CreateDictionaryItemWithIdentity(string key, Guid? parentId, string? defaultValue = null)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            // validate the parent
+            if (parentId.HasValue && parentId.Value != Guid.Empty)
             {
-                //validate the parent
-
-                if (parentId.HasValue && parentId.Value != Guid.Empty)
+                IDictionaryItem? parent = GetDictionaryItemById(parentId.Value);
+                if (parent == null)
                 {
-                    var parent = GetDictionaryItemById(parentId.Value);
-                    if (parent == null)
-                        throw new ArgumentException($"No parent dictionary item was found with id {parentId.Value}.");
+                    throw new ArgumentException($"No parent dictionary item was found with id {parentId.Value}.");
                 }
+            }
 
-                var item = new DictionaryItem(parentId, key);
+            var item = new DictionaryItem(parentId, key);
 
-                if (defaultValue.IsNullOrWhiteSpace() == false)
-                {
-                    var langs = GetAllLanguages();
-                    var translations = langs.Select(language => new DictionaryTranslation(language, defaultValue!))
-                        .Cast()
-                        .ToList();
+            if (defaultValue.IsNullOrWhiteSpace() == false)
+            {
+                IEnumerable langs = GetAllLanguages();
+                var translations = langs.Select(language => new DictionaryTranslation(language, defaultValue!))
+                    .Cast()
+                    .ToList();
 
-                    item.Translations = translations;
-                }
+                item.Translations = translations;
+            }
 
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new DictionaryItemSavingNotification(item, eventMessages);
-
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return item;
-                }
-                _dictionaryRepository.Save(item);
-
-                // ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
-
-                scope.Notifications.Publish(new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification));
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new DictionaryItemSavingNotification(item, eventMessages);
 
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
                 scope.Complete();
-
                 return item;
             }
-        }
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        public IDictionaryItem? GetDictionaryItemById(int id)
+            _dictionaryRepository.Save(item);
+
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+
+            scope.Notifications.Publish(
+                new DictionaryItemSavedNotification(item, eventMessages).WithStateFrom(savingNotification));
+
+            scope.Complete();
+
+            return item;
+        }
+    }
+
+    /// 
+    ///     Gets a  by its  id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    public IDictionaryItem? GetDictionaryItemById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var item = _dictionaryRepository.Get(id);
-                //ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
-                return item;
-            }
-        }
+            IDictionaryItem? item = _dictionaryRepository.Get(id);
 
-        /// 
-        /// Gets a  by its  id
-        /// 
-        /// Id of the 
-        /// 
-        public IDictionaryItem? GetDictionaryItemById(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var item = _dictionaryRepository.Get(id);
-                //ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
-                return item;
-            }
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+            return item;
         }
+    }
 
-        /// 
-        /// Gets a  by its key
-        /// 
-        /// Key of the 
-        /// 
-        public IDictionaryItem? GetDictionaryItemByKey(string key)
+    /// 
+    ///     Gets a  by its  id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    public IDictionaryItem? GetDictionaryItemById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var item = _dictionaryRepository.Get(key);
-                //ensure the lazy Language callback is assigned
-                EnsureDictionaryItemLanguageCallback(item);
-                return item;
-            }
+            IDictionaryItem? item = _dictionaryRepository.Get(id);
+
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+            return item;
         }
+    }
+
+    /// 
+    ///     Gets a  by its key
+    /// 
+    /// Key of the 
+    /// 
+    ///     
+    /// 
+    public IDictionaryItem? GetDictionaryItemByKey(string key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            IDictionaryItem? item = _dictionaryRepository.Get(key);
+
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(item);
+            return item;
+        }
+    }
 
         /// 
         /// Gets a list of children for a 
@@ -189,26 +204,30 @@ namespace Umbraco.Cms.Core.Services
                 foreach (var item in items)
                     EnsureDictionaryItemLanguageCallback(item);
 
-                return items;
-            }
+            return items;
         }
+    }
 
-        /// 
-        /// Gets a list of descendants for a 
-        /// 
-        /// Id of the parent, null will return all dictionary items
-        /// An enumerable list of  objects
-        public IEnumerable GetDictionaryItemDescendants(Guid? parentId)
+    /// 
+    ///     Gets a list of descendants for a 
+    /// 
+    /// Id of the parent, null will return all dictionary items
+    /// An enumerable list of  objects
+    public IEnumerable GetDictionaryItemDescendants(Guid? parentId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            IDictionaryItem[] items = _dictionaryRepository.GetDictionaryItemDescendants(parentId).ToArray();
+
+            // ensure the lazy Language callback is assigned
+            foreach (IDictionaryItem item in items)
             {
-                var items = _dictionaryRepository.GetDictionaryItemDescendants(parentId).ToArray();
-                //ensure the lazy Language callback is assigned
-                foreach (var item in items)
-                    EnsureDictionaryItemLanguageCallback(item);
-                return items;
+                EnsureDictionaryItemLanguageCallback(item);
             }
+
+            return items;
         }
+    }
 
         /// 
         /// Gets the root/top  objects
@@ -227,269 +246,301 @@ namespace Umbraco.Cms.Core.Services
             }
         }
 
-        /// 
-        /// Checks if a  with given key exists
-        /// 
-        /// Key of the 
-        /// True if a  exists, otherwise false
-        public bool DictionaryItemExists(string key)
+    /// 
+    ///     Checks if a  with given key exists
+    /// 
+    /// Key of the 
+    /// True if a  exists, otherwise false
+    public bool DictionaryItemExists(string key)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var item = _dictionaryRepository.Get(key);
-                return item != null;
-            }
+            IDictionaryItem? item = _dictionaryRepository.Get(key);
+            return item != null;
         }
+    }
 
-        /// 
-        /// Saves a  object
-        /// 
-        ///  to save
-        /// Optional id of the user saving the dictionary item
-        public void Save(IDictionaryItem dictionaryItem, int userId = Cms.Core.Constants.Security.SuperUserId)
+    /// 
+    ///     Saves a  object
+    /// 
+    ///  to save
+    /// Optional id of the user saving the dictionary item
+    public void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _dictionaryRepository.Save(dictionaryItem);
-
-                // ensure the lazy Language callback is assigned
-                // ensure the lazy Language callback is assigned
-
-                EnsureDictionaryItemLanguageCallback(dictionaryItem);
-                scope.Notifications.Publish(new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
                 scope.Complete();
+                return;
             }
+
+            _dictionaryRepository.Save(dictionaryItem);
+
+            // ensure the lazy Language callback is assigned
+            // ensure the lazy Language callback is assigned
+            EnsureDictionaryItemLanguageCallback(dictionaryItem);
+            scope.Notifications.Publish(
+                new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
+            scope.Complete();
         }
+    }
 
-        /// 
-        /// Deletes a  object and its related translations
-        /// as well as its children.
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the dictionary item
-        public void Delete(IDictionaryItem dictionaryItem, int userId = Cms.Core.Constants.Security.SuperUserId)
+    /// 
+    ///     Deletes a  object and its related translations
+    ///     as well as its children.
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the dictionary item
+    public void Delete(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _dictionaryRepository.Delete(dictionaryItem);
-                scope.Notifications.Publish(new DictionaryItemDeletedNotification(dictionaryItem, eventMessages).WithStateFrom(deletingNotification));
-
-                Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
-
                 scope.Complete();
+                return;
             }
+
+            _dictionaryRepository.Delete(dictionaryItem);
+            scope.Notifications.Publish(
+                new DictionaryItemDeletedNotification(dictionaryItem, eventMessages)
+                    .WithStateFrom(deletingNotification));
+
+            Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Gets a  by its id
+    /// 
+    /// Id of the 
+    /// 
+    ///     
+    /// 
+    public ILanguage? GetLanguageById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _languageRepository.Get(id);
+        }
+    }
+
+    /// 
+    ///     Gets a  by its iso code
+    /// 
+    /// Iso Code of the language (ie. en-US)
+    /// 
+    ///     
+    /// 
+    public ILanguage? GetLanguageByIsoCode(string? isoCode)
+    {
+        if (isoCode is null)
+        {
+            return null;
         }
 
-        /// 
-        /// Gets a  by its id
-        /// 
-        /// Id of the 
-        /// 
-        public ILanguage? GetLanguageById(int id)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.Get(id);
-            }
+            return _languageRepository.GetByIsoCode(isoCode);
         }
+    }
 
-        /// 
-        /// Gets a  by its iso code
-        /// 
-        /// Iso Code of the language (ie. en-US)
-        /// 
-        public ILanguage? GetLanguageByIsoCode(string? isoCode)
+    /// 
+    public int? GetLanguageIdByIsoCode(string isoCode)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (isoCode is null)
-            {
-                return null;
-            }
-            
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetByIsoCode(isoCode);
-            }
+            return _languageRepository.GetIdByIsoCode(isoCode);
         }
+    }
 
-        /// 
-        public int? GetLanguageIdByIsoCode(string isoCode)
+    /// 
+    public string? GetLanguageIsoCodeById(int id)
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetIdByIsoCode(isoCode);
-            }
+            return _languageRepository.GetIsoCodeById(id);
         }
+    }
 
-        /// 
-        public string? GetLanguageIsoCodeById(int id)
+    /// 
+    public string GetDefaultLanguageIsoCode()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetIsoCodeById(id);
-            }
+            return _languageRepository.GetDefaultIsoCode();
         }
+    }
 
-        /// 
-        public string GetDefaultLanguageIsoCode()
+    /// 
+    public int? GetDefaultLanguageId()
+    {
+        using (ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetDefaultIsoCode();
-            }
+            return _languageRepository.GetDefaultId();
         }
+    }
 
-        /// 
-        public int? GetDefaultLanguageId()
+    /// 
+    ///     Gets all available languages
+    /// 
+    /// An enumerable list of  objects
+    public IEnumerable GetAllLanguages()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetDefaultId();
-            }
+            return _languageRepository.GetMany();
         }
+    }
 
-        /// 
-        /// Gets all available languages
-        /// 
-        /// An enumerable list of  objects
-        public IEnumerable GetAllLanguages()
+    /// 
+    ///     Saves a  object
+    /// 
+    ///  to save
+    /// Optional id of the user saving the language
+    public void Save(ILanguage language, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _languageRepository.GetMany();
-            }
-        }
+            // write-lock languages to guard against race conds when dealing with default language
+            scope.WriteLock(Constants.Locks.Languages);
 
-        /// 
-        /// Saves a  object
-        /// 
-        ///  to save
-        /// Optional id of the user saving the language
-        public void Save(ILanguage language, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            // look for cycles - within write-lock
+            if (language.FallbackLanguageId.HasValue)
             {
-                // write-lock languages to guard against race conds when dealing with default language
-                scope.WriteLock(Cms.Core.Constants.Locks.Languages);
-
-                // look for cycles - within write-lock
-                if (language.FallbackLanguageId.HasValue)
+                var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x);
+                if (!languages.ContainsKey(language.FallbackLanguageId.Value))
                 {
-                    var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x);
-                    if (!languages.ContainsKey(language.FallbackLanguageId.Value))
-                        throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id.");
-                    if (CreatesCycle(language, languages))
-                        throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle.");
+                    throw new InvalidOperationException(
+                        $"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id.");
                 }
 
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new LanguageSavingNotification(language, eventMessages);
-                if (scope.Notifications.PublishCancelable(savingNotification))
+                if (CreatesCycle(language, languages))
                 {
-                    scope.Complete();
-                    return;
+                    throw new InvalidOperationException(
+                        $"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle.");
                 }
+            }
 
-                _languageRepository.Save(language);
-                scope.Notifications.Publish(new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, "Save Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language));
-
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new LanguageSavingNotification(language, eventMessages);
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
                 scope.Complete();
+                return;
             }
+
+            _languageRepository.Save(language);
+            scope.Notifications.Publish(
+                new LanguageSavedNotification(language, eventMessages).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, "Save Language", userId, language.Id, UmbracoObjectTypes.Language.GetName());
+
+            scope.Complete();
         }
+    }
 
-        private bool CreatesCycle(ILanguage language, IDictionary languages)
+    /// 
+    ///     Deletes a  by removing it (but not its usages) from the db
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the language
+    public void Delete(ILanguage language, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            // a new language is not referenced yet, so cannot be part of a cycle
-            if (!language.HasIdentity) return false;
+            // write-lock languages to guard against race conds when dealing with default language
+            scope.WriteLock(Constants.Locks.Languages);
 
-            var id = language.FallbackLanguageId;
-            while (true) // assuming languages does not already contains a cycle, this must end
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingLanguageNotification))
             {
-                if (!id.HasValue) return false; // no fallback means no cycle
-                if (id.Value == language.Id) return true; // back to language = cycle!
-                id = languages[id.Value].FallbackLanguageId; // else keep chaining
-            }
-        }
-
-        /// 
-        /// Deletes a  by removing it (but not its usages) from the db
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the language
-        public void Delete(ILanguage language, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                // write-lock languages to guard against race conds when dealing with default language
-                scope.WriteLock(Cms.Core.Constants.Locks.Languages);
-
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingLanguageNotification = new LanguageDeletingNotification(language, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingLanguageNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted
-                _languageRepository.Delete(language);
-
-                scope.Notifications.Publish(new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification));
-
-                Audit(AuditType.Delete, "Delete Language", userId, language.Id, ObjectTypes.GetName(UmbracoObjectTypes.Language));
                 scope.Complete();
+                return;
             }
+
+            // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted
+            _languageRepository.Delete(language);
+
+            scope.Notifications.Publish(
+                new LanguageDeletedNotification(language, eventMessages).WithStateFrom(deletingLanguageNotification));
+
+            Audit(AuditType.Delete, "Delete Language", userId, language.Id, UmbracoObjectTypes.Language.GetName());
+            scope.Complete();
+        }
+    }
+
+    public Dictionary GetDictionaryItemKeyMap()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _dictionaryRepository.GetDictionaryItemKeyMap();
+        }
+    }
+
+    private bool CreatesCycle(ILanguage language, IDictionary languages)
+    {
+        // a new language is not referenced yet, so cannot be part of a cycle
+        if (!language.HasIdentity)
+        {
+            return false;
         }
 
-        private void Audit(AuditType type, string message, int userId, int objectId, string? entityType)
-        {
-            _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, message));
-        }
+        var id = language.FallbackLanguageId;
 
-        /// 
-        /// This is here to take care of a hack - the DictionaryTranslation model contains an ILanguage reference which we don't want but
-        /// we cannot remove it because it would be a large breaking change, so we need to make sure it's resolved lazily. This is because
-        /// if developers have a lot of dictionary items and translations, the caching and cloning size gets much larger because of
-        /// the large object graphs. So now we don't cache or clone the attached ILanguage
-        /// 
-        private void EnsureDictionaryItemLanguageCallback(IDictionaryItem? d)
+        // assuming languages does not already contains a cycle, this must end
+        while (true)
         {
-            var item = d as DictionaryItem;
-            if (item == null) return;
-
-            item.GetLanguage = GetLanguageById;
-            var translations = item.Translations?.OfType();
-            if (translations is not null)
+            if (!id.HasValue)
             {
-                foreach (var trans in translations)
-                    trans.GetLanguage = GetLanguageById;
+                return false; // no fallback means no cycle
             }
+
+            if (id.Value == language.Id)
+            {
+                return true; // back to language = cycle!
+            }
+
+            id = languages[id.Value].FallbackLanguageId; // else keep chaining
+        }
+    }
+
+    private void Audit(AuditType type, string message, int userId, int objectId, string? entityType) =>
+        _auditRepository.Save(new AuditItem(objectId, type, userId, entityType, message));
+
+    /// 
+    ///     This is here to take care of a hack - the DictionaryTranslation model contains an ILanguage reference which we
+    ///     don't want but
+    ///     we cannot remove it because it would be a large breaking change, so we need to make sure it's resolved lazily. This
+    ///     is because
+    ///     if developers have a lot of dictionary items and translations, the caching and cloning size gets much larger
+    ///     because of
+    ///     the large object graphs. So now we don't cache or clone the attached ILanguage
+    /// 
+    private void EnsureDictionaryItemLanguageCallback(IDictionaryItem? d)
+    {
+        if (d is not DictionaryItem item)
+        {
+            return;
         }
 
-        public Dictionary GetDictionaryItemKeyMap()
+        item.GetLanguage = GetLanguageById;
+        IEnumerable? translations = item.Translations?.OfType();
+        if (translations is not null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            foreach (DictionaryTranslation trans in translations)
             {
-                return _dictionaryRepository.GetDictionaryItemKeyMap();
+                trans.GetLanguage = GetLanguageById;
             }
         }
     }
diff --git a/src/Umbraco.Core/Services/LocalizedTextService.cs b/src/Umbraco.Core/Services/LocalizedTextService.cs
index 6aa7e8fb2b..839e52f49e 100644
--- a/src/Umbraco.Core/Services/LocalizedTextService.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextService.cs
@@ -1,469 +1,520 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using System.Xml.Linq;
 using System.Xml.XPath;
 using Microsoft.Extensions.Logging;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+public class LocalizedTextService : ILocalizedTextService
 {
-    /// 
-    public class LocalizedTextService : ILocalizedTextService
+    private readonly Lazy>>>>
+        _dictionarySourceLazy;
+
+    private readonly Lazy? _fileSources;
+    private readonly ILogger _logger;
+
+    private readonly Lazy>>> _noAreaDictionarySourceLazy;
+
+    /// 
+    ///     Initializes with a file sources instance
+    /// 
+    /// 
+    /// 
+    public LocalizedTextService(
+        Lazy fileSources,
+        ILogger logger)
     {
-        private readonly ILogger _logger;
-        private readonly Lazy? _fileSources;
-
-        private IDictionary>>> _dictionarySource =>
-            _dictionarySourceLazy.Value;
-
-        private IDictionary>> _noAreaDictionarySource =>
-            _noAreaDictionarySourceLazy.Value;
-
-        private readonly Lazy>>>>
-            _dictionarySourceLazy;
-
-        private readonly Lazy>>> _noAreaDictionarySourceLazy;
-
-        /// 
-        /// Initializes with a file sources instance
-        /// 
-        /// 
-        /// 
-        public LocalizedTextService(Lazy fileSources,
-            ILogger logger)
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        if (fileSources == null)
         {
-            if (logger == null) throw new ArgumentNullException(nameof(logger));
-            _logger = logger;
-            if (fileSources == null) throw new ArgumentNullException(nameof(fileSources));
-            _dictionarySourceLazy =
-                new Lazy>>>>(() =>
-                    FileSourcesToAreaDictionarySources(fileSources.Value));
-            _noAreaDictionarySourceLazy =
-                new Lazy>>>(() =>
-                    FileSourcesToNoAreaDictionarySources(fileSources.Value));
-            _fileSources = fileSources;
+            throw new ArgumentNullException(nameof(fileSources));
         }
 
-        private IDictionary>> FileSourcesToNoAreaDictionarySources(
-            LocalizedTextServiceFileSources fileSources)
-        {
-            var xmlSources = fileSources.GetXmlSources();
+        _dictionarySourceLazy =
+            new Lazy>>>>(() =>
+                FileSourcesToAreaDictionarySources(fileSources.Value));
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() =>
+                FileSourcesToNoAreaDictionarySources(fileSources.Value));
+        _fileSources = fileSources;
+    }
 
-            return XmlSourceToNoAreaDictionary(xmlSources);
+    /// 
+    ///     Initializes with an XML source
+    /// 
+    /// 
+    /// 
+    public LocalizedTextService(
+        IDictionary> source,
+        ILogger logger)
+    {
+        if (source == null)
+        {
+            throw new ArgumentNullException(nameof(source));
         }
 
-        private IDictionary>> XmlSourceToNoAreaDictionary(
-            IDictionary> xmlSources)
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+        _dictionarySourceLazy =
+            new Lazy>>>>(() =>
+                XmlSourcesToAreaDictionary(source));
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() =>
+                XmlSourceToNoAreaDictionary(source));
+    }
+
+    [Obsolete(
+        "Use other ctor with IDictionary>>> as input parameter.")]
+    public LocalizedTextService(
+        IDictionary>> source,
+        ILogger logger)
+        : this(
+        source.ToDictionary(x => x.Key, x => new Lazy>>(() => x.Value)),
+        logger)
+    {
+    }
+
+    /// 
+    ///     Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values
+    /// 
+    /// 
+    /// 
+    public LocalizedTextService(
+        IDictionary>>> source,
+        ILogger logger)
+    {
+        IDictionary>>> dictionarySource =
+            source ?? throw new ArgumentNullException(nameof(source));
+        _dictionarySourceLazy =
+            new Lazy>>>>(() =>
+                dictionarySource);
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        var cultureNoAreaDictionary = new Dictionary>>();
+        foreach (KeyValuePair>>> cultureDictionary in
+                 dictionarySource)
         {
-            var cultureNoAreaDictionary = new Dictionary>>();
-            foreach (var xmlSource in xmlSources)
+            Dictionary> areaAliaValue =
+                GetAreaStoredTranslations(source, cultureDictionary.Key);
+
+            cultureNoAreaDictionary.Add(
+                cultureDictionary.Key,
+                new Lazy>(() => GetAliasValues(areaAliaValue)));
+        }
+
+        _noAreaDictionarySourceLazy =
+            new Lazy>>>(() => cultureNoAreaDictionary);
+    }
+
+    private IDictionary>>> DictionarySource =>
+        _dictionarySourceLazy.Value;
+
+    private IDictionary>> NoAreaDictionarySource =>
+        _noAreaDictionarySourceLazy.Value;
+
+    public string Localize(string? area, string? alias, CultureInfo? culture, IDictionary? tokens = null)
+    {
+        if (culture == null)
+        {
+            throw new ArgumentNullException(nameof(culture));
+        }
+
+        // This is what the legacy ui service did
+        if (string.IsNullOrEmpty(alias))
+        {
+            return string.Empty;
+        }
+
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+        return GetFromDictionarySource(culture, area, alias, tokens);
+    }
+
+    /// 
+    ///     Returns all key/values in storage for the given culture
+    /// 
+    public IDictionary GetAllStoredValues(CultureInfo culture)
+    {
+        if (culture == null)
+        {
+            throw new ArgumentNullException(nameof(culture));
+        }
+
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+        if (DictionarySource.ContainsKey(culture) == false)
+        {
+            _logger.LogWarning(
+                "The culture specified {Culture} was not found in any configured sources for this service",
+                culture);
+            return new Dictionary(0);
+        }
+
+        IDictionary result = new Dictionary();
+
+        // convert all areas + keys to a single key with a '/'
+        foreach (KeyValuePair> area in DictionarySource[culture].Value)
+        {
+            foreach (KeyValuePair key in area.Value)
             {
-                var noAreaAliasValue =
-                    new Lazy>(() => GetNoAreaStoredTranslations(xmlSources, xmlSource.Key));
-                cultureNoAreaDictionary.Add(xmlSource.Key, noAreaAliasValue);
-            }
+                var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key);
 
-            return cultureNoAreaDictionary;
-        }
-
-        private IDictionary>>>
-            FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
-        {
-            var xmlSources = fileSources.GetXmlSources();
-            return XmlSourcesToAreaDictionary(xmlSources);
-        }
-
-        private IDictionary>>>
-            XmlSourcesToAreaDictionary(IDictionary> xmlSources)
-        {
-            var cultureDictionary =
-                new Dictionary>>>();
-            foreach (var xmlSource in xmlSources)
-            {
-                var areaAliaValue =
-                    new Lazy>>(() =>
-                        GetAreaStoredTranslations(xmlSources, xmlSource.Key));
-                cultureDictionary.Add(xmlSource.Key, areaAliaValue);
-            }
-
-            return cultureDictionary;
-        }
-
-        /// 
-        /// Initializes with an XML source
-        /// 
-        /// 
-        /// 
-        public LocalizedTextService(IDictionary> source,
-            ILogger logger)
-        {
-            if (source == null) throw new ArgumentNullException(nameof(source));
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-
-            _dictionarySourceLazy =
-                new Lazy>>>>(() =>
-                    XmlSourcesToAreaDictionary(source));
-            _noAreaDictionarySourceLazy =
-                new Lazy>>>(() =>
-                    XmlSourceToNoAreaDictionary(source));
-        }
-
-        [Obsolete("Use other ctor with IDictionary>>> as input parameter.")]
-        public LocalizedTextService(IDictionary>> source,
-            ILogger logger) : this(source.ToDictionary(x=>x.Key, x=> new Lazy>>(() => x.Value)), logger)
-        {
-
-        }
-        /// 
-        /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values
-        /// 
-        /// 
-        /// 
-        public LocalizedTextService(
-            IDictionary>>> source,
-            ILogger logger)
-        {
-            var dictionarySource = source ?? throw new ArgumentNullException(nameof(source));
-            _dictionarySourceLazy =
-                new Lazy>>>>(() =>
-                    dictionarySource);
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            var cultureNoAreaDictionary = new Dictionary>>();
-            foreach (var cultureDictionary in dictionarySource)
-            {
-                var areaAliaValue = GetAreaStoredTranslations(source, cultureDictionary.Key);
-
-                cultureNoAreaDictionary.Add(cultureDictionary.Key,
-                    new Lazy>(() => GetAliasValues(areaAliaValue)));
-            }
-
-            _noAreaDictionarySourceLazy =
-                new Lazy>>>(() => cultureNoAreaDictionary);
-        }
-
-        private static Dictionary GetAliasValues(
-            Dictionary> areaAliaValue)
-        {
-            var aliasValue = new Dictionary();
-            foreach (var area in areaAliaValue)
-            {
-                foreach (var alias in area.Value)
-                {
-                    if (!aliasValue.ContainsKey(alias.Key))
-                    {
-                        aliasValue.Add(alias.Key, alias.Value);
-                    }
-                }
-            }
-
-            return aliasValue;
-        }
-
-        public string Localize(string key, CultureInfo culture, IDictionary? tokens = null)
-        {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
-
-            //This is what the legacy ui service did
-            if (string.IsNullOrEmpty(key))
-                return string.Empty;
-
-            var keyParts = key.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
-            var area = keyParts.Length > 1 ? keyParts[0] : null;
-            var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
-            return Localize(area, alias, culture, tokens);
-        }
-
-        public string Localize(string? area, string? alias, CultureInfo? culture,
-            IDictionary? tokens = null)
-        {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
-
-            //This is what the legacy ui service did
-            if (string.IsNullOrEmpty(alias))
-                return string.Empty;
-
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
-
-            return GetFromDictionarySource(culture, area, alias, tokens);
-        }
-
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        public IDictionary GetAllStoredValues(CultureInfo culture)
-        {
-            if (culture == null) throw new ArgumentNullException(nameof(culture));
-
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
-
-            if (_dictionarySource.ContainsKey(culture) == false)
-            {
-                _logger.LogWarning(
-                    "The culture specified {Culture} was not found in any configured sources for this service",
-                    culture);
-                return new Dictionary(0);
-            }
-
-            IDictionary result = new Dictionary();
-            //convert all areas + keys to a single key with a '/'
-            foreach (var area in _dictionarySource[culture].Value)
-            {
-                foreach (var key in area.Value)
-                {
-                    var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key);
-                    //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case.
-                    if (result.ContainsKey(dictionaryKey) == false)
-                    {
-                        result.Add(dictionaryKey, key.Value);
-                    }
-                }
-            }
-
-            return result;
-        }
-
-        private IDictionary> GetAreaStoredTranslations(
-            IDictionary> xmlSource, CultureInfo cult)
-        {
-            var overallResult = new Dictionary>(StringComparer.InvariantCulture);
-            var areas = xmlSource[cult].Value.XPathSelectElements("//area");
-            foreach (var area in areas)
-            {
-                var result = new Dictionary(StringComparer.InvariantCulture);
-                var keys = area.XPathSelectElements("./key");
-                foreach (var key in keys)
-                {
-                    var dictionaryKey =
-                        (string)key.Attribute("alias")!;
-                    //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
-                    if (result.ContainsKey(dictionaryKey) == false)
-                        result.Add(dictionaryKey, key.Value);
-                }
-
-                overallResult.Add(area.Attribute("alias")!.Value, result);
-            }
-
-            //Merge English Dictionary
-            var englishCulture = new CultureInfo("en-US");
-            if (!cult.Equals(englishCulture))
-            {
-                var enUS = xmlSource[englishCulture].Value.XPathSelectElements("//area");
-                foreach (var area in enUS)
-                {
-                    IDictionary
-                        result = new Dictionary(StringComparer.InvariantCulture);
-                    if (overallResult.ContainsKey(area.Attribute("alias")!.Value))
-                    {
-                        result = overallResult[area.Attribute("alias")!.Value];
-                    }
-
-                    var keys = area.XPathSelectElements("./key");
-                    foreach (var key in keys)
-                    {
-                        var dictionaryKey =
-                            (string)key.Attribute("alias")!;
-                        //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
-                        if (result.ContainsKey(dictionaryKey) == false)
-                            result.Add(dictionaryKey, key.Value);
-                    }
-
-                    if (!overallResult.ContainsKey(area.Attribute("alias")!.Value))
-                    {
-                        overallResult.Add(area.Attribute("alias")!.Value, result);
-                    }
-                }
-            }
-
-            return overallResult;
-        }
-
-        private Dictionary GetNoAreaStoredTranslations(
-            IDictionary> xmlSource, CultureInfo cult)
-        {
-            var result = new Dictionary(StringComparer.InvariantCulture);
-            var keys = xmlSource[cult].Value.XPathSelectElements("//key");
-
-            foreach (var key in keys)
-            {
-                var dictionaryKey =
-                    (string)key.Attribute("alias")!;
-                //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                // i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case.
                 if (result.ContainsKey(dictionaryKey) == false)
+                {
                     result.Add(dictionaryKey, key.Value);
-            }
-
-            //Merge English Dictionary
-            var englishCulture = new CultureInfo("en-US");
-            if (!cult.Equals(englishCulture))
-            {
-                var keysEn = xmlSource[englishCulture].Value.XPathSelectElements("//key");
-
-                foreach (var key in keys)
-                {
-                    var dictionaryKey =
-                        (string)key.Attribute("alias")!;
-                    //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
-                    if (result.ContainsKey(dictionaryKey) == false)
-                        result.Add(dictionaryKey, key.Value);
                 }
             }
-
-            return result;
         }
 
-        private Dictionary> GetAreaStoredTranslations(
-            IDictionary>>> dictionarySource,
-            CultureInfo cult)
+        return result;
+    }
+
+    /// 
+    ///     Returns a list of all currently supported cultures
+    /// 
+    /// 
+    public IEnumerable GetSupportedCultures() => DictionarySource.Keys;
+
+    /// 
+    ///     Tries to resolve a full 4 letter culture from a 2 letter culture name
+    /// 
+    /// 
+    ///     The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be
+    ///     returned
+    /// 
+    /// 
+    /// 
+    ///     TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since
+    ///     that
+    ///     is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this
+    ///     attempts
+    ///     to resolve the full culture if possible.
+    ///     This only works when this service is constructed with the LocalizedTextServiceFileSources
+    /// 
+    public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture)
+    {
+        if (currentCulture == null)
         {
-            var overallResult = new Dictionary>(StringComparer.InvariantCulture);
-            var areaDict = dictionarySource[cult];
-
-            foreach (var area in areaDict.Value)
-            {
-                var result = new Dictionary(StringComparer.InvariantCulture);
-                var keys = area.Value.Keys;
-                foreach (var key in keys)
-                {
-                    //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
-                    if (result.ContainsKey(key) == false)
-                        result.Add(key, area.Value[key]);
-                }
-
-                overallResult.Add(area.Key, result);
-            }
-
-            return overallResult;
+            throw new ArgumentNullException("currentCulture");
         }
 
-        /// 
-        /// Returns a list of all currently supported cultures
-        /// 
-        /// 
-        public IEnumerable GetSupportedCultures()
+        if (_fileSources == null)
         {
-            return _dictionarySource.Keys;
+            return currentCulture;
         }
 
-        /// 
-        /// Tries to resolve a full 4 letter culture from a 2 letter culture name
-        /// 
-        /// 
-        /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned
-        /// 
-        /// 
-        /// 
-        /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that
-        /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts
-        /// to resolve the full culture if possible.
-        ///
-        /// This only works when this service is constructed with the LocalizedTextServiceFileSources
-        /// 
-        public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture)
+        if (currentCulture.Name.Length > 2)
         {
-            if (currentCulture == null) throw new ArgumentNullException("currentCulture");
-
-            if (_fileSources == null) return currentCulture;
-            if (currentCulture.Name.Length > 2) return currentCulture;
-
-            var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
-            return attempt.Success ? attempt.Result! : currentCulture;
+            return currentCulture;
         }
 
-        private string GetFromDictionarySource(CultureInfo culture, string? area, string key,
-            IDictionary? tokens)
+        Attempt attempt =
+            _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
+        return attempt.Success ? attempt.Result! : currentCulture;
+    }
+
+    /// 
+    ///     Returns all key/values in storage for the given culture
+    /// 
+    /// 
+    public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture)
+    {
+        if (culture == null)
         {
-            if (_dictionarySource.ContainsKey(culture) == false)
-            {
-                _logger.LogWarning(
-                    "The culture specified {Culture} was not found in any configured sources for this service",
-                    culture);
-                return "[" + key + "]";
-            }
-
-
-            string? found = null;
-            if (string.IsNullOrWhiteSpace(area))
-            {
-                _noAreaDictionarySource[culture].Value.TryGetValue(key, out found);
-            }
-            else
-            {
-                if (_dictionarySource[culture].Value.TryGetValue(area, out var areaDictionary))
-                {
-                    areaDictionary.TryGetValue(key, out found);
-                }
-
-                if (found == null)
-                {
-                    _noAreaDictionarySource[culture].Value.TryGetValue(key, out found);
-                }
-            }
-
-
-            if (found != null)
-            {
-                return ParseTokens(found, tokens);
-            }
-
-            //NOTE: Based on how legacy works, the default text does not contain the area, just the key
-            return "[" + key + "]";
+            throw new ArgumentNullException("culture");
         }
 
-        /// 
-        /// Parses the tokens in the value
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a % symbol.
-        /// For example: hello %0%, you are %1% !
-        ///
-        /// Since we're going to continue using the same language files for now, the token system needs to remain the same. With our new service
-        /// we support a dictionary which means in the future we can really have any sort of token system.
-        /// Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw exceptions if that is not the case.
-        /// 
-        internal static string ParseTokens(string value, IDictionary? tokens)
+        // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+        culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+        if (DictionarySource.ContainsKey(culture) == false)
         {
-            if (tokens == null || tokens.Any() == false)
-            {
-                return value;
-            }
+            _logger.LogWarning(
+                "The culture specified {Culture} was not found in any configured sources for this service",
+                culture);
+            return new Dictionary>(0);
+        }
 
-            foreach (var token in tokens)
-            {
-                value = value.Replace(string.Concat("%", token.Key, "%"), token.Value);
-            }
+        return DictionarySource[culture].Value;
+    }
 
+    public string Localize(string key, CultureInfo culture, IDictionary? tokens = null)
+    {
+        if (culture == null)
+        {
+            throw new ArgumentNullException(nameof(culture));
+        }
+
+        // This is what the legacy ui service did
+        if (string.IsNullOrEmpty(key))
+        {
+            return string.Empty;
+        }
+
+        var keyParts = key.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
+        var area = keyParts.Length > 1 ? keyParts[0] : null;
+        var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
+        return Localize(area, alias, culture, tokens);
+    }
+
+    /// 
+    ///     Parses the tokens in the value
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    ///     This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a %
+    ///     symbol.
+    ///     For example: hello %0%, you are %1% !
+    ///     Since we're going to continue using the same language files for now, the token system needs to remain the same.
+    ///     With our new service
+    ///     we support a dictionary which means in the future we can really have any sort of token system.
+    ///     Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw
+    ///     exceptions if that is not the case.
+    /// 
+    internal static string ParseTokens(string value, IDictionary? tokens)
+    {
+        if (tokens == null || tokens.Any() == false)
+        {
             return value;
         }
 
-        /// 
-        /// Returns all key/values in storage for the given culture
-        /// 
-        /// 
-        public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture)
+        foreach (KeyValuePair token in tokens)
         {
-            if (culture == null)
-            {
-                throw new ArgumentNullException("culture");
-            }
-
-            // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
-            culture = ConvertToSupportedCultureWithRegionCode(culture);
-
-            if (_dictionarySource.ContainsKey(culture) == false)
-            {
-                _logger.LogWarning(
-                    "The culture specified {Culture} was not found in any configured sources for this service",
-                    culture);
-                return new Dictionary>(0);
-            }
-
-            return _dictionarySource[culture].Value;
+            value = value.Replace(string.Concat("%", token.Key, "%"), token.Value);
         }
+
+        return value;
+    }
+
+    private static Dictionary GetAliasValues(
+        Dictionary> areaAliaValue)
+    {
+        var aliasValue = new Dictionary();
+        foreach (KeyValuePair> area in areaAliaValue)
+        {
+            foreach (KeyValuePair alias in area.Value)
+            {
+                if (!aliasValue.ContainsKey(alias.Key))
+                {
+                    aliasValue.Add(alias.Key, alias.Value);
+                }
+            }
+        }
+
+        return aliasValue;
+    }
+
+    private IDictionary>> FileSourcesToNoAreaDictionarySources(
+        LocalizedTextServiceFileSources fileSources)
+    {
+        IDictionary> xmlSources = fileSources.GetXmlSources();
+
+        return XmlSourceToNoAreaDictionary(xmlSources);
+    }
+
+    private IDictionary>> XmlSourceToNoAreaDictionary(
+        IDictionary> xmlSources)
+    {
+        var cultureNoAreaDictionary = new Dictionary>>();
+        foreach (KeyValuePair> xmlSource in xmlSources)
+        {
+            var noAreaAliasValue =
+                new Lazy>(() => GetNoAreaStoredTranslations(xmlSources, xmlSource.Key));
+            cultureNoAreaDictionary.Add(xmlSource.Key, noAreaAliasValue);
+        }
+
+        return cultureNoAreaDictionary;
+    }
+
+    private IDictionary>>>
+        FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
+    {
+        IDictionary> xmlSources = fileSources.GetXmlSources();
+        return XmlSourcesToAreaDictionary(xmlSources);
+    }
+
+    private IDictionary>>>
+        XmlSourcesToAreaDictionary(IDictionary> xmlSources)
+    {
+        var cultureDictionary =
+            new Dictionary>>>();
+        foreach (KeyValuePair> xmlSource in xmlSources)
+        {
+            var areaAliaValue =
+                new Lazy>>(() =>
+                    GetAreaStoredTranslations(xmlSources, xmlSource.Key));
+            cultureDictionary.Add(xmlSource.Key, areaAliaValue);
+        }
+
+        return cultureDictionary;
+    }
+
+    private IDictionary> GetAreaStoredTranslations(
+        IDictionary> xmlSource, CultureInfo cult)
+    {
+        var overallResult = new Dictionary>(StringComparer.InvariantCulture);
+        IEnumerable areas = xmlSource[cult].Value.XPathSelectElements("//area");
+        foreach (XElement area in areas)
+        {
+            var result = new Dictionary(StringComparer.InvariantCulture);
+            IEnumerable keys = area.XPathSelectElements("./key");
+            foreach (XElement key in keys)
+            {
+                var dictionaryKey =
+                    (string)key.Attribute("alias")!;
+
+                // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                if (result.ContainsKey(dictionaryKey) == false)
+                {
+                    result.Add(dictionaryKey, key.Value);
+                }
+            }
+
+            overallResult.Add(area.Attribute("alias")!.Value, result);
+        }
+
+        // Merge English Dictionary
+        var englishCulture = new CultureInfo("en-US");
+        if (!cult.Equals(englishCulture))
+        {
+            IEnumerable enUS = xmlSource[englishCulture].Value.XPathSelectElements("//area");
+            foreach (XElement area in enUS)
+            {
+                IDictionary
+                    result = new Dictionary(StringComparer.InvariantCulture);
+                if (overallResult.ContainsKey(area.Attribute("alias")!.Value))
+                {
+                    result = overallResult[area.Attribute("alias")!.Value];
+                }
+
+                IEnumerable keys = area.XPathSelectElements("./key");
+                foreach (XElement key in keys)
+                {
+                    var dictionaryKey =
+                        (string)key.Attribute("alias")!;
+
+                    // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                    if (result.ContainsKey(dictionaryKey) == false)
+                    {
+                        result.Add(dictionaryKey, key.Value);
+                    }
+                }
+
+                if (!overallResult.ContainsKey(area.Attribute("alias")!.Value))
+                {
+                    overallResult.Add(area.Attribute("alias")!.Value, result);
+                }
+            }
+        }
+
+        return overallResult;
+    }
+
+    private Dictionary GetNoAreaStoredTranslations(
+        IDictionary> xmlSource, CultureInfo cult)
+    {
+        var result = new Dictionary(StringComparer.InvariantCulture);
+        IEnumerable keys = xmlSource[cult].Value.XPathSelectElements("//key");
+
+        foreach (XElement key in keys)
+        {
+            var dictionaryKey =
+                (string)key.Attribute("alias")!;
+
+            // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+            if (result.ContainsKey(dictionaryKey) == false)
+            {
+                result.Add(dictionaryKey, key.Value);
+            }
+        }
+
+        // Merge English Dictionary
+        var englishCulture = new CultureInfo("en-US");
+        if (!cult.Equals(englishCulture))
+        {
+            IEnumerable keysEn = xmlSource[englishCulture].Value.XPathSelectElements("//key");
+
+            foreach (XElement key in keys)
+            {
+                var dictionaryKey =
+                    (string)key.Attribute("alias")!;
+
+                // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                if (result.ContainsKey(dictionaryKey) == false)
+                {
+                    result.Add(dictionaryKey, key.Value);
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private Dictionary> GetAreaStoredTranslations(
+        IDictionary>>> dictionarySource,
+        CultureInfo cult)
+    {
+        var overallResult = new Dictionary>(StringComparer.InvariantCulture);
+        Lazy>> areaDict = dictionarySource[cult];
+
+        foreach (KeyValuePair> area in areaDict.Value)
+        {
+            var result = new Dictionary(StringComparer.InvariantCulture);
+            ICollection keys = area.Value.Keys;
+            foreach (var key in keys)
+            {
+                // there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+                if (result.ContainsKey(key) == false)
+                {
+                    result.Add(key, area.Value[key]);
+                }
+            }
+
+            overallResult.Add(area.Key, result);
+        }
+
+        return overallResult;
+    }
+
+    private string GetFromDictionarySource(CultureInfo culture, string? area, string key, IDictionary? tokens)
+    {
+        if (DictionarySource.ContainsKey(culture) == false)
+        {
+            _logger.LogWarning(
+                "The culture specified {Culture} was not found in any configured sources for this service",
+                culture);
+            return "[" + key + "]";
+        }
+
+        string? found = null;
+        if (string.IsNullOrWhiteSpace(area))
+        {
+            NoAreaDictionarySource[culture].Value.TryGetValue(key, out found);
+        }
+        else
+        {
+            if (DictionarySource[culture].Value.TryGetValue(area, out IDictionary? areaDictionary))
+            {
+                areaDictionary.TryGetValue(key, out found);
+            }
+
+            if (found == null)
+            {
+                NoAreaDictionarySource[culture].Value.TryGetValue(key, out found);
+            }
+        }
+
+        if (found != null)
+        {
+            return ParseTokens(found, tokens);
+        }
+
+        // NOTE: Based on how legacy works, the default text does not contain the area, just the key
+        return "[" + key + "]";
     }
 }
diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
index 8a9559f7bc..f8b44759a0 100644
--- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
@@ -1,91 +1,103 @@
 // Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
-using System.Threading;
 using Umbraco.Cms.Core.Dictionary;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+/// 
+///     Extension methods for ILocalizedTextService
+/// 
+public static class LocalizedTextServiceExtensions
 {
+    public static string Localize(this ILocalizedTextService manager, string area, T key)
+        where T : Enum =>
+        manager.Localize(area, key.ToString(), Thread.CurrentThread.CurrentUICulture);
+
+    public static string Localize(this ILocalizedTextService manager, string? area, string? alias)
+        => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture);
+
     /// 
-    /// Extension methods for ILocalizedTextService
+    ///     Localize using the current thread culture
     /// 
-    public static class LocalizedTextServiceExtensions
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public static string Localize(this ILocalizedTextService manager, string? area, string alias, string?[]? tokens)
+        => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens));
+
+    /// 
+    ///     Localize a key without any variables
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture, string?[] tokens)
+        => manager.Localize(area, alias, culture, ConvertToDictionaryVars(tokens));
+
+    public static string? UmbracoDictionaryTranslate(
+        this ILocalizedTextService manager,
+        ICultureDictionary cultureDictionary,
+        string? text)
     {
-         public static string Localize(this ILocalizedTextService manager, string area, T key)
-         where T: System.Enum =>
-             manager.Localize(area, key.ToString(), Thread.CurrentThread.CurrentUICulture);
+        if (text == null)
+        {
+            return null;
+        }
 
-        public static string Localize(this ILocalizedTextService manager, string? area, string? alias)
-            => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture);
+        if (text.StartsWith("#") == false)
+        {
+            return text;
+        }
 
-        /// 
-        /// Localize using the current thread culture
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public static string Localize(this ILocalizedTextService manager, string? area, string alias, string?[]? tokens)
-                    => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens));
+        text = text.Substring(1);
+        var value = cultureDictionary[text];
+        if (value.IsNullOrWhiteSpace() == false)
+        {
+            return value;
+        }
 
-        /// 
-        /// Localize a key without any variables
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture, string?[] tokens)
-            => manager.Localize(area, alias, culture, ConvertToDictionaryVars(tokens));
+        if (text.IndexOf('_') == -1)
+        {
+            return text;
+        }
 
-         /// 
-         /// Convert an array of strings to a dictionary of indices -> values
-         /// 
-         /// 
-         /// 
-         internal static IDictionary? ConvertToDictionaryVars(string?[]? variables)
-         {
-             if (variables == null) return null;
-             if (variables.Any() == false) return null;
+        var areaAndKey = text.Split('_');
 
-             return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s })
-                 .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value);
-         }
+        if (areaAndKey.Length < 2)
+        {
+            return text;
+        }
 
-         public static string? UmbracoDictionaryTranslate(this ILocalizedTextService manager, ICultureDictionary cultureDictionary, string? text)
-         {
-             if (text == null)
-                 return null;
+        value = manager.Localize(areaAndKey[0], areaAndKey[1]);
+        return value.StartsWith("[") ? text : value;
+    }
 
-             if (text.StartsWith("#") == false)
-                 return text;
+    /// 
+    ///     Convert an array of strings to a dictionary of indices -> values
+    /// 
+    /// 
+    /// 
+    internal static IDictionary? ConvertToDictionaryVars(string?[]? variables)
+    {
+        if (variables == null)
+        {
+            return null;
+        }
 
-             text = text.Substring(1);
-             var value = cultureDictionary[text];
-             if (value.IsNullOrWhiteSpace() == false)
-             {
-                 return value;
-             }
-
-             if (text.IndexOf('_') == -1)
-                 return text;
-
-             var areaAndKey = text.Split('_');
-
-             if (areaAndKey.Length < 2)
-                return text;
-
-             value = manager.Localize(areaAndKey[0], areaAndKey[1]);
-             return value.StartsWith("[") ? text : value;
-         }
+        if (variables.Any() == false)
+        {
+            return null;
+        }
 
+        return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s })
+            .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value);
     }
 }
diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
index 41d12f9a45..26a2e9fb60 100644
--- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.IO;
-using System.Linq;
 using System.Xml;
 using System.Xml.Linq;
 using Microsoft.Extensions.FileProviders;
@@ -11,291 +7,313 @@ using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Cache;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care
+///     of
+/// 
+public class LocalizedTextServiceFileSources
 {
-    /// 
-    /// Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care of
-    /// 
-    public class LocalizedTextServiceFileSources
+    private readonly IAppPolicyCache _cache;
+    private readonly IDirectoryContents _directoryContents;
+    private readonly DirectoryInfo? _fileSourceFolder;
+    private readonly ILogger _logger;
+    private readonly IEnumerable? _supplementFileSources;
+
+    // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
+    private readonly Dictionary _twoLetterCultureConverter = new();
+
+    private readonly Lazy>> _xmlSources;
+
+    [Obsolete("Use ctor with all params. This will be removed in Umbraco 12")]
+    public LocalizedTextServiceFileSources(
+        ILogger logger,
+        AppCaches appCaches,
+        DirectoryInfo fileSourceFolder,
+        IEnumerable supplementFileSources)
+        : this(
+            logger,
+            appCaches,
+            fileSourceFolder,
+            supplementFileSources,
+            new NotFoundDirectoryContents())
     {
-        private readonly ILogger _logger;
-        private readonly IDirectoryContents _directoryContents;
-        private readonly IAppPolicyCache _cache;
-        private readonly IEnumerable? _supplementFileSources;
-        private readonly DirectoryInfo? _fileSourceFolder;
+    }
 
-        // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
-        private readonly Dictionary _twoLetterCultureConverter = new Dictionary();
-
-        private readonly Lazy>> _xmlSources;
-
-        [Obsolete("Use ctor with all params. This will be removed in Umbraco 12")]
-        public LocalizedTextServiceFileSources(
-            ILogger logger,
-            AppCaches appCaches,
-            DirectoryInfo fileSourceFolder,
-            IEnumerable supplementFileSources)
-            :this(
-                logger,
-                appCaches,
-                fileSourceFolder,
-                supplementFileSources, new NotFoundDirectoryContents())
+    /// 
+    ///     This is used to configure the file sources with the main file sources shipped with Umbraco and also including
+    ///     supplemental/plugin based
+    ///     localization files. The supplemental files will be loaded in and merged in after the primary files.
+    ///     The supplemental files must be named with the 4 letter culture name with a hyphen such as : en-AU.xml
+    /// 
+    public LocalizedTextServiceFileSources(
+        ILogger logger,
+        AppCaches appCaches,
+        DirectoryInfo fileSourceFolder,
+        IEnumerable supplementFileSources,
+        IDirectoryContents directoryContents)
+    {
+        if (appCaches == null)
         {
-
+            throw new ArgumentNullException("appCaches");
         }
 
-        /// 
-        /// This is used to configure the file sources with the main file sources shipped with Umbraco and also including supplemental/plugin based
-        /// localization files. The supplemental files will be loaded in and merged in after the primary files.
-        /// The supplemental files must be named with the 4 letter culture name with a hyphen such as : en-AU.xml
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public LocalizedTextServiceFileSources(
-            ILogger logger,
-            AppCaches appCaches,
-            DirectoryInfo fileSourceFolder,
-            IEnumerable supplementFileSources,
-            IDirectoryContents directoryContents
-            )
+        _logger = logger ?? throw new ArgumentNullException("logger");
+        _directoryContents = directoryContents;
+        _cache = appCaches.RuntimeCache;
+        _fileSourceFolder = fileSourceFolder ?? throw new ArgumentNullException("fileSourceFolder");
+        _supplementFileSources = supplementFileSources;
+
+        // Create the lazy source for the _xmlSources
+        _xmlSources = new Lazy>>(() =>
         {
-            if (logger == null) throw new ArgumentNullException("logger");
-            if (appCaches == null) throw new ArgumentNullException("cache");
-            if (fileSourceFolder == null) throw new ArgumentNullException("fileSourceFolder");
+            var result = new Dictionary>();
 
-            _logger = logger;
-            _directoryContents = directoryContents;
-            _cache = appCaches.RuntimeCache;
-            _fileSourceFolder = fileSourceFolder;
-            _supplementFileSources = supplementFileSources;
+            IEnumerable files = GetLanguageFiles();
 
-            //Create the lazy source for the _xmlSources
-            _xmlSources = new Lazy>>(() =>
+            if (!files.Any())
             {
-                var result = new Dictionary>();
+                return result;
+            }
 
+            foreach (IFileInfo fileInfo in files)
+            {
+                IFileInfo localCopy = fileInfo;
+                var filename = Path.GetFileNameWithoutExtension(localCopy.Name).Replace("_", "-");
 
-                var files = GetLanguageFiles();
-
-                if (!files.Any())
+                // TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct
+                // names instead of storing them as 2 letters but actually having a 4 letter culture. So now, we
+                // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that
+                // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea)
+                // that any 4 letter file is named with the actual culture that it is!
+                CultureInfo? culture = null;
+                if (filename.Length == 2)
                 {
-                    return result;
-                }
-
-                foreach (var fileInfo in files)
-                {
-                    var localCopy = fileInfo;
-                    var filename = Path.GetFileNameWithoutExtension(localCopy.Name).Replace("_", "-");
-
-                    // TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct
-                    // names instead of storing them as 2 letters but actually having a 4 letter culture. So now, we
-                    // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that
-                    // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea)
-                    // that any 4 letter file is named with the actual culture that it is!
-                    CultureInfo? culture = null;
-                    if (filename.Length == 2)
+                    // we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't
+                    // want to load in the entire doc into mem just to read a single value
+                    using (Stream fs = fileInfo.CreateReadStream())
+                    using (var reader = XmlReader.Create(fs))
                     {
-                        //we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't
-                        //want to load in the entire doc into mem just to read a single value
-                        using (var fs = fileInfo.CreateReadStream())
-                        using (var reader = XmlReader.Create(fs))
+                        if (reader.IsStartElement())
                         {
-                            if (reader.IsStartElement())
+                            if (reader.Name == "language")
                             {
-                                if (reader.Name == "language")
+                                if (reader.MoveToAttribute("culture"))
                                 {
-                                    if (reader.MoveToAttribute("culture"))
+                                    var cultureVal = reader.Value;
+                                    try
                                     {
-                                        var cultureVal = reader.Value;
-                                        try
-                                        {
-                                            culture = CultureInfo.GetCultureInfo(cultureVal);
-                                            //add to the tracked dictionary
-                                            _twoLetterCultureConverter[filename] = culture;
-                                        }
-                                        catch (CultureNotFoundException)
-                                        {
-                                            _logger.LogWarning("The culture {CultureValue} found in the file {CultureFile} is not a valid culture", cultureVal, fileInfo.Name);
-                                            //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise
-                                            // an exception will be thrown.
-                                        }
+                                        culture = CultureInfo.GetCultureInfo(cultureVal);
+
+                                        // add to the tracked dictionary
+                                        _twoLetterCultureConverter[filename] = culture;
+                                    }
+                                    catch (CultureNotFoundException)
+                                    {
+                                        _logger.LogWarning(
+                                            "The culture {CultureValue} found in the file {CultureFile} is not a valid culture",
+                                            cultureVal,
+                                            fileInfo.Name);
+
+                                        // If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise
+                                        // an exception will be thrown.
                                     }
                                 }
                             }
                         }
                     }
-                    if (culture == null)
-                    {
-                        culture = CultureInfo.GetCultureInfo(filename);
-                    }
-
-                    //get the lazy value from cache
-                    result[culture] = new Lazy(() => _cache.GetCacheItem(
-                        string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name), () =>
-                        {
-                            XDocument xdoc;
-
-                            //load in primary
-                            using (var fs = localCopy.CreateReadStream())
-                            {
-                                xdoc = XDocument.Load(fs);
-                            }
-
-                            //load in supplementary
-                            MergeSupplementaryFiles(culture, xdoc);
-
-                            return xdoc;
-                        }, isSliding: true, timeout: TimeSpan.FromMinutes(10))!);
                 }
-                return result;
-            });
 
+                if (culture == null)
+                {
+                    culture = CultureInfo.GetCultureInfo(filename);
+                }
 
-        }
+                // get the lazy value from cache
+                result[culture] = new Lazy(
+                    () => _cache.GetCacheItem(
+                        string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name),
+                        () =>
+                    {
+                        XDocument xdoc;
 
-        private IEnumerable GetLanguageFiles()
-        {
-            var result = new List();
+                        // load in primary
+                        using (Stream fs = localCopy.CreateReadStream())
+                        {
+                            xdoc = XDocument.Load(fs);
+                        }
 
-            if (_fileSourceFolder is not null && _fileSourceFolder.Exists)
-            {
+                        // load in supplementary
+                        MergeSupplementaryFiles(culture, xdoc);
 
-                result.AddRange(
-                    new PhysicalDirectoryContents(_fileSourceFolder.FullName)
-                    .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))
-                );
-            }
-
-            if (_directoryContents.Exists)
-            {
-                result.AddRange(
-                _directoryContents
-                        .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))
-                );
+                        return xdoc;
+                    },
+                        isSliding: true,
+                        timeout: TimeSpan.FromMinutes(10))!);
             }
 
             return result;
+        });
+    }
+
+    /// 
+    ///     Constructor
+    /// 
+    public LocalizedTextServiceFileSources(ILogger logger, AppCaches appCaches, DirectoryInfo fileSourceFolder)
+        : this(logger, appCaches, fileSourceFolder, Enumerable.Empty())
+    {
+    }
+
+    /// 
+    ///     returns all xml sources for all culture files found in the folder
+    /// 
+    /// 
+    public IDictionary> GetXmlSources() => _xmlSources.Value;
+
+    private IEnumerable GetLanguageFiles()
+    {
+        var result = new List();
+
+        if (_fileSourceFolder is not null && _fileSourceFolder.Exists)
+        {
+            result.AddRange(
+                new PhysicalDirectoryContents(_fileSourceFolder.FullName)
+                    .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml")));
         }
 
-        /// 
-        /// Constructor
-        /// 
-        public LocalizedTextServiceFileSources(ILogger logger, AppCaches appCaches, DirectoryInfo fileSourceFolder)
-            : this(logger, appCaches, fileSourceFolder, Enumerable.Empty())
-        { }
-
-        /// 
-        /// returns all xml sources for all culture files found in the folder
-        /// 
-        /// 
-        public IDictionary> GetXmlSources()
+        if (_directoryContents.Exists)
         {
-            return _xmlSources.Value;
+            result.AddRange(
+                _directoryContents
+                    .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml")));
         }
 
-        // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
-        public Attempt TryConvert2LetterCultureTo4Letter(string twoLetterCulture)
+        return result;
+    }
+
+    // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
+    public Attempt TryConvert2LetterCultureTo4Letter(string twoLetterCulture)
+    {
+        if (twoLetterCulture.Length != 2)
         {
-            if (twoLetterCulture.Length != 2) return Attempt.Fail();
-
-            //This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
-            var resolved = _xmlSources.Value;
-
-            return _twoLetterCultureConverter.ContainsKey(twoLetterCulture)
-                ? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture])
-                : Attempt.Fail();
+            return Attempt.Fail();
         }
 
-        // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
-        public Attempt TryConvert4LetterCultureTo2Letter(CultureInfo culture)
+        // This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
+        Dictionary> resolved = _xmlSources.Value;
+
+        return _twoLetterCultureConverter.ContainsKey(twoLetterCulture)
+            ? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture])
+            : Attempt.Fail();
+    }
+
+    // TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
+    public Attempt TryConvert4LetterCultureTo2Letter(CultureInfo culture)
+    {
+        if (culture == null)
         {
-            if (culture == null) throw new ArgumentNullException("culture");
-
-            //This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
-            var resolved = _xmlSources.Value;
-
-            return _twoLetterCultureConverter.Values.Contains(culture)
-                ? Attempt.Succeed(culture.Name.Substring(0, 2))
-                : Attempt.Fail();
+            throw new ArgumentNullException("culture");
         }
 
-        private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc)
+        // This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
+        Dictionary> resolved = _xmlSources.Value;
+
+        return _twoLetterCultureConverter.Values.Contains(culture)
+            ? Attempt.Succeed(culture.Name.Substring(0, 2))
+            : Attempt.Fail();
+    }
+
+    private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc)
+    {
+        if (xMasterDoc.Root == null)
         {
-            if (xMasterDoc.Root == null) return;
-            if (_supplementFileSources != null)
+            return;
+        }
+
+        if (_supplementFileSources != null)
+        {
+            // now load in supplementary
+            IEnumerable found = _supplementFileSources.Where(x =>
             {
-                //now load in supplementary
-                var found = _supplementFileSources.Where(x =>
-                {
-                    var extension = Path.GetExtension(x.File.FullName);
-                    var fileCultureName = Path.GetFileNameWithoutExtension(x.File.FullName).Replace("_", "-").Replace(".user", "");
-                    return extension.InvariantEquals(".xml") && (
-                        fileCultureName.InvariantEquals(culture.Name)
-                        || fileCultureName.InvariantEquals(culture.TwoLetterISOLanguageName)
-                    );
-                });
+                var extension = Path.GetExtension(x.File.FullName);
+                var fileCultureName = Path.GetFileNameWithoutExtension(x.File.FullName).Replace("_", "-")
+                    .Replace(".user", string.Empty);
+                return extension.InvariantEquals(".xml") && (
+                    fileCultureName.InvariantEquals(culture.Name)
+                    || fileCultureName.InvariantEquals(culture.TwoLetterISOLanguageName));
+            });
 
-                foreach (var supplementaryFile in found)
+            foreach (LocalizedTextServiceSupplementaryFileSource supplementaryFile in found)
+            {
+                using (FileStream fs = supplementaryFile.File.OpenRead())
                 {
-                    using (var fs = supplementaryFile.File.OpenRead())
+                    XDocument xChildDoc;
+                    try
                     {
-                        XDocument xChildDoc;
-                        try
-                        {
-                            xChildDoc = XDocument.Load(fs);
-                        }
-                        catch (Exception ex)
-                        {
-                            _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName);
-                            continue;
-                        }
+                        xChildDoc = XDocument.Load(fs);
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName);
+                        continue;
+                    }
 
-                        if (xChildDoc.Root == null || xChildDoc.Root.Name != "language") continue;
-                        foreach (var xArea in xChildDoc.Root.Elements("area")
-                            .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
-                        {
-                            var areaAlias = (string)xArea.Attribute("alias")!;
+                    if (xChildDoc.Root == null || xChildDoc.Root.Name != "language")
+                    {
+                        continue;
+                    }
 
-                            var areaFound = xMasterDoc.Root.Elements("area").FirstOrDefault(x => ((string)x.Attribute("alias")!) == areaAlias);
-                            if (areaFound == null)
-                            {
-                                //add the whole thing
-                                xMasterDoc.Root.Add(xArea);
-                            }
-                            else
-                            {
-                                MergeChildKeys(xArea, areaFound, supplementaryFile.OverwriteCoreKeys);
-                            }
+                    foreach (XElement xArea in xChildDoc.Root.Elements("area")
+                                 .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
+                    {
+                        var areaAlias = (string)xArea.Attribute("alias")!;
+
+                        XElement? areaFound = xMasterDoc.Root.Elements("area").FirstOrDefault(x => (string)x.Attribute("alias")! == areaAlias);
+                        if (areaFound == null)
+                        {
+                            // add the whole thing
+                            xMasterDoc.Root.Add(xArea);
+                        }
+                        else
+                        {
+                            MergeChildKeys(xArea, areaFound, supplementaryFile.OverwriteCoreKeys);
                         }
                     }
                 }
             }
         }
-
-        private void MergeChildKeys(XElement source, XElement destination, bool overwrite)
-        {
-            if (destination == null) throw new ArgumentNullException("destination");
-            if (source == null) throw new ArgumentNullException("source");
-
-            //merge in the child elements
-            foreach (var key in source.Elements("key")
-                .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
-            {
-                var keyAlias = (string)key.Attribute("alias")!;
-                var keyFound = destination.Elements("key").FirstOrDefault(x => ((string)x.Attribute("alias")!) == keyAlias);
-                if (keyFound == null)
-                {
-                    //append, it doesn't exist
-                    destination.Add(key);
-                }
-                else if (overwrite)
-                {
-                    //overwrite
-                    keyFound.Value = key.Value;
-                }
-            }
-        }
+    }
+
+    private void MergeChildKeys(XElement source, XElement destination, bool overwrite)
+    {
+        if (destination == null)
+        {
+            throw new ArgumentNullException("destination");
+        }
+
+        if (source == null)
+        {
+            throw new ArgumentNullException("source");
+        }
+
+        // merge in the child elements
+        foreach (XElement key in source.Elements("key")
+                     .Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
+        {
+            var keyAlias = (string)key.Attribute("alias")!;
+            XElement? keyFound = destination.Elements("key")
+                .FirstOrDefault(x => (string)x.Attribute("alias")! == keyAlias);
+            if (keyFound == null)
+            {
+                // append, it doesn't exist
+                destination.Add(key);
+            }
+            else if (overwrite)
+            {
+                // overwrite
+                keyFound.Value = key.Value;
+            }
+        }
     }
 }
diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs
index 7fe5e0e48a..cff9a55234 100644
--- a/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs
@@ -1,20 +1,14 @@
-using System;
-using System.IO;
+namespace Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Core.Services
+public class LocalizedTextServiceSupplementaryFileSource
 {
-    public class LocalizedTextServiceSupplementaryFileSource
+    public LocalizedTextServiceSupplementaryFileSource(FileInfo file, bool overwriteCoreKeys)
     {
-
-        public LocalizedTextServiceSupplementaryFileSource(FileInfo file, bool overwriteCoreKeys)
-        {
-            if (file == null) throw new ArgumentNullException("file");
-
-            File = file;
-            OverwriteCoreKeys = overwriteCoreKeys;
-        }
-
-        public FileInfo File { get; private set; }
-        public bool OverwriteCoreKeys { get; private set; }
+        File = file ?? throw new ArgumentNullException("file");
+        OverwriteCoreKeys = overwriteCoreKeys;
     }
+
+    public FileInfo File { get; }
+
+    public bool OverwriteCoreKeys { get; }
 }
diff --git a/src/Umbraco.Core/Services/MacroService.cs b/src/Umbraco.Core/Services/MacroService.cs
index 6b598921e1..73889895e2 100644
--- a/src/Umbraco.Core/Services/MacroService.cs
+++ b/src/Umbraco.Core/Services/MacroService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -8,172 +5,172 @@ using Umbraco.Cms.Core.Notifications;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     Represents the Macro Service, which is an easy access to operations involving 
+/// 
+internal class MacroService : RepositoryService, IMacroWithAliasService
 {
-    /// 
-    /// Represents the Macro Service, which is an easy access to operations involving 
-    /// 
-    internal class MacroService : RepositoryService, IMacroWithAliasService
+    private readonly IAuditRepository _auditRepository;
+    private readonly IMacroRepository _macroRepository;
+
+    public MacroService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMacroRepository macroRepository,
+        IAuditRepository auditRepository)
+        : base(provider, loggerFactory, eventMessagesFactory)
     {
-        private readonly IMacroRepository _macroRepository;
-        private readonly IAuditRepository _auditRepository;
+        _macroRepository = macroRepository;
+        _auditRepository = auditRepository;
+    }
 
-        public MacroService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMacroRepository macroRepository, IAuditRepository auditRepository)
-            : base(provider, loggerFactory, eventMessagesFactory)
+    /// 
+    ///     Gets an  object by its alias
+    /// 
+    /// Alias to retrieve an  for
+    /// An  object
+    public IMacro? GetByAlias(string alias)
+    {
+        if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository)
         {
-            _macroRepository = macroRepository;
-            _auditRepository = auditRepository;
+            return GetAll().FirstOrDefault(x => x.Alias == alias);
         }
 
-        /// 
-        /// Gets an  object by its alias
-        /// 
-        /// Alias to retrieve an  for
-        /// An  object
-        public IMacro? GetByAlias(string alias)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository)
-            {
-                return GetAll().FirstOrDefault(x => x.Alias == alias);
-            }
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return macroWithAliasRepository.GetByAlias(alias);
-            }
-        }
-
-        public IEnumerable GetAll()
-        {
-            return GetAll(new int[0]);
-        }
-
-        public IEnumerable GetAll(params int[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.GetMany(ids);
-            }
-        }
-
-        public IEnumerable GetAll(params Guid[] ids)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.GetMany(ids);
-            }
-        }
-
-        public IEnumerable GetAll(params string[] aliases)
-        {
-            if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository)
-            {
-                var hashset = new HashSet(aliases);
-                return GetAll().Where(x => hashset.Contains(x.Alias));
-            }
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return macroWithAliasRepository.GetAllByAlias(aliases) ?? Enumerable.Empty();
-            }
-        }
-
-        public IMacro? GetById(int id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.Get(id);
-            }
-        }
-
-        public IMacro? GetById(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _macroRepository.Get(id);
-            }
-        }
-
-        /// 
-        /// Deletes an 
-        /// 
-        ///  to delete
-        /// Optional id of the user deleting the macro
-        public void Delete(IMacro macro, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var deletingNotification = new MacroDeletingNotification(macro, eventMessages);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _macroRepository.Delete(macro);
-
-                scope.Notifications.Publish(new MacroDeletedNotification(macro, eventMessages).WithStateFrom(deletingNotification));
-                Audit(AuditType.Delete, userId, -1);
-
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Saves an 
-        /// 
-        ///  to save
-        /// Optional Id of the user deleting the macro
-        public void Save(IMacro macro, int userId = Cms.Core.Constants.Security.SuperUserId)
-        {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                EventMessages eventMessages = EventMessagesFactory.Get();
-                var savingNotification = new MacroSavingNotification(macro, eventMessages);
-
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (string.IsNullOrWhiteSpace(macro.Name))
-                {
-                    throw new ArgumentException("Cannot save macro with empty name.");
-                }
-
-                _macroRepository.Save(macro);
-
-                scope.Notifications.Publish(new MacroSavedNotification(macro, eventMessages).WithStateFrom(savingNotification));
-                Audit(AuditType.Save, userId, -1);
-
-                scope.Complete();
-            }
-        }
-
-        ///// 
-        ///// Gets a list all available  plugins
-        ///// 
-        ///// An enumerable list of  objects
-        //public IEnumerable GetMacroPropertyTypes()
-        //{
-        //    return MacroPropertyTypeResolver.Current.MacroPropertyTypes;
-        //}
-
-        ///// 
-        ///// Gets an  by its alias
-        ///// 
-        ///// Alias to retrieve an  for
-        ///// An  object
-        //public IMacroPropertyType GetMacroPropertyTypeByAlias(string alias)
-        //{
-        //    return MacroPropertyTypeResolver.Current.MacroPropertyTypes.FirstOrDefault(x => x.Alias == alias);
-        //}
-
-        private void Audit(AuditType type, int userId, int objectId)
-        {
-            _auditRepository.Save(new AuditItem(objectId, type, userId, "Macro"));
+            return macroWithAliasRepository.GetByAlias(alias);
         }
     }
+
+    public IEnumerable GetAll() => GetAll(new int[0]);
+
+    public IEnumerable GetAll(params int[] ids)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _macroRepository.GetMany(ids);
+        }
+    }
+
+    public IEnumerable GetAll(params Guid[] ids)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _macroRepository.GetMany(ids);
+        }
+    }
+
+    public IEnumerable GetAll(params string[] aliases)
+    {
+        if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository)
+        {
+            var hashset = new HashSet(aliases);
+            return GetAll().Where(x => hashset.Contains(x.Alias));
+        }
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return macroWithAliasRepository.GetAllByAlias(aliases);
+        }
+    }
+
+    public IMacro? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _macroRepository.Get(id);
+        }
+    }
+
+    public IMacro? GetById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _macroRepository.Get(id);
+        }
+    }
+
+    /// 
+    ///     Deletes an 
+    /// 
+    ///  to delete
+    /// Optional id of the user deleting the macro
+    public void Delete(IMacro macro, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var deletingNotification = new MacroDeletingNotification(macro, eventMessages);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            _macroRepository.Delete(macro);
+
+            scope.Notifications.Publish(
+                new MacroDeletedNotification(macro, eventMessages).WithStateFrom(deletingNotification));
+            Audit(AuditType.Delete, userId, -1);
+
+            scope.Complete();
+        }
+    }
+
+    /// 
+    ///     Saves an 
+    /// 
+    ///  to save
+    /// Optional Id of the user deleting the macro
+    public void Save(IMacro macro, int userId = Constants.Security.SuperUserId)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+        {
+            EventMessages eventMessages = EventMessagesFactory.Get();
+            var savingNotification = new MacroSavingNotification(macro, eventMessages);
+
+            if (scope.Notifications.PublishCancelable(savingNotification))
+            {
+                scope.Complete();
+                return;
+            }
+
+            if (string.IsNullOrWhiteSpace(macro.Name))
+            {
+                throw new ArgumentException("Cannot save macro with empty name.");
+            }
+
+            _macroRepository.Save(macro);
+
+            scope.Notifications.Publish(
+                new MacroSavedNotification(macro, eventMessages).WithStateFrom(savingNotification));
+            Audit(AuditType.Save, userId, -1);
+
+            scope.Complete();
+        }
+    }
+
+    ///// 
+    ///// Gets a list all available  plugins
+    ///// 
+    ///// An enumerable list of  objects
+    // public IEnumerable GetMacroPropertyTypes()
+    // {
+    //    return MacroPropertyTypeResolver.Current.MacroPropertyTypes;
+    // }
+
+    ///// 
+    ///// Gets an  by its alias
+    ///// 
+    ///// Alias to retrieve an  for
+    ///// An  object
+    // public IMacroPropertyType GetMacroPropertyTypeByAlias(string alias)
+    // {
+    //    return MacroPropertyTypeResolver.Current.MacroPropertyTypes.FirstOrDefault(x => x.Alias == alias);
+    // }
+    private void Audit(AuditType type, int userId, int objectId) =>
+        _auditRepository.Save(new AuditItem(objectId, type, userId, "Macro"));
 }
diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs
index 13ba415fee..325677407e 100644
--- a/src/Umbraco.Core/Services/MediaService.cs
+++ b/src/Umbraco.Core/Services/MediaService.cs
@@ -1,12 +1,9 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
-using System.IO;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.IO;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Entities;
 using Umbraco.Cms.Core.Notifications;
 using Umbraco.Cms.Core.Persistence;
 using Umbraco.Cms.Core.Persistence.Querying;
@@ -33,9 +30,16 @@ namespace Umbraco.Cms.Core.Services
 
         #region Constructors
 
-        public MediaService(ICoreScopeProvider provider, MediaFileManager mediaFileManager, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IMediaRepository mediaRepository, IAuditRepository auditRepository, IMediaTypeRepository mediaTypeRepository,
-            IEntityRepository entityRepository, IShortStringHelper shortStringHelper)
+        public MediaService(
+            ICoreScopeProvider provider,
+            MediaFileManager mediaFileManager,
+            ILoggerFactory loggerFactory,
+            IEventMessagesFactory eventMessagesFactory,
+            IMediaRepository mediaRepository,
+            IAuditRepository auditRepository,
+            IMediaTypeRepository mediaTypeRepository,
+            IEntityRepository entityRepository,
+            IShortStringHelper shortStringHelper)
             : base(provider, loggerFactory, eventMessagesFactory)
         {
             _mediaFileManager = mediaFileManager;
@@ -52,50 +56,51 @@ namespace Umbraco.Cms.Core.Services
 
         public int Count(string? mediaTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.Count(mediaTypeAlias);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.Count(mediaTypeAlias);
         }
 
         public int CountNotTrashed(string? mediaTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
 
-                var mediaTypeId = 0;
-                if (string.IsNullOrWhiteSpace(mediaTypeAlias) == false)
+            var mediaTypeId = 0;
+            if (string.IsNullOrWhiteSpace(mediaTypeAlias) == false)
+            {
+                IMediaType? mediaType = _mediaTypeRepository.Get(mediaTypeAlias);
+                if (mediaType == null)
                 {
-                    var mediaType = _mediaTypeRepository.Get(mediaTypeAlias);
-                    if (mediaType == null) return 0;
-                    mediaTypeId = mediaType.Id;
+                    return 0;
                 }
 
-                var query = Query().Where(x => x.Trashed == false);
-                if (mediaTypeId > 0)
-                    query = query.Where(x => x.ContentTypeId == mediaTypeId);
-                return _mediaRepository.Count(query);
+                mediaTypeId = mediaType.Id;
             }
+
+            IQuery query = Query().Where(x => x.Trashed == false);
+            if (mediaTypeId > 0)
+            {
+                query = query.Where(x => x.ContentTypeId == mediaTypeId);
+            }
+
+            return _mediaRepository.Count(query);
         }
 
         public int CountChildren(int parentId, string? mediaTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.ReadLock(Constants.Locks.MediaTree);
                 return _mediaRepository.CountChildren(parentId, mediaTypeAlias);
             }
         }
 
         public int CountDescendants(int parentId, string? mediaTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.CountDescendants(parentId, mediaTypeAlias);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.CountDescendants(parentId, mediaTypeAlias);
         }
 
         #endregion
@@ -116,9 +121,9 @@ namespace Umbraco.Cms.Core.Services
         /// Alias of the 
         /// Optional id of the user creating the media item
         /// 
-        public IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMedia(string name, Guid parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            var parent = GetById(parentId);
+            IMedia? parent = GetById(parentId);
             return CreateMedia(name, parent, mediaTypeAlias, userId);
         }
 
@@ -134,25 +139,29 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMedia(string? name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            var mediaType = GetMediaType(mediaTypeAlias);
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias);
             if (mediaType == null)
+            {
                 throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias));
-            var parent = parentId > 0 ? GetById(parentId) : null;
+            }
+
+            IMedia? parent = parentId > 0 ? GetById(parentId) : null;
             if (parentId > 0 && parent == null)
+            {
                 throw new ArgumentException("No media with that id.", nameof(parentId));
+            }
+
             if (name != null && name.Length > 255)
             {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
             }
 
             var media = new Core.Models.Media(name, parentId, mediaType);
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                CreateMedia(scope, media, parent!, userId, false);
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            CreateMedia(scope, media, parent!, userId, false);
+            scope.Complete();
 
             return media;
         }
@@ -168,24 +177,25 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMedia(string name, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMedia(string name, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
             // not locking since not saving anything
 
-            var mediaType = GetMediaType(mediaTypeAlias);
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias);
             if (mediaType == null)
+            {
                 throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias));
+            }
+
             if (name != null && name.Length > 255)
             {
-                throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
             }
 
             var media = new Core.Models.Media(name, -1, mediaType);
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                CreateMedia(scope, media, null, userId, false);
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            CreateMedia(scope, media, null, userId, false);
+            scope.Complete();
 
             return media;
         }
@@ -202,28 +212,32 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMedia(string name, IMedia? parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            if (parent == null) throw new ArgumentNullException(nameof(parent));
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+            if (parent == null)
             {
-                // not locking since not saving anything
-
-                var mediaType = GetMediaType(mediaTypeAlias);
-                if (mediaType == null)
-                    throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
-                if (name != null && name.Length > 255)
-                {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
-                }
-
-                var media = new Core.Models.Media(name, parent, mediaType);
-                CreateMedia(scope, media, parent, userId, false);
-
-                scope.Complete();
-                return media;
+                throw new ArgumentNullException(nameof(parent));
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // not locking since not saving anything
+
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias);
+            if (mediaType == null)
+            {
+                throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
+            }
+
+            if (name != null && name.Length > 255)
+            {
+                throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+            }
+
+            var media = new Core.Models.Media(name, parent, mediaType);
+            CreateMedia(scope, media, parent, userId, false);
+
+            scope.Complete();
+            return media;
         }
 
         /// 
@@ -235,27 +249,29 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // locking the media tree secures media types too
+            scope.WriteLock(Constants.Locks.MediaTree);
+
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias); // + locks
+            if (mediaType == null)
             {
-                // locking the media tree secures media types too
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
-
-                var mediaType = GetMediaType(mediaTypeAlias); // + locks
-                if (mediaType == null)
-                    throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
-
-                var parent = parentId > 0 ? GetById(parentId) : null; // + locks
-                if (parentId > 0 && parent == null)
-                    throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback
-
-                var media = parentId > 0 ? new Core.Models.Media(name, parent, mediaType) : new Core.Models.Media(name, parentId, mediaType);
-                CreateMedia(scope, media, parent, userId, true);
-
-                scope.Complete();
-                return media;
+                throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
             }
+
+            IMedia? parent = parentId > 0 ? GetById(parentId) : null; // + locks
+            if (parentId > 0 && parent == null)
+            {
+                throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback
+            }
+
+            Models.Media media = parentId > 0 ? new Core.Models.Media(name, parent, mediaType) : new Core.Models.Media(name, parentId, mediaType);
+            CreateMedia(scope, media, parent, userId, true);
+
+            scope.Complete();
+            return media;
         }
 
         /// 
@@ -267,25 +283,28 @@ namespace Umbraco.Cms.Core.Services
         /// The alias of the media type.
         /// The optional id of the user creating the media.
         /// The media object.
-        public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
         {
-            if (parent == null) throw new ArgumentNullException(nameof(parent));
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+            if (parent == null)
             {
-                // locking the media tree secures media types too
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
-
-                var mediaType = GetMediaType(mediaTypeAlias); // + locks
-                if (mediaType == null)
-                    throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
-
-                var media = new Core.Models.Media(name, parent, mediaType);
-                CreateMedia(scope, media, parent, userId, true);
-
-                scope.Complete();
-                return media;
+                throw new ArgumentNullException(nameof(parent));
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            // locking the media tree secures media types too
+            scope.WriteLock(Constants.Locks.MediaTree);
+
+            IMediaType? mediaType = GetMediaType(mediaTypeAlias); // + locks
+            if (mediaType == null)
+            {
+                throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback
+            }
+
+            var media = new Core.Models.Media(name, parent, mediaType);
+            CreateMedia(scope, media, parent, userId, true);
+
+            scope.Complete();
+            return media;
         }
 
         private void CreateMedia(ICoreScope scope, Core.Models.Media media, IMedia? parent, int userId, bool withIdentity)
@@ -309,7 +328,9 @@ namespace Umbraco.Cms.Core.Services
             }
 
             if (withIdentity == false)
+            {
                 return;
+            }
 
             Audit(AuditType.New, media.CreatorId, media.Id, $"Media '{media.Name}' was created with Id {media.Id}");
         }
@@ -325,11 +346,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMedia? GetById(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.Get(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.Get(id);
         }
 
         /// 
@@ -340,13 +359,14 @@ namespace Umbraco.Cms.Core.Services
         public IEnumerable GetByIds(IEnumerable ids)
         {
             var idsA = ids.ToArray();
-            if (idsA.Length == 0) return Enumerable.Empty();
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            if (idsA.Length == 0)
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetMany(idsA);
+                return Enumerable.Empty();
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetMany(idsA);
         }
 
         /// 
@@ -356,11 +376,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMedia? GetById(Guid key)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.Get(key);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.Get(key);
         }
 
         /// 
@@ -370,50 +388,62 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetByIds(IEnumerable ids)
         {
-            var idsA = ids.ToArray();
-            if (idsA.Length == 0) return Enumerable.Empty();
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            Guid[] idsA = ids.ToArray();
+            if (idsA.Length == 0)
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetMany(idsA);
+                return Enumerable.Empty();
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetMany(idsA);
         }
 
         /// 
         public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null)
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+            if (pageIndex < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageIndex));
+            }
+
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
 
             if (ordering == null)
-                ordering = Ordering.By("sortOrder");
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.ContentTree);
-                return _mediaRepository.GetPage(
-                    Query()?.Where(x => x.ContentTypeId == contentTypeId),
-                    pageIndex, pageSize, out totalRecords, filter, ordering);
+                ordering = Ordering.By("sortOrder");
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _mediaRepository.GetPage(Query()?.Where(x => x.ContentTypeId == contentTypeId), pageIndex, pageSize, out totalRecords, filter, ordering);
         }
 
         /// 
         public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null)
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+            if (pageIndex < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageIndex));
+            }
+
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
 
             if (ordering == null)
-                ordering = Ordering.By("sortOrder");
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.ContentTree);
-                return _mediaRepository.GetPage(
-                    Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)),
-                    pageIndex, pageSize, out totalRecords, filter, ordering);
+                ordering = Ordering.By("sortOrder");
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.ContentTree);
+            return _mediaRepository.GetPage(
+                Query()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)), pageIndex, pageSize, out totalRecords, filter, ordering);
         }
 
         /// 
@@ -424,12 +454,10 @@ namespace Umbraco.Cms.Core.Services
         /// Contrary to most methods, this method filters out trashed media items.
         public IEnumerable? GetByLevel(int level)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                var query = Query().Where(x => x.Level == level && x.Trashed == false);
-                return _mediaRepository.Get(query);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            IQuery query = Query().Where(x => x.Level == level && x.Trashed == false);
+            return _mediaRepository.Get(query);
         }
 
         /// 
@@ -439,11 +467,9 @@ namespace Umbraco.Cms.Core.Services
         /// An  item
         public IMedia? GetVersion(int versionId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetVersion(versionId);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetVersion(versionId);
         }
 
         /// 
@@ -453,11 +479,9 @@ namespace Umbraco.Cms.Core.Services
         /// An Enumerable list of  objects
         public IEnumerable GetVersions(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetAllVersions(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetAllVersions(id);
         }
 
         /// 
@@ -468,7 +492,7 @@ namespace Umbraco.Cms.Core.Services
         public IEnumerable GetAncestors(int id)
         {
             // intentionally not locking
-            var media = GetById(id);
+            IMedia? media = GetById(id);
             return GetAncestors(media);
         }
 
@@ -480,82 +504,105 @@ namespace Umbraco.Cms.Core.Services
         public IEnumerable GetAncestors(IMedia? media)
         {
             //null check otherwise we get exceptions
-            if (media is null || media.Path.IsNullOrWhiteSpace()) return Enumerable.Empty();
+            if (media is null || media.Path.IsNullOrWhiteSpace())
+            {
+                return Enumerable.Empty();
+            }
 
-            var rootId = Cms.Core.Constants.System.RootString;
+            var rootId = Constants.System.RootString;
             var ids = media.Path.Split(Constants.CharArrays.Comma)
                 .Where(x => x != rootId && x != media.Id.ToString(CultureInfo.InvariantCulture))
                 .Select(s => int.Parse(s, CultureInfo.InvariantCulture))
                 .ToArray();
             if (ids.Any() == false)
+            {
                 return new List();
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                return _mediaRepository.GetMany(ids);
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.GetMany(ids);
         }
 
         /// 
-        public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
+        public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
         {
-            if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
-            if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+            if (pageIndex < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageIndex));
+            }
+
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
 
             if (ordering == null)
+            {
                 ordering = Ordering.By("sortOrder");
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-
-                var query = Query()?.Where(x => x.ParentId == id);
-                return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+
+            IQuery? query = Query()?.Where(x => x.ParentId == id);
+            return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
         }
 
         /// 
-        public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren,
-            IQuery? filter = null, Ordering? ordering = null)
+        public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
         {
             if (ordering == null)
-                ordering = Ordering.By("Path");
-
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-
-                //if the id is System Root, then just get all
-                if (id != Cms.Core.Constants.System.Root)
-                {
-                    var mediaPath = _entityRepository.GetAllPaths(Cms.Core.Constants.ObjectTypes.Media, id).ToArray();
-                    if (mediaPath.Length == 0)
-                    {
-                        totalChildren = 0;
-                        return Enumerable.Empty();
-                    }
-                    return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
-                }
-                return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering);
+                ordering = Ordering.By("Path");
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+
+            //if the id is System Root, then just get all
+            if (id != Constants.System.Root)
+            {
+                TreeEntityPath[] mediaPath = _entityRepository.GetAllPaths(Constants.ObjectTypes.Media, id).ToArray();
+                if (mediaPath.Length == 0)
+                {
+                    totalChildren = 0;
+                    return Enumerable.Empty();
+                }
+
+                return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
+            }
+
+            return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering);
         }
 
         private IQuery? GetPagedDescendantQuery(string? mediaPath)
         {
-            var query = Query();
+            IQuery? 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)
+        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));
+            if (pageIndex < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageIndex));
+            }
+
+            if (pageSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(pageSize));
+            }
+
+            if (ordering == null)
+            {
+                throw new ArgumentNullException(nameof(ordering));
+            }
 
             return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
         }
@@ -568,7 +615,7 @@ namespace Umbraco.Cms.Core.Services
         public IMedia? GetParent(int id)
         {
             // intentionally not locking
-            var media = GetById(id);
+            IMedia? media = GetById(id);
             return GetParent(media);
         }
 
@@ -580,8 +627,10 @@ namespace Umbraco.Cms.Core.Services
         public IMedia? GetParent(IMedia? media)
         {
             var parentId = media?.ParentId;
-            if (parentId is null || media?.ParentId == Cms.Core.Constants.System.Root || media?.ParentId == Cms.Core.Constants.System.RecycleBinMedia)
+            if (parentId is null || media?.ParentId == Constants.System.Root || media?.ParentId == Constants.System.RecycleBinMedia)
+            {
                 return null;
+            }
 
             return GetById(parentId.Value);
         }
@@ -592,27 +641,24 @@ namespace Umbraco.Cms.Core.Services
         /// An Enumerable list of  objects
         public IEnumerable GetRootMedia()
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                var query = Query().Where(x => x.ParentId == Cms.Core.Constants.System.Root);
-                return _mediaRepository.Get(query);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            IQuery query = Query().Where(x => x.ParentId == Constants.System.Root);
+            return _mediaRepository.Get(query);
         }
 
         /// 
-        public IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
-            IQuery? filter = null, Ordering? ordering = null)
+        public IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords, IQuery? filter = null, Ordering? ordering = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            if (ordering == null)
             {
-                if (ordering == null)
-                    ordering = Ordering.By("Path");
-
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTree);
-                var query = Query()?.Where(x => x.Path.StartsWith(Cms.Core.Constants.System.RecycleBinMediaPathPrefix));
-                return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
+                ordering = Ordering.By("Path");
             }
+
+            scope.ReadLock(Constants.Locks.MediaTree);
+            IQuery? query = Query()?.Where(x => x.Path.StartsWith(Constants.System.RecycleBinMediaPathPrefix));
+            return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
         }
 
         /// 
@@ -622,12 +668,10 @@ namespace Umbraco.Cms.Core.Services
         /// True if the media has any children otherwise False
         public bool HasChildren(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                var query = Query().Where(x => x.ParentId == id);
-                var count = _mediaRepository.Count(query);
-                return count > 0;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery query = Query().Where(x => x.ParentId == id);
+            var count = _mediaRepository.Count(query);
+            return count > 0;
         }
 
         /// 
@@ -654,7 +698,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// The  to save
         /// Id of the User saving the Media
-        public Attempt Save(IMedia media, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId)
         {
             EventMessages eventMessages = EventMessagesFactory.Get();
 
@@ -675,10 +719,10 @@ namespace Umbraco.Cms.Core.Services
 
                 if (media.Name != null && media.Name.Length > 255)
                 {
-                    throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
+                    throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
                 }
 
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
                 if (media.HasIdentity == false)
                 {
                     media.CreatorId = userId;
@@ -701,7 +745,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// Collection of  to save
         /// Id of the User saving the Media
-        public Attempt Save(IEnumerable medias, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId)
         {
             EventMessages messages = EventMessagesFactory.Get();
             IMedia[] mediasA = medias.ToArray();
@@ -717,7 +761,7 @@ namespace Umbraco.Cms.Core.Services
 
                 IEnumerable> treeChanges = mediasA.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode));
 
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
                 foreach (IMedia media in mediasA)
                 {
                     if (media.HasIdentity == false)
@@ -731,7 +775,7 @@ namespace Umbraco.Cms.Core.Services
                 scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification));
                 // TODO: See note about suppressing events in content service
                 scope.Notifications.Publish(new MediaTreeChangeNotification(treeChanges, messages));
-                Audit(AuditType.Save, userId == -1 ? 0 : userId, Cms.Core.Constants.System.Root, "Bulk save media");
+                Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Bulk save media");
 
                 scope.Complete();
             }
@@ -748,7 +792,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// The  to delete
         /// Id of the User deleting the Media
-        public Attempt Delete(IMedia media, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt Delete(IMedia media, int userId = Constants.Security.SuperUserId)
         {
             EventMessages messages = EventMessagesFactory.Get();
 
@@ -760,7 +804,7 @@ namespace Umbraco.Cms.Core.Services
                     return OperationResult.Attempt.Cancel(messages);
                 }
 
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 DeleteLocked(scope, media, messages);
 
@@ -789,10 +833,13 @@ namespace Umbraco.Cms.Core.Services
             while (page * pageSize < total)
             {
                 //get descendants - ordered from deepest to shallowest
-                var descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
-                foreach (var c in descendants)
+                IEnumerable descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
+                foreach (IMedia c in descendants)
+                {
                     DoDelete(c);
+                }
             }
+
             DoDelete(media);
         }
 
@@ -808,18 +855,16 @@ namespace Umbraco.Cms.Core.Services
         /// Id of the  object to delete versions from
         /// Latest version date
         /// Optional Id of the User deleting versions of a Media object
-        public void DeleteVersions(int id, DateTime versionDate, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                DeleteVersions(scope, true, id, versionDate, userId);
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            DeleteVersions(scope, true, id, versionDate, userId);
+            scope.Complete();
         }
 
-        private void DeleteVersions(ICoreScope scope, bool wlock, int id, DateTime versionDate, int userId = Cms.Core.Constants.Security.SuperUserId)
+        private void DeleteVersions(ICoreScope scope, bool wlock, int id, DateTime versionDate, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
             var deletingVersionsNotification = new MediaDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
             if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
@@ -828,11 +873,14 @@ namespace Umbraco.Cms.Core.Services
             }
 
             if (wlock)
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+            {
+                scope.WriteLock(Constants.Locks.MediaTree);
+            }
+
             _mediaRepository.DeleteVersions(id, versionDate);
 
             scope.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(deletingVersionsNotification));
-            Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete Media by version date");
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version date");
         }
 
         /// 
@@ -843,39 +891,37 @@ namespace Umbraco.Cms.Core.Services
         /// Id of the version to delete
         /// Boolean indicating whether to delete versions prior to the versionId
         /// Optional Id of the User deleting versions of a Media object
-        public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Constants.Security.SuperUserId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var deletingVersionsNotification = new MediaDeletingVersionsNotification(id, evtMsgs, specificVersion: versionId);
+            if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
             {
-                var deletingVersionsNotification = new MediaDeletingVersionsNotification(id, evtMsgs, specificVersion: versionId);
-                if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (deletePriorVersions)
-                {
-                    var media = GetVersion(versionId);
-                    if (media is not null)
-                    {
-                        DeleteVersions(scope, true, id, media.UpdateDate, userId);
-                    }
-                }
-                else
-                {
-                    scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
-                }
-
-                _mediaRepository.DeleteVersion(versionId);
-
-                scope.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, specificVersion: versionId).WithStateFrom(deletingVersionsNotification));
-                Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete Media by version");
-
                 scope.Complete();
+                return;
             }
+
+            if (deletePriorVersions)
+            {
+                IMedia? media = GetVersion(versionId);
+                if (media is not null)
+                {
+                    DeleteVersions(scope, true, id, media.UpdateDate, userId);
+                }
+            }
+            else
+            {
+                scope.WriteLock(Constants.Locks.MediaTree);
+            }
+
+            _mediaRepository.DeleteVersion(versionId);
+
+            scope.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, specificVersion: versionId).WithStateFrom(deletingVersionsNotification));
+            Audit(AuditType.Delete, userId, Constants.System.Root, "Delete Media by version");
+
+            scope.Complete();
         }
 
         #endregion
@@ -887,21 +933,21 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// The  to delete
         /// Id of the User deleting the Media
-        public Attempt MoveToRecycleBin(IMedia media, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId)
         {
             EventMessages messages = EventMessagesFactory.Get();
             var moves = new List<(IMedia, string)>();
 
             using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 // TODO: missing 7.6 "ensure valid path" thing here?
                 // but then should be in PerformMoveLocked on every moved item?
 
                 var originalPath = media.Path;
 
-                var moveEventInfo = new MoveEventInfo(media, originalPath, Cms.Core.Constants.System.RecycleBinMedia);
+                var moveEventInfo = new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia);
 
                 var movingToRecycleBinNotification = new MediaMovingToRecycleBinNotification(moveEventInfo, messages);
                 if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification))
@@ -910,7 +956,7 @@ namespace Umbraco.Cms.Core.Services
                     return OperationResult.Attempt.Cancel(messages);
                 }
 
-                PerformMoveLocked(media, Cms.Core.Constants.System.RecycleBinMedia, null, userId, moves, true);
+                PerformMoveLocked(media, Constants.System.RecycleBinMedia, null, userId, moves, true);
 
                 scope.Notifications.Publish(new MediaTreeChangeNotification(media, TreeChangeTypes.RefreshBranch, messages));
                 MoveEventInfo[] moveInfo = moves.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)).ToArray();
@@ -929,12 +975,12 @@ namespace Umbraco.Cms.Core.Services
         /// The  to move
         /// Id of the Media's new Parent
         /// Id of the User moving the Media
-        public Attempt Move(IMedia media, int parentId, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public Attempt Move(IMedia media, int parentId, int userId = Constants.Security.SuperUserId)
         {
             EventMessages messages = EventMessagesFactory.Get();
 
             // if moving to the recycle bin then use the proper method
-            if (parentId == Cms.Core.Constants.System.RecycleBinMedia)
+            if (parentId == Constants.System.RecycleBinMedia)
             {
                 MoveToRecycleBin(media, userId);
                 return OperationResult.Attempt.Succeed(messages);
@@ -944,10 +990,10 @@ namespace Umbraco.Cms.Core.Services
 
             using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
-                IMedia? parent = parentId == Cms.Core.Constants.System.Root ? null : GetById(parentId);
-                if (parentId != Cms.Core.Constants.System.Root && (parent == null || parent.Trashed))
+                IMedia? parent = parentId == Constants.System.Root ? null : GetById(parentId);
+                if (parentId != Constants.System.Root && (parent == null || parent.Trashed))
                 {
                     throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
                 }
@@ -999,38 +1045,43 @@ namespace Umbraco.Cms.Core.Services
             //media.Path = (parent == null ? "-1" : parent.Path) + "," + media.Id;
             //media.SortOrder = ((MediaRepository) repository).NextChildSortOrder(parentId);
             //media.Level += levelDelta;
-            PerformMoveMediaLocked(media, userId, trash);
+            PerformMoveMediaLocked(media, trash);
 
             // if uow is not immediate, content.Path will be updated only when the UOW commits,
             // and because we want it now, we have to calculate it by ourselves
             //paths[media.Id] = media.Path;
-            paths[media.Id] = (parent == null ? (parentId == Cms.Core.Constants.System.RecycleBinMedia ? "-1,-21" : Cms.Core.Constants.System.RootString) : parent.Path) + "," + media.Id;
+            paths[media.Id] = (parent == null ? parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString : parent.Path) + "," + media.Id;
 
             const int pageSize = 500;
-            var query = GetPagedDescendantQuery(originalPath);
+            IQuery? query = GetPagedDescendantQuery(originalPath);
             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));
+                IEnumerable descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path"));
 
-                foreach (var descendant in descendants)
+                foreach (IMedia descendant in descendants)
                 {
                     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);
+                    PerformMoveMediaLocked(descendant, trash);
                 }
 
-            } while (total > pageSize);
+            }
+            while (total > pageSize);
 
         }
 
-        private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash)
+        private void PerformMoveMediaLocked(IMedia media, bool? trash)
         {
-            if (trash.HasValue) ((ContentBase)media).Trashed = trash.Value;
+            if (trash.HasValue)
+            {
+                ((ContentBase)media).Trashed = trash.Value;
+            }
+
             _mediaRepository.Save(media);
         }
 
@@ -1038,17 +1089,17 @@ namespace Umbraco.Cms.Core.Services
         /// Empties the Recycle Bin by deleting all  that resides in the bin
         /// 
         /// Optional Id of the User emptying the Recycle Bin
-        public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security.SuperUserId)
+        public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId)
         {
             var deleted = new List();
             EventMessages messages = EventMessagesFactory.Get(); // TODO: and then?
 
             using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 // emptying the recycle bin means deleting whatever is in there - do it properly!
-                IQuery? query = Query().Where(x => x.ParentId == Cms.Core.Constants.System.RecycleBinMedia);
+                IQuery? query = Query().Where(x => x.ParentId == Constants.System.RecycleBinMedia);
                 IMedia[] medias = _mediaRepository.Get(query)?.ToArray() ?? Array.Empty();
 
                 var emptyingRecycleBinNotification = new MediaEmptyingRecycleBinNotification(medias, messages);
@@ -1065,7 +1116,7 @@ namespace Umbraco.Cms.Core.Services
                 }
                 scope.Notifications.Publish(new MediaEmptiedRecycleBinNotification(deleted, new EventMessages()).WithStateFrom(emptyingRecycleBinNotification));
                 scope.Notifications.Publish(new MediaTreeChangeNotification(deleted, TreeChangeTypes.Remove, messages));
-                Audit(AuditType.Delete, userId, Cms.Core.Constants.System.RecycleBinMedia, "Empty Media recycle bin");
+                Audit(AuditType.Delete, userId, Constants.System.RecycleBinMedia, "Empty Media recycle bin");
                 scope.Complete();
             }
 
@@ -1074,11 +1125,9 @@ namespace Umbraco.Cms.Core.Services
 
         public bool RecycleBinSmells()
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MediaTree);
-                return _mediaRepository.RecycleBinSmells();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MediaTree);
+            return _mediaRepository.RecycleBinSmells();
         }
 
         #endregion
@@ -1092,7 +1141,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// 
         /// True if sorting succeeded, otherwise False
-        public bool Sort(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId)
         {
             IMedia[] itemsA = items.ToArray();
             if (itemsA.Length == 0)
@@ -1113,7 +1162,7 @@ namespace Umbraco.Cms.Core.Services
 
                 var saved = new List();
 
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
                 var sortOrder = 0;
 
                 foreach (IMedia media in itemsA)
@@ -1148,7 +1197,7 @@ namespace Umbraco.Cms.Core.Services
         {
             using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 ContentDataIntegrityReport report = _mediaRepository.CheckDataIntegrity(options);
 
@@ -1222,7 +1271,7 @@ namespace Umbraco.Cms.Core.Services
         /// 
         /// Id of the 
         /// Optional id of the user deleting the media
-        public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = Constants.Security.SuperUserId)
         {
             // TODO: This currently this is called from the ContentTypeService but that needs to change,
             // if we are deleting a content type, we should just delete the data and do this operation slightly differently.
@@ -1238,7 +1287,7 @@ namespace Umbraco.Cms.Core.Services
 
             using (ICoreScope scope = ScopeProvider.CreateCoreScope())
             {
-                scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
+                scope.WriteLock(Constants.Locks.MediaTree);
 
                 IQuery? query = Query().WhereIn(x => x.ContentTypeId, mediaTypeIdsA);
                 IMedia[] medias = _mediaRepository.Get(query)?.ToArray() ?? Array.Empty();
@@ -1262,7 +1311,7 @@ namespace Umbraco.Cms.Core.Services
                         foreach (IMedia child in children.Where(x => mediaTypeIdsA.Contains(x.ContentTypeId) == false))
                         {
                             // see MoveToRecycleBin
-                            PerformMoveLocked(child, Cms.Core.Constants.System.RecycleBinMedia, null, userId, moves, true);
+                            PerformMoveLocked(child, Constants.System.RecycleBinMedia, null, userId, moves, true);
                             changes.Add(new TreeChange(media, TreeChangeTypes.RefreshBranch));
                         }
                     }
@@ -1281,7 +1330,7 @@ namespace Umbraco.Cms.Core.Services
                 }
                 scope.Notifications.Publish(new MediaTreeChangeNotification(changes, messages));
 
-                Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, $"Delete Media of types {string.Join(",", mediaTypeIdsA)}");
+                Audit(AuditType.Delete, userId, Constants.System.Root, $"Delete Media of types {string.Join(",", mediaTypeIdsA)}");
 
                 scope.Complete();
             }
@@ -1293,29 +1342,36 @@ namespace Umbraco.Cms.Core.Services
         /// This needs extra care and attention as its potentially a dangerous and extensive operation
         /// Id of the 
         /// Optional id of the user deleting the media
-        public void DeleteMediaOfType(int mediaTypeId, int userId = Cms.Core.Constants.Security.SuperUserId)
+        public void DeleteMediaOfType(int mediaTypeId, int userId = Constants.Security.SuperUserId)
         {
             DeleteMediaOfTypes(new[] { mediaTypeId }, userId);
         }
 
         private IMediaType GetMediaType(string mediaTypeAlias)
         {
-            if (mediaTypeAlias == null) throw new ArgumentNullException(nameof(mediaTypeAlias));
-            if (string.IsNullOrWhiteSpace(mediaTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(mediaTypeAlias));
-
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            if (mediaTypeAlias == null)
             {
-                scope.ReadLock(Cms.Core.Constants.Locks.MediaTypes);
-
-                var query = Query().Where(x => x.Alias == mediaTypeAlias);
-                var mediaType = _mediaTypeRepository.Get(query)?.FirstOrDefault();
-
-                if (mediaType == null)
-                    throw new InvalidOperationException($"No media type matched the specified alias '{mediaTypeAlias}'.");
-
-                scope.Complete();
-                return mediaType;
+                throw new ArgumentNullException(nameof(mediaTypeAlias));
             }
+
+            if (string.IsNullOrWhiteSpace(mediaTypeAlias))
+            {
+                throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(mediaTypeAlias));
+            }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.ReadLock(Constants.Locks.MediaTypes);
+
+            IQuery query = Query().Where(x => x.Alias == mediaTypeAlias);
+            IMediaType? mediaType = _mediaTypeRepository.Get(query)?.FirstOrDefault();
+
+            if (mediaType == null)
+            {
+                throw new InvalidOperationException($"No media type matched the specified alias '{mediaTypeAlias}'.");
+            }
+
+            scope.Complete();
+            return mediaType;
         }
 
         #endregion
diff --git a/src/Umbraco.Core/Services/MediaServiceExtensions.cs b/src/Umbraco.Core/Services/MediaServiceExtensions.cs
index 1cf648c35d..8d45367e61 100644
--- a/src/Umbraco.Core/Services/MediaServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/MediaServiceExtensions.cs
@@ -1,45 +1,48 @@
 // Copyright (c) Umbraco.
 // See LICENSE for more details.
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Extensions
+namespace Umbraco.Extensions;
+
+/// 
+///     Media service extension methods
+/// 
+/// 
+///     Many of these have to do with UDI lookups but we don't need to add these methods to the service interface since a
+///     UDI is just a GUID
+///     and the services already support GUIDs
+/// 
+public static class MediaServiceExtensions
 {
-    /// 
-    /// Media service extension methods
-    /// 
-    /// 
-    /// Many of these have to do with UDI lookups but we don't need to add these methods to the service interface since a UDI is just a GUID
-    /// and the services already support GUIDs
-    /// 
-    public static class MediaServiceExtensions
+    public static IEnumerable GetByIds(this IMediaService mediaService, IEnumerable ids)
     {
-        public static IEnumerable GetByIds(this IMediaService mediaService, IEnumerable ids)
+        var guids = new List();
+        foreach (Udi udi in ids)
         {
-            var guids = new List();
-            foreach (var udi in ids)
+            if (udi is not GuidUdi guidUdi)
             {
-                var guidUdi = udi as GuidUdi;
-                if (guidUdi is null)
-                    throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by media");
-                guids.Add(guidUdi);
+                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                    " which is required by media");
             }
 
-            return mediaService.GetByIds(guids.Select(x => x.Guid));
+            guids.Add(guidUdi);
         }
 
-        public static IMedia CreateMedia(this IMediaService mediaService, string name, Udi parentId, string mediaTypeAlias, int userId = 0)
+        return mediaService.GetByIds(guids.Select(x => x.Guid));
+    }
+
+    public static IMedia CreateMedia(this IMediaService mediaService, string name, Udi parentId, string mediaTypeAlias, int userId = 0)
+    {
+        if (parentId is not GuidUdi guidUdi)
         {
-            var guidUdi = parentId as GuidUdi;
-            if (guidUdi is null)
-                throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by media");
-            var parent = mediaService.GetById(guidUdi.Guid);
-            return mediaService.CreateMedia(name, parent, mediaTypeAlias, userId);
+            throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) +
+                                                " which is required by media");
         }
+
+        IMedia? parent = mediaService.GetById(guidUdi.Guid);
+        return mediaService.CreateMedia(name, parent, mediaTypeAlias, userId);
     }
 }
diff --git a/src/Umbraco.Core/Services/MediaTypeService.cs b/src/Umbraco.Core/Services/MediaTypeService.cs
index 6873fb4a39..eff6ba0fba 100644
--- a/src/Umbraco.Core/Services/MediaTypeService.cs
+++ b/src/Umbraco.Core/Services/MediaTypeService.cs
@@ -1,5 +1,3 @@
-using System;
-using System.Collections.Generic;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -8,70 +6,84 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services.Changes;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class MediaTypeService : ContentTypeServiceBase, IMediaTypeService
 {
-    public class MediaTypeService : ContentTypeServiceBase, IMediaTypeService
+    public MediaTypeService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMediaService mediaService,
+        IMediaTypeRepository mediaTypeRepository,
+        IAuditRepository auditRepository,
+        IMediaTypeContainerRepository entityContainerRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : base(provider, loggerFactory, eventMessagesFactory, mediaTypeRepository, auditRepository, entityContainerRepository, entityRepository, eventAggregator) => MediaService = mediaService;
+
+    // beware! order is important to avoid deadlocks
+    protected override int[] ReadLockIds { get; } = { Constants.Locks.MediaTypes };
+
+    protected override int[] WriteLockIds { get; } = { Constants.Locks.MediaTree, Constants.Locks.MediaTypes };
+
+    protected override Guid ContainedObjectType => Constants.ObjectTypes.MediaType;
+
+    private IMediaService MediaService { get; }
+
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
     {
-        public MediaTypeService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMediaService mediaService,
-            IMediaTypeRepository mediaTypeRepository, IAuditRepository auditRepository, IMediaTypeContainerRepository entityContainerRepository,
-            IEntityRepository entityRepository, IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory, mediaTypeRepository, auditRepository, entityContainerRepository, entityRepository, eventAggregator)
+        foreach (var typeId in typeIds)
         {
-            MediaService = mediaService;
-        }
-
-        // beware! order is important to avoid deadlocks
-        protected override int[] ReadLockIds { get; } = { Cms.Core.Constants.Locks.MediaTypes };
-        protected override int[] WriteLockIds { get; } = { Cms.Core.Constants.Locks.MediaTree, Cms.Core.Constants.Locks.MediaTypes };
-
-        private IMediaService MediaService { get; }
-
-        protected override Guid ContainedObjectType => Cms.Core.Constants.ObjectTypes.MediaType;
-
-        #region Notifications
-
-        protected override SavingNotification GetSavingNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeSavingNotification(item, eventMessages);
-
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeSavingNotification(items, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeSavedNotification(item, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeSavedNotification(items, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IMediaType item,
-            EventMessages eventMessages) => new MediaTypeDeletingNotification(item, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeDeletingNotification(items, eventMessages);
-
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MediaTypeDeletedNotification(items, eventMessages);
-
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new MediaTypeMovingNotification(moveInfo, eventMessages);
-
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new MediaTypeMovedNotification(moveInfo, eventMessages);
-
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MediaTypeChangedNotification(changes, eventMessages);
-
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MediaTypeRefreshedNotification(changes, eventMessages);
-
-        #endregion
-
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            foreach (var typeId in typeIds)
-                MediaService.DeleteMediaOfType(typeId);
+            MediaService.DeleteMediaOfType(typeId);
         }
     }
+
+    #region Notifications
+
+    protected override SavingNotification GetSavingNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeSavingNotification(item, eventMessages);
+
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeSavingNotification(items, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeSavedNotification(item, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeSavedNotification(items, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IMediaType item,
+        EventMessages eventMessages) => new MediaTypeDeletingNotification(item, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeDeletingNotification(items, eventMessages);
+
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MediaTypeDeletedNotification(items, eventMessages);
+
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new MediaTypeMovingNotification(moveInfo, eventMessages);
+
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new MediaTypeMovedNotification(moveInfo, eventMessages);
+
+    protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MediaTypeChangedNotification(changes, eventMessages);
+
+    protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MediaTypeRefreshedNotification(changes, eventMessages);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/MemberGroupService.cs b/src/Umbraco.Core/Services/MemberGroupService.cs
index 2290f9d84a..5a68236455 100644
--- a/src/Umbraco.Core/Services/MemberGroupService.cs
+++ b/src/Umbraco.Core/Services/MemberGroupService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -8,105 +5,105 @@ using Umbraco.Cms.Core.Notifications;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+internal class MemberGroupService : RepositoryService, IMemberGroupService
 {
-    internal class MemberGroupService : RepositoryService, IMemberGroupService
+    private readonly IMemberGroupRepository _memberGroupRepository;
+
+    public MemberGroupService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberGroupRepository memberGroupRepository)
+        : base(provider, loggerFactory, eventMessagesFactory) =>
+        _memberGroupRepository = memberGroupRepository;
+
+    public IEnumerable GetAll()
     {
-        private readonly IMemberGroupRepository _memberGroupRepository;
-
-        public MemberGroupService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
-            IMemberGroupRepository memberGroupRepository)
-            : base(provider, loggerFactory, eventMessagesFactory) =>
-            _memberGroupRepository = memberGroupRepository;
-
-        public IEnumerable GetAll()
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.GetMany();
-            }
+            return _memberGroupRepository.GetMany();
+        }
+    }
+
+    public IEnumerable GetByIds(IEnumerable ids)
+    {
+        if (ids == null || ids.Any() == false)
+        {
+            return new IMemberGroup[0];
         }
 
-        public IEnumerable GetByIds(IEnumerable ids)
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-            if (ids == null || ids.Any() == false)
-            {
-                return new IMemberGroup[0];
-            }
+            return _memberGroupRepository.GetMany(ids.ToArray());
+        }
+    }
 
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.GetMany(ids.ToArray());
-            }
+    public IMemberGroup? GetById(int id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _memberGroupRepository.Get(id);
+        }
+    }
+
+    public IMemberGroup? GetById(Guid id)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _memberGroupRepository.Get(id);
+        }
+    }
+
+    public IMemberGroup? GetByName(string? name)
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+        {
+            return _memberGroupRepository.GetByName(name);
+        }
+    }
+
+    public void Save(IMemberGroup memberGroup)
+    {
+        if (string.IsNullOrWhiteSpace(memberGroup.Name))
+        {
+            throw new InvalidOperationException("The name of a MemberGroup can not be empty");
         }
 
-        public IMemberGroup? GetById(int id)
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            var savingNotification = new MemberGroupSavingNotification(memberGroup, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                return _memberGroupRepository.Get(id);
-            }
-        }
-
-        public IMemberGroup? GetById(Guid id)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.Get(id);
-            }
-        }
-
-        public IMemberGroup? GetByName(string? name)
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _memberGroupRepository.GetByName(name);
-            }
-        }
-
-        public void Save(IMemberGroup memberGroup)
-        {
-            if (string.IsNullOrWhiteSpace(memberGroup.Name))
-            {
-                throw new InvalidOperationException("The name of a MemberGroup can not be empty");
-            }
-
-            var evtMsgs = EventMessagesFactory.Get();
-
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                var savingNotification = new MemberGroupSavingNotification(memberGroup, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _memberGroupRepository.Save(memberGroup);
                 scope.Complete();
-
-                scope.Notifications.Publish(new MemberGroupSavedNotification(memberGroup, evtMsgs).WithStateFrom(savingNotification));
+                return;
             }
+
+            _memberGroupRepository.Save(memberGroup);
+            scope.Complete();
+
+            scope.Notifications.Publish(
+                new MemberGroupSavedNotification(memberGroup, evtMsgs).WithStateFrom(savingNotification));
         }
+    }
 
-        public void Delete(IMemberGroup memberGroup)
+    public void Delete(IMemberGroup memberGroup)
+    {
+        EventMessages evtMsgs = EventMessagesFactory.Get();
+
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope())
         {
-            var evtMsgs = EventMessagesFactory.Get();
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+            var deletingNotification = new MemberGroupDeletingNotification(memberGroup, evtMsgs);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                var deletingNotification = new MemberGroupDeletingNotification(memberGroup, evtMsgs);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                _memberGroupRepository.Delete(memberGroup);
                 scope.Complete();
-
-                scope.Notifications.Publish(new MemberGroupDeletedNotification(memberGroup, evtMsgs).WithStateFrom(deletingNotification));
+                return;
             }
+
+            _memberGroupRepository.Delete(memberGroup);
+            scope.Complete();
+
+            scope.Notifications.Publish(
+                new MemberGroupDeletedNotification(memberGroup, evtMsgs).WithStateFrom(deletingNotification));
         }
     }
 }
diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs
index 2a4498f7e4..76d730dc78 100644
--- a/src/Umbraco.Core/Services/MemberService.cs
+++ b/src/Umbraco.Core/Services/MemberService.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Events;
 using Umbraco.Cms.Core.Models;
@@ -28,8 +25,15 @@ namespace Umbraco.Cms.Core.Services
 
         #region Constructor
 
-        public MemberService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService,
-            IMemberRepository memberRepository, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, IAuditRepository auditRepository)
+        public MemberService(
+            ICoreScopeProvider provider,
+            ILoggerFactory loggerFactory,
+            IEventMessagesFactory eventMessagesFactory,
+            IMemberGroupService memberGroupService,
+            IMemberRepository memberRepository,
+            IMemberTypeRepository memberTypeRepository,
+            IMemberGroupRepository memberGroupRepository,
+            IAuditRepository auditRepository)
             : base(provider, loggerFactory, eventMessagesFactory)
         {
             _memberRepository = memberRepository;
@@ -55,29 +59,27 @@ namespace Umbraco.Cms.Core.Services
         ///  with number of Members for passed in type
         public int GetCount(MemberCountType countType)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+
+            IQuery? query;
+
+            switch (countType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-
-                IQuery? query;
-
-                switch (countType)
-                {
-                    case MemberCountType.All:
-                        query = Query();
-                        break;
-                    case MemberCountType.LockedOut:
-                        query = Query()?.Where(x => x.IsLockedOut == true);
-                        break;
-                    case MemberCountType.Approved:
-                        query = Query()?.Where(x => x.IsApproved == true);
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(countType));
-                }
-
-                return _memberRepository.GetCountByQuery(query);
+                case MemberCountType.All:
+                    query = Query();
+                    break;
+                case MemberCountType.LockedOut:
+                    query = Query()?.Where(x => x.IsLockedOut == true);
+                    break;
+                case MemberCountType.Approved:
+                    query = Query()?.Where(x => x.IsApproved == true);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(countType));
             }
+
+            return _memberRepository.GetCountByQuery(query);
         }
 
         /// 
@@ -88,11 +90,9 @@ namespace Umbraco.Cms.Core.Services
         ///  with number of Members
         public int Count(string? memberTypeAlias = null)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.Count(memberTypeAlias);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.Count(memberTypeAlias);
         }
 
         #endregion
@@ -137,7 +137,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember CreateMember(string username, string email, string name, IMemberType memberType)
         {
-            if (memberType == null) throw new ArgumentNullException(nameof(memberType));
+            if (memberType == null)
+            {
+                throw new ArgumentNullException(nameof(memberType));
+            }
 
             var member = new Member(name, email.ToLower().Trim(), username, memberType, 0);
 
@@ -171,16 +174,16 @@ namespace Umbraco.Cms.Core.Services
             => CreateMemberWithIdentity(username, email, username, passwordValue, memberTypeAlias, isApproved);
 
         public IMember CreateMemberWithIdentity(string username, string email, string memberTypeAlias)
-            => CreateMemberWithIdentity(username, email, username, "", memberTypeAlias);
+            => CreateMemberWithIdentity(username, email, username, string.Empty, memberTypeAlias);
 
         public IMember CreateMemberWithIdentity(string username, string email, string memberTypeAlias, bool isApproved)
-            => CreateMemberWithIdentity(username, email, username, "", memberTypeAlias, isApproved);
+            => CreateMemberWithIdentity(username, email, string.Empty, string.Empty, memberTypeAlias, isApproved);
 
         public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias)
-            => CreateMemberWithIdentity(username, email, name, "", memberTypeAlias);
+            => CreateMemberWithIdentity(username, email, string.Empty, string.Empty, memberTypeAlias);
 
         public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias, bool isApproved)
-            => CreateMemberWithIdentity(username, email, name, "", memberTypeAlias, isApproved);
+            => CreateMemberWithIdentity(username, string.Empty, name, string.Empty, memberTypeAlias, isApproved);
 
         /// 
         /// Creates and persists a Member
@@ -217,7 +220,7 @@ namespace Umbraco.Cms.Core.Services
         }
 
         public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType)
-            => CreateMemberWithIdentity(username, email, username, "", memberType);
+            => CreateMemberWithIdentity(username, email, username, string.Empty, memberType);
 
         /// 
         /// Creates and persists a Member
@@ -229,10 +232,10 @@ namespace Umbraco.Cms.Core.Services
         /// MemberType the Member should be based on
         /// 
         public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType, bool isApproved)
-            => CreateMemberWithIdentity(username, email, username, "", memberType, isApproved);
+            => CreateMemberWithIdentity(username, email, username, string.Empty, memberType, isApproved);
 
         public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType)
-            => CreateMemberWithIdentity(username, email, name, "", memberType);
+            => CreateMemberWithIdentity(username, email, name, string.Empty, memberType);
 
         /// 
         /// Creates and persists a Member
@@ -245,7 +248,7 @@ namespace Umbraco.Cms.Core.Services
         /// MemberType the Member should be based on
         /// 
         public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType, bool isApproved)
-            => CreateMemberWithIdentity(username, email, name, "", memberType, isApproved);
+            => CreateMemberWithIdentity(username, email, name, string.Empty, memberType, isApproved);
 
         /// 
         /// Creates and persists a Member
@@ -260,29 +263,30 @@ namespace Umbraco.Cms.Core.Services
         /// 
         private IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, IMemberType memberType, bool isApproved = true)
         {
-            if (memberType == null) throw new ArgumentNullException(nameof(memberType));
-
-            using (var scope = ScopeProvider.CreateCoreScope())
+            if (memberType == null)
             {
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                // ensure it all still make sense
-                // ensure it all still make sense
-                var vrfy = GetMemberType(scope, memberType.Alias); // + locks
-
-                if (vrfy == null || vrfy.Id != memberType.Id)
-                {
-                    throw new ArgumentException($"Member type with alias {memberType.Alias} does not exist or is a different member type."); // causes rollback
-                }
-
-                var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved, -1);
-
-                Save(member);
-
-                scope.Complete();
-
-                return member;
+                throw new ArgumentNullException(nameof(memberType));
             }
+
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            // ensure it all still make sense
+            // ensure it all still make sense
+            IMemberType? vrfy = GetMemberType(scope, memberType.Alias); // + locks
+
+            if (vrfy == null || vrfy.Id != memberType.Id)
+            {
+                throw new ArgumentException($"Member type with alias {memberType.Alias} does not exist or is a different member type."); // causes rollback
+            }
+
+            var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved, -1);
+
+            Save(member);
+
+            scope.Complete();
+
+            return member;
         }
 
         #endregion
@@ -296,11 +300,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetById(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.Get(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.Get(id);
         }
 
         /// 
@@ -312,12 +314,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetByKey(Guid id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => x.Key == id);
-                return _memberRepository.Get(query)?.FirstOrDefault();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => x.Key == id);
+            return _memberRepository.Get(query)?.FirstOrDefault();
         }
 
         /// 
@@ -329,29 +329,36 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetPage(null, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetPage(null, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
         }
 
-        public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection, string? memberTypeAlias = null, string filter = "")
-        {
-            return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, true, memberTypeAlias, filter);
-        }
+        public IEnumerable GetAll(
+            long pageIndex,
+            int pageSize,
+            out long totalRecords,
+            string orderBy,
+            Direction orderDirection,
+            string? memberTypeAlias = null,
+            string filter = "") =>
+            GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, true, memberTypeAlias, filter);
 
-        public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords,
-            string orderBy, Direction orderDirection, bool orderBySystemField, string? memberTypeAlias, string filter)
+        public IEnumerable GetAll(
+            long pageIndex,
+            int pageSize,
+            out long totalRecords,
+            string orderBy,
+            Direction orderDirection,
+            bool orderBySystemField,
+            string? memberTypeAlias,
+            string filter)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query1 = memberTypeAlias == null ? null : Query()?.Where(x => x.ContentTypeAlias == memberTypeAlias);
-                var query2 = filter == null ? null : Query()?.Where(x => (x.Name != null && x.Name.Contains(filter)) || x.Username.Contains(filter) || x.Email.Contains(filter));
-                return _memberRepository.GetPage(query1, pageIndex, pageSize, out totalRecords, query2, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField));
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query1 = memberTypeAlias == null ? null : Query()?.Where(x => x.ContentTypeAlias == memberTypeAlias);
+            IQuery? query2 = filter == null ? null : Query()?.Where(x => (x.Name != null && x.Name.Contains(filter)) || x.Username.Contains(filter) || x.Email.Contains(filter));
+            return _memberRepository.GetPage(query1, pageIndex, pageSize, out totalRecords, query2, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField));
         }
 
         /// 
@@ -361,13 +368,17 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetByProviderKey(object id)
         {
-            var asGuid = id.TryConvertTo();
+            Attempt asGuid = id.TryConvertTo();
             if (asGuid.Success)
+            {
                 return GetByKey(asGuid.Result);
+            }
 
-            var asInt = id.TryConvertTo();
+            Attempt asInt = id.TryConvertTo();
             if (asInt.Success)
+            {
                 return GetById(asInt.Result);
+            }
 
             return null;
         }
@@ -379,12 +390,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetByEmail(string email)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => x.Email.Equals(email));
-                return _memberRepository.Get(query)?.FirstOrDefault();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => x.Email.Equals(email));
+            return _memberRepository.Get(query)?.FirstOrDefault();
         }
 
         /// 
@@ -394,11 +403,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IMember? GetByUsername(string? username)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetByUsername(username);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetByUsername(username);
         }
 
         /// 
@@ -408,12 +415,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetMembersByMemberType(string memberTypeAlias)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => x.ContentTypeAlias == memberTypeAlias);
-                return _memberRepository.Get(query);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => x.ContentTypeAlias == memberTypeAlias);
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -423,12 +428,10 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetMembersByMemberType(int memberTypeId)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => x.ContentTypeId == memberTypeId);
-                return _memberRepository.Get(query);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => x.ContentTypeId == memberTypeId);
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -438,11 +441,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetMembersByGroup(string memberGroupName)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetByMemberGroup(memberGroupName);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetByMemberGroup(memberGroupName);
         }
 
         /// 
@@ -453,11 +454,9 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable GetAllMembers(params int[] ids)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetMany(ids);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetMany(ids);
         }
 
         /// 
@@ -471,34 +470,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query = Query();
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query();
-
-                switch (matchType)
-                {
-                    case StringPropertyMatchType.Exact:
-                        query?.Where(member => string.Equals(member.Name, displayNameToMatch));
-                        break;
-                    case StringPropertyMatchType.Contains:
-                        query?.Where(member => member.Name != null && member.Name.Contains(displayNameToMatch));
-                        break;
-                    case StringPropertyMatchType.StartsWith:
-                        query?.Where(member => member.Name != null && member.Name.StartsWith(displayNameToMatch));
-                        break;
-                    case StringPropertyMatchType.EndsWith:
-                        query?.Where(member => member.Name != null && member.Name.EndsWith(displayNameToMatch));
-                        break;
-                    case StringPropertyMatchType.Wildcard:
-                        query?.Where(member => member.Name != null && member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar));
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback
-                }
-
-                return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Name"));
+                case StringPropertyMatchType.Exact:
+                    query?.Where(member => string.Equals(member.Name, displayNameToMatch));
+                    break;
+                case StringPropertyMatchType.Contains:
+                    query?.Where(member => member.Name != null && member.Name.Contains(displayNameToMatch));
+                    break;
+                case StringPropertyMatchType.StartsWith:
+                    query?.Where(member => member.Name != null && member.Name.StartsWith(displayNameToMatch));
+                    break;
+                case StringPropertyMatchType.EndsWith:
+                    query?.Where(member => member.Name != null && member.Name.EndsWith(displayNameToMatch));
+                    break;
+                case StringPropertyMatchType.Wildcard:
+                    query?.Where(member => member.Name != null && member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback
             }
+
+            return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Name"));
         }
 
         /// 
@@ -512,34 +509,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query = Query();
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query();
-
-                switch (matchType)
-                {
-                    case StringPropertyMatchType.Exact:
-                        query?.Where(member => member.Email.Equals(emailStringToMatch));
-                        break;
-                    case StringPropertyMatchType.Contains:
-                        query?.Where(member => member.Email.Contains(emailStringToMatch));
-                        break;
-                    case StringPropertyMatchType.StartsWith:
-                        query?.Where(member => member.Email.StartsWith(emailStringToMatch));
-                        break;
-                    case StringPropertyMatchType.EndsWith:
-                        query?.Where(member => member.Email.EndsWith(emailStringToMatch));
-                        break;
-                    case StringPropertyMatchType.Wildcard:
-                        query?.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar));
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType));
-                }
-
-                return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Email"));
+                case StringPropertyMatchType.Exact:
+                    query?.Where(member => member.Email.Equals(emailStringToMatch));
+                    break;
+                case StringPropertyMatchType.Contains:
+                    query?.Where(member => member.Email.Contains(emailStringToMatch));
+                    break;
+                case StringPropertyMatchType.StartsWith:
+                    query?.Where(member => member.Email.StartsWith(emailStringToMatch));
+                    break;
+                case StringPropertyMatchType.EndsWith:
+                    query?.Where(member => member.Email.EndsWith(emailStringToMatch));
+                    break;
+                case StringPropertyMatchType.Wildcard:
+                    query?.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType));
             }
+
+            return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Email"));
         }
 
         /// 
@@ -553,34 +548,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery? query = Query();
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query();
-
-                switch (matchType)
-                {
-                    case StringPropertyMatchType.Exact:
-                        query?.Where(member => member.Username.Equals(login));
-                        break;
-                    case StringPropertyMatchType.Contains:
-                        query?.Where(member => member.Username.Contains(login));
-                        break;
-                    case StringPropertyMatchType.StartsWith:
-                        query?.Where(member => member.Username.StartsWith(login));
-                        break;
-                    case StringPropertyMatchType.EndsWith:
-                        query?.Where(member => member.Username.EndsWith(login));
-                        break;
-                    case StringPropertyMatchType.Wildcard:
-                        query?.Where(member => member.Username.SqlWildcard(login, TextColumnType.NVarchar));
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType));
-                }
-
-                return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
+                case StringPropertyMatchType.Exact:
+                    query?.Where(member => member.Username.Equals(login));
+                    break;
+                case StringPropertyMatchType.Contains:
+                    query?.Where(member => member.Username.Contains(login));
+                    break;
+                case StringPropertyMatchType.StartsWith:
+                    query?.Where(member => member.Username.StartsWith(login));
+                    break;
+                case StringPropertyMatchType.EndsWith:
+                    query?.Where(member => member.Username.EndsWith(login));
+                    break;
+                case StringPropertyMatchType.Wildcard:
+                    query?.Where(member => member.Username.SqlWildcard(login, TextColumnType.NVarchar));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType));
             }
+
+            return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName"));
         }
 
         /// 
@@ -592,31 +585,29 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query;
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IQuery query;
-
-                switch (matchType)
-                {
-                    case StringPropertyMatchType.Exact:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlEquals(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlEquals(value, TextColumnType.NVarchar)));
-                        break;
-                    case StringPropertyMatchType.Contains:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlContains(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlContains(value, TextColumnType.NVarchar)));
-                        break;
-                    case StringPropertyMatchType.StartsWith:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar)));
-                        break;
-                    case StringPropertyMatchType.EndsWith:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlEndsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlEndsWith(value, TextColumnType.NVarchar)));
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType));
-                }
-
-                return _memberRepository.Get(query);
+                case StringPropertyMatchType.Exact:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlEquals(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlEquals(value, TextColumnType.NVarchar)));
+                    break;
+                case StringPropertyMatchType.Contains:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlContains(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlContains(value, TextColumnType.NVarchar)));
+                    break;
+                case StringPropertyMatchType.StartsWith:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar)));
+                    break;
+                case StringPropertyMatchType.EndsWith:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue!.SqlEndsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue!.SqlEndsWith(value, TextColumnType.NVarchar)));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType));
             }
+
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -628,34 +619,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query;
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IQuery query;
-
-                switch (matchType)
-                {
-                    case ValuePropertyMatchType.Exact:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue == value);
-                        break;
-                    case ValuePropertyMatchType.GreaterThan:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue > value);
-                        break;
-                    case ValuePropertyMatchType.LessThan:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue < value);
-                        break;
-                    case ValuePropertyMatchType.GreaterThanOrEqualTo:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue >= value);
-                        break;
-                    case ValuePropertyMatchType.LessThanOrEqualTo:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue <= value);
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType));
-                }
-
-                return _memberRepository.Get(query);
+                case ValuePropertyMatchType.Exact:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue == value);
+                    break;
+                case ValuePropertyMatchType.GreaterThan:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue > value);
+                    break;
+                case ValuePropertyMatchType.LessThan:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue < value);
+                    break;
+                case ValuePropertyMatchType.GreaterThanOrEqualTo:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue >= value);
+                    break;
+                case ValuePropertyMatchType.LessThanOrEqualTo:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue <= value);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType));
             }
+
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -666,13 +655,11 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, bool value)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).BoolPropertyValue == value);
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).BoolPropertyValue == value);
 
-                return _memberRepository.Get(query);
-            }
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -684,35 +671,33 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public IEnumerable? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IQuery query;
+
+            switch (matchType)
             {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IQuery query;
-
-                switch (matchType)
-                {
-                    case ValuePropertyMatchType.Exact:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue == value);
-                        break;
-                    case ValuePropertyMatchType.GreaterThan:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue > value);
-                        break;
-                    case ValuePropertyMatchType.LessThan:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue < value);
-                        break;
-                    case ValuePropertyMatchType.GreaterThanOrEqualTo:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue >= value);
-                        break;
-                    case ValuePropertyMatchType.LessThanOrEqualTo:
-                        query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue <= value);
-                        break;
-                    default:
-                        throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback
-                }
-
-                // TODO: Since this is by property value, we need a GetByPropertyQuery on the repo!
-                return _memberRepository.Get(query);
+                case ValuePropertyMatchType.Exact:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue == value);
+                    break;
+                case ValuePropertyMatchType.GreaterThan:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue > value);
+                    break;
+                case ValuePropertyMatchType.LessThan:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue < value);
+                    break;
+                case ValuePropertyMatchType.GreaterThanOrEqualTo:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue >= value);
+                    break;
+                case ValuePropertyMatchType.LessThanOrEqualTo:
+                    query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue <= value);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback
             }
+
+            // TODO: Since this is by property value, we need a GetByPropertyQuery on the repo!
+            return _memberRepository.Get(query);
         }
 
         /// 
@@ -722,11 +707,9 @@ namespace Umbraco.Cms.Core.Services
         /// True if the Member exists otherwise False
         public bool Exists(int id)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.Exists(id);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.Exists(id);
         }
 
         /// 
@@ -736,11 +719,9 @@ namespace Umbraco.Cms.Core.Services
         /// True if the Member exists otherwise False
         public bool Exists(string username)
         {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.Exists(username);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.Exists(username);
         }
 
         #endregion
@@ -760,67 +741,63 @@ namespace Umbraco.Cms.Core.Services
             member.Username = member.Username.Trim();
             member.Email = member.Email.Trim();
 
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var savingNotification = new MemberSavingNotification(member, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                var savingNotification = new MemberSavingNotification(member, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                if (string.IsNullOrWhiteSpace(member.Name))
-                {
-                    throw new ArgumentException("Cannot save member with empty name.");
-                }
-
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                _memberRepository.Save(member);
-
-                scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, 0, member.Id);
-
                 scope.Complete();
+                return;
             }
+
+            if (string.IsNullOrWhiteSpace(member.Name))
+            {
+                throw new ArgumentException("Cannot save member with empty name.");
+            }
+
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            _memberRepository.Save(member);
+
+            scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, 0, member.Id);
+
+            scope.Complete();
         }
 
         /// 
         public void Save(IEnumerable members)
         {
-            var membersA = members.ToArray();
+            IMember[] membersA = members.ToArray();
 
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var savingNotification = new MemberSavingNotification(membersA, evtMsgs);
+            if (scope.Notifications.PublishCancelable(savingNotification))
             {
-                var savingNotification = new MemberSavingNotification(membersA, evtMsgs);
-                if (scope.Notifications.PublishCancelable(savingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                foreach (var member in membersA)
-                {
-                    //trimming username and email to make sure we have no trailing space
-                    member.Username = member.Username.Trim();
-                    member.Email = member.Email.Trim();
-
-                    _memberRepository.Save(member);
-                }
-
-                scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification));
-
-                Audit(AuditType.Save, 0, -1, "Save multiple Members");
-
                 scope.Complete();
+                return;
             }
+
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            foreach (IMember member in membersA)
+            {
+                //trimming username and email to make sure we have no trailing space
+                member.Username = member.Username.Trim();
+                member.Email = member.Email.Trim();
+
+                _memberRepository.Save(member);
+            }
+
+            scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification));
+
+            Audit(AuditType.Save, 0, -1, "Save multiple Members");
+
+            scope.Complete();
         }
 
         #endregion
@@ -833,23 +810,21 @@ namespace Umbraco.Cms.Core.Services
         ///  to Delete
         public void Delete(IMember member)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
-            using (var scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            var deletingNotification = new MemberDeletingNotification(member, evtMsgs);
+            if (scope.Notifications.PublishCancelable(deletingNotification))
             {
-                var deletingNotification = new MemberDeletingNotification(member, evtMsgs);
-                if (scope.Notifications.PublishCancelable(deletingNotification))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                scope.WriteLock(Constants.Locks.MemberTree);
-                DeleteLocked(scope, member, evtMsgs, deletingNotification.State);
-
-                Audit(AuditType.Delete, 0, member.Id);
                 scope.Complete();
+                return;
             }
+
+            scope.WriteLock(Constants.Locks.MemberTree);
+            DeleteLocked(scope, member, evtMsgs, deletingNotification.State);
+
+            Audit(AuditType.Delete, 0, member.Id);
+            scope.Complete();
         }
 
         private void DeleteLocked(ICoreScope scope, IMember member, EventMessages evtMsgs, IDictionary? notificationState = null)
@@ -867,12 +842,10 @@ namespace Umbraco.Cms.Core.Services
 
         public void AddRole(string roleName)
         {
-            using (var scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                _memberGroupRepository.CreateIfNotExists(roleName);
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            _memberGroupRepository.CreateIfNotExists(roleName);
+            scope.Complete();
         }
 
         /// 
@@ -882,11 +855,9 @@ namespace Umbraco.Cms.Core.Services
 
         public IEnumerable GetAllRoles()
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberGroupRepository.GetMany().Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberGroupRepository.GetMany().Distinct();
         }
 
         /// 
@@ -896,178 +867,150 @@ namespace Umbraco.Cms.Core.Services
         /// A list of member roles
         public IEnumerable GetAllRoles(int memberId)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                var result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
-                return result.Select(x => x.Name).WhereNotNull().Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
+            return result.Select(x => x.Name).WhereNotNull().Distinct();
         }
 
         public IEnumerable GetAllRoles(string username)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username);
-                return result.Where(x => x.Name != null).Select(x => x.Name).Distinct()!;
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username);
+            return result.Where(x => x.Name != null).Select(x => x.Name).Distinct()!;
         }
 
         public IEnumerable GetAllRolesIds()
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct();
         }
 
         public IEnumerable GetAllRolesIds(int memberId)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
-                return result.Select(x => x.Id).Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(memberId);
+            return result.Select(x => x.Id).Distinct();
         }
 
         public IEnumerable GetAllRolesIds(string username)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username);
-                return result.Select(x => x.Id).Distinct();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            IEnumerable result = _memberGroupRepository.GetMemberGroupsForMember(username);
+            return result.Select(x => x.Id).Distinct();
         }
 
         public IEnumerable GetMembersInRole(string roleName)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.GetByMemberGroup(roleName);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.GetByMemberGroup(roleName);
         }
 
         public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                scope.ReadLock(Constants.Locks.MemberTree);
-                return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            scope.ReadLock(Constants.Locks.MemberTree);
+            return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType);
         }
 
         public bool DeleteRole(string roleName, bool throwIfBeingUsed)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            if (throwIfBeingUsed)
             {
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                if (throwIfBeingUsed)
+                // get members in role
+                IEnumerable membersInRole = _memberRepository.GetByMemberGroup(roleName);
+                if (membersInRole.Any())
                 {
-                    // get members in role
-                    IEnumerable membersInRole = _memberRepository.GetByMemberGroup(roleName);
-                    if (membersInRole.Any())
-                    {
-                        throw new InvalidOperationException("The role " + roleName + " is currently assigned to members");
-                    }
+                    throw new InvalidOperationException("The role " + roleName + " is currently assigned to members");
                 }
-
-                IQuery query = Query().Where(g => g.Name == roleName);
-                IMemberGroup[]? found = _memberGroupRepository.Get(query)?.ToArray();
-
-                if (found is not null)
-                {
-                    foreach (IMemberGroup memberGroup in found)
-                    {
-                        _memberGroupService.Delete(memberGroup);
-                    }
-                }
-
-                scope.Complete();
-                return found?.Length > 0;
             }
+
+            IQuery query = Query().Where(g => g.Name == roleName);
+            IMemberGroup[]? found = _memberGroupRepository.Get(query)?.ToArray();
+
+            if (found is not null)
+            {
+                foreach (IMemberGroup memberGroup in found)
+                {
+                    _memberGroupService.Delete(memberGroup);
+                }
+            }
+
+            scope.Complete();
+            return found?.Length > 0;
         }
 
         public void AssignRole(string username, string roleName) => AssignRoles(new[] { username }, new[] { roleName });
 
         public void AssignRoles(string[] usernames, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                int[] ids = _memberRepository.GetMemberIds(usernames);
-                _memberGroupRepository.AssignRoles(ids, roleNames);
-                scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            var ids = _memberRepository.GetMemberIds(usernames);
+            _memberGroupRepository.AssignRoles(ids, roleNames);
+            scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames));
+            scope.Complete();
         }
 
         public void DissociateRole(string username, string roleName) => DissociateRoles(new[] { username }, new[] { roleName });
 
         public void DissociateRoles(string[] usernames, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                int[] ids = _memberRepository.GetMemberIds(usernames);
-                _memberGroupRepository.DissociateRoles(ids, roleNames);
-                scope.Notifications.Publish(new RemovedMemberRolesNotification(ids, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            var ids = _memberRepository.GetMemberIds(usernames);
+            _memberGroupRepository.DissociateRoles(ids, roleNames);
+            scope.Notifications.Publish(new RemovedMemberRolesNotification(ids, roleNames));
+            scope.Complete();
         }
 
         public void AssignRole(int memberId, string roleName) => AssignRoles(new[] { memberId }, new[] { roleName });
 
         public void AssignRoles(int[] memberIds, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                _memberGroupRepository.AssignRoles(memberIds, roleNames);
-                scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            _memberGroupRepository.AssignRoles(memberIds, roleNames);
+            scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames));
+            scope.Complete();
         }
 
         public void DissociateRole(int memberId, string roleName) => DissociateRoles(new[] { memberId }, new[] { roleName });
 
         public void DissociateRoles(int[] memberIds, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                _memberGroupRepository.DissociateRoles(memberIds, roleNames);
-                scope.Notifications.Publish(new RemovedMemberRolesNotification(memberIds, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            _memberGroupRepository.DissociateRoles(memberIds, roleNames);
+            scope.Notifications.Publish(new RemovedMemberRolesNotification(memberIds, roleNames));
+            scope.Complete();
         }
 
         public void ReplaceRoles(string[] usernames, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                int[] ids = _memberRepository.GetMemberIds(usernames);
-                _memberGroupRepository.ReplaceRoles(ids, roleNames);
-                scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            int[] ids = _memberRepository.GetMemberIds(usernames);
+            _memberGroupRepository.ReplaceRoles(ids, roleNames);
+            scope.Notifications.Publish(new AssignedMemberRolesNotification(ids, roleNames));
+            scope.Complete();
         }
 
         public void ReplaceRoles(int[] memberIds, string[] roleNames)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
-            {
-                scope.WriteLock(Constants.Locks.MemberTree);
-                _memberGroupRepository.ReplaceRoles(memberIds, roleNames);
-                scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames));
-                scope.Complete();
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+            _memberGroupRepository.ReplaceRoles(memberIds, roleNames);
+            scope.Notifications.Publish(new AssignedMemberRolesNotification(memberIds, roleNames));
+            scope.Complete();
         }
 
         #endregion
@@ -1090,34 +1033,32 @@ namespace Umbraco.Cms.Core.Services
         /// 
         public MemberExportModel? ExportMember(Guid key)
         {
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            IQuery? query = Query().Where(x => x.Key == key);
+            IMember? member = _memberRepository.Get(query)?.FirstOrDefault();
+
+            if (member == null)
             {
-                IQuery? query = Query().Where(x => x.Key == key);
-                IMember? member = _memberRepository.Get(query)?.FirstOrDefault();
-
-                if (member == null)
-                {
-                    return null;
-                }
-
-                var model = new MemberExportModel
-                {
-                    Id = member.Id,
-                    Key = member.Key,
-                    Name = member.Name,
-                    Username = member.Username,
-                    Email = member.Email,
-                    Groups = GetAllRoles(member.Id).ToList(),
-                    ContentTypeAlias = member.ContentTypeAlias,
-                    CreateDate = member.CreateDate,
-                    UpdateDate = member.UpdateDate,
-                    Properties = new List(GetPropertyExportItems(member))
-                };
-
-                scope.Notifications.Publish(new ExportedMemberNotification(member, model));
-
-                return model;
+                return null;
             }
+
+            var model = new MemberExportModel
+            {
+                Id = member.Id,
+                Key = member.Key,
+                Name = member.Name,
+                Username = member.Username,
+                Email = member.Email,
+                Groups = GetAllRoles(member.Id).ToList(),
+                ContentTypeAlias = member.ContentTypeAlias,
+                CreateDate = member.CreateDate,
+                UpdateDate = member.UpdateDate,
+                Properties = new List(GetPropertyExportItems(member))
+            };
+
+            scope.Notifications.Publish(new ExportedMemberNotification(member, model));
+
+            return model;
         }
 
         private static IEnumerable GetPropertyExportItems(IMember member)
@@ -1156,38 +1097,36 @@ namespace Umbraco.Cms.Core.Services
         /// Id of the MemberType
         public void DeleteMembersOfType(int memberTypeId)
         {
-            var evtMsgs = EventMessagesFactory.Get();
+            EventMessages evtMsgs = EventMessagesFactory.Get();
 
             // note: no tree to manage here
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+            using ICoreScope scope = ScopeProvider.CreateCoreScope();
+            scope.WriteLock(Constants.Locks.MemberTree);
+
+            // TODO: What about content that has the contenttype as part of its composition?
+            IQuery? query = Query().Where(x => x.ContentTypeId == memberTypeId);
+
+            IMember[]? members = _memberRepository.Get(query)?.ToArray();
+
+            if (members is null)
             {
-                scope.WriteLock(Constants.Locks.MemberTree);
-
-                // TODO: What about content that has the contenttype as part of its composition?
-                IQuery? query = Query().Where(x => x.ContentTypeId == memberTypeId);
-
-                IMember[]? members = _memberRepository.Get(query)?.ToArray();
-
-                if (members is null)
-                {
-                    return;
-                }
-
-                if (scope.Notifications.PublishCancelable(new MemberDeletingNotification(members, evtMsgs)))
-                {
-                    scope.Complete();
-                    return;
-                }
-
-                foreach (IMember member in members)
-                {
-                    // delete media
-                    // triggers the deleted event (and handles the files)
-                    DeleteLocked(scope, member, evtMsgs);
-                }
-
-                scope.Complete();
+                return;
             }
+
+            if (scope.Notifications.PublishCancelable(new MemberDeletingNotification(members, evtMsgs)))
+            {
+                scope.Complete();
+                return;
+            }
+
+            foreach (IMember member in members)
+            {
+                // delete media
+                // triggers the deleted event (and handles the files)
+                DeleteLocked(scope, member, evtMsgs);
+            }
+
+            scope.Complete();
         }
 
         private IMemberType GetMemberType(ICoreScope scope, string memberTypeAlias)
@@ -1226,10 +1165,8 @@ namespace Umbraco.Cms.Core.Services
                 throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias));
             }
 
-            using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
-            {
-                return GetMemberType(scope, memberTypeAlias);
-            }
+            using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+            return GetMemberType(scope, memberTypeAlias);
         }
         #endregion
     }
diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs
index 1d42989841..67b7f08111 100644
--- a/src/Umbraco.Core/Services/MemberTypeService.cs
+++ b/src/Umbraco.Core/Services/MemberTypeService.cs
@@ -9,98 +9,145 @@ using Umbraco.Cms.Core.Services.Changes;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class MemberTypeService : ContentTypeServiceBase, IMemberTypeService
 {
-    public class MemberTypeService : ContentTypeServiceBase, IMemberTypeService
+    private readonly IMemberTypeRepository _memberTypeRepository;
+
+    [Obsolete("Please use the constructor taking all parameters. This constructor will be removed in V12.")]
+    public MemberTypeService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMemberService memberService,
+        IMemberTypeRepository memberTypeRepository,
+        IAuditRepository auditRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : this(
+            provider,
+            loggerFactory,
+            eventMessagesFactory,
+            memberService,
+            memberTypeRepository,
+            auditRepository,
+            StaticServiceProvider.Instance.GetRequiredService(),
+            entityRepository,
+            eventAggregator)
     {
-        private readonly IMemberTypeRepository _memberTypeRepository;
+    }
 
-        [Obsolete("Please use the constructor taking all parameters. This constructor will be removed in V12.")]
-        public MemberTypeService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberService memberService,
-            IMemberTypeRepository memberTypeRepository, IAuditRepository auditRepository, IEntityRepository entityRepository, IEventAggregator eventAggregator)
-            : this(provider, loggerFactory, eventMessagesFactory, memberService, memberTypeRepository, auditRepository, StaticServiceProvider.Instance.GetRequiredService(), entityRepository, eventAggregator)
+    public MemberTypeService(
+        ICoreScopeProvider provider,
+        ILoggerFactory loggerFactory,
+        IEventMessagesFactory eventMessagesFactory,
+        IMemberService memberService,
+        IMemberTypeRepository memberTypeRepository,
+        IAuditRepository auditRepository,
+        IMemberTypeContainerRepository entityContainerRepository,
+        IEntityRepository entityRepository,
+        IEventAggregator eventAggregator)
+        : base(
+            provider,
+            loggerFactory,
+            eventMessagesFactory,
+            memberTypeRepository,
+            auditRepository,
+            entityContainerRepository,
+            entityRepository,
+            eventAggregator)
+    {
+        MemberService = memberService;
+        _memberTypeRepository = memberTypeRepository;
+    }
+
+    // beware! order is important to avoid deadlocks
+    protected override int[] ReadLockIds { get; } = { Constants.Locks.MemberTypes };
+
+    protected override int[] WriteLockIds { get; } = { Constants.Locks.MemberTree, Constants.Locks.MemberTypes };
+
+    protected override Guid ContainedObjectType => Constants.ObjectTypes.MemberType;
+
+    private IMemberService MemberService { get; }
+
+    public string GetDefault()
+    {
+        using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
         {
-        }
+            scope.ReadLock(ReadLockIds);
 
-        public MemberTypeService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberService memberService,
-            IMemberTypeRepository memberTypeRepository, IAuditRepository auditRepository, IMemberTypeContainerRepository entityContainerRepository, IEntityRepository entityRepository, IEventAggregator eventAggregator)
-            : base(provider, loggerFactory, eventMessagesFactory, memberTypeRepository, auditRepository, entityContainerRepository, entityRepository, eventAggregator)
-        {
-            MemberService = memberService;
-            _memberTypeRepository = memberTypeRepository;
-        }
-
-        // beware! order is important to avoid deadlocks
-        protected override int[] ReadLockIds { get; } = { Cms.Core.Constants.Locks.MemberTypes };
-        protected override int[] WriteLockIds { get; } = { Cms.Core.Constants.Locks.MemberTree, Cms.Core.Constants.Locks.MemberTypes };
-
-        private IMemberService MemberService { get; }
-
-        protected override Guid ContainedObjectType => Cms.Core.Constants.ObjectTypes.MemberType;
-
-        #region Notifications
-
-        protected override SavingNotification GetSavingNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeSavingNotification(item, eventMessages);
-
-        protected override SavingNotification GetSavingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeSavingNotification(items, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeSavedNotification(item, eventMessages);
-
-        protected override SavedNotification GetSavedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeSavedNotification(items, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IMemberType item,
-            EventMessages eventMessages) => new MemberTypeDeletingNotification(item, eventMessages);
-
-        protected override DeletingNotification GetDeletingNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeDeletingNotification(items, eventMessages);
-
-        protected override DeletedNotification GetDeletedNotification(IEnumerable items,
-            EventMessages eventMessages) => new MemberTypeDeletedNotification(items, eventMessages);
-
-        protected override MovingNotification GetMovingNotification(MoveEventInfo moveInfo,
-            EventMessages eventMessages) => new MemberTypeMovingNotification(moveInfo, eventMessages);
-
-        protected override MovedNotification GetMovedNotification(
-            IEnumerable> moveInfo, EventMessages eventMessages) =>
-            new MemberTypeMovedNotification(moveInfo, eventMessages);
-
-        protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MemberTypeChangedNotification(changes, eventMessages);
-
-        protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
-            IEnumerable> changes, EventMessages eventMessages) =>
-            new MemberTypeRefreshedNotification(changes, eventMessages);
-
-        #endregion
-
-        protected override void DeleteItemsOfTypes(IEnumerable typeIds)
-        {
-            foreach (var typeId in typeIds)
-                MemberService.DeleteMembersOfType(typeId);
-        }
-
-        public string GetDefault()
-        {
-            using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
+            using (IEnumerator e = _memberTypeRepository.GetMany(new int[0]).GetEnumerator())
             {
-                scope.ReadLock(ReadLockIds);
-
-                using (var e = _memberTypeRepository.GetMany(new int[0]).GetEnumerator())
+                if (e.MoveNext() == false)
                 {
-                    if (e.MoveNext() == false)
-                        throw new InvalidOperationException("No member types could be resolved");
-                    var first = e.Current.Alias;
-                    var current = true;
-                    while (e.Current.Alias.InvariantEquals("Member") == false && (current = e.MoveNext()))
-                    { }
-                    return current ? e.Current.Alias : first;
+                    throw new InvalidOperationException("No member types could be resolved");
                 }
+
+                var first = e.Current.Alias;
+                var current = true;
+                while (e.Current.Alias.InvariantEquals("Member") == false && (current = e.MoveNext()))
+                {
+                }
+
+                return current ? e.Current.Alias : first;
             }
         }
     }
+
+    protected override void DeleteItemsOfTypes(IEnumerable typeIds)
+    {
+        foreach (var typeId in typeIds)
+        {
+            MemberService.DeleteMembersOfType(typeId);
+        }
+    }
+
+    #region Notifications
+
+    protected override SavingNotification GetSavingNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeSavingNotification(item, eventMessages);
+
+    protected override SavingNotification GetSavingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeSavingNotification(items, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeSavedNotification(item, eventMessages);
+
+    protected override SavedNotification GetSavedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeSavedNotification(items, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IMemberType item,
+        EventMessages eventMessages) => new MemberTypeDeletingNotification(item, eventMessages);
+
+    protected override DeletingNotification GetDeletingNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeDeletingNotification(items, eventMessages);
+
+    protected override DeletedNotification GetDeletedNotification(
+        IEnumerable items,
+        EventMessages eventMessages) => new MemberTypeDeletedNotification(items, eventMessages);
+
+    protected override MovingNotification GetMovingNotification(
+        MoveEventInfo moveInfo,
+        EventMessages eventMessages) => new MemberTypeMovingNotification(moveInfo, eventMessages);
+
+    protected override MovedNotification GetMovedNotification(
+        IEnumerable> moveInfo, EventMessages eventMessages) =>
+        new MemberTypeMovedNotification(moveInfo, eventMessages);
+
+    protected override ContentTypeChangeNotification GetContentTypeChangedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MemberTypeChangedNotification(changes, eventMessages);
+
+    protected override ContentTypeRefreshNotification GetContentTypeRefreshedNotification(
+        IEnumerable> changes, EventMessages eventMessages) =>
+        new MemberTypeRefreshedNotification(changes, eventMessages);
+
+    #endregion
 }
diff --git a/src/Umbraco.Core/Services/MetricsConsentService.cs b/src/Umbraco.Core/Services/MetricsConsentService.cs
index ca64d42810..a5309d35f1 100644
--- a/src/Umbraco.Core/Services/MetricsConsentService.cs
+++ b/src/Umbraco.Core/Services/MetricsConsentService.cs
@@ -1,81 +1,80 @@
-using System;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Security;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class MetricsConsentService : IMetricsConsentService
 {
-    public class MetricsConsentService : IMetricsConsentService
-    {
-        internal const string Key = "UmbracoAnalyticsLevel";
+    internal const string Key = "UmbracoAnalyticsLevel";
 
-        private readonly IKeyValueService _keyValueService;
-        private readonly ILogger _logger;
-        private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
-        private readonly IUserService _userService;
+    private readonly IKeyValueService _keyValueService;
+    private readonly ILogger _logger;
+    private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
+    private readonly IUserService _userService;
 
-        // Scheduled for removal in V12
-        [Obsolete("Please use the constructor that takes an ILogger and IBackOfficeSecurity instead")]
-        public MetricsConsentService(IKeyValueService keyValueService)
+    // Scheduled for removal in V12
+    [Obsolete("Please use the constructor that takes an ILogger and IBackOfficeSecurity instead")]
+    public MetricsConsentService(IKeyValueService keyValueService)
         : this(
             keyValueService,
             StaticServiceProvider.Instance.GetRequiredService>(),
             StaticServiceProvider.Instance.GetRequiredService(),
             StaticServiceProvider.Instance.GetRequiredService())
-        {
-        }
+    {
+    }
 
-        // Scheduled for removal in V12
-        [Obsolete("Please use the constructor that takes an IUserService instead")]
-        public MetricsConsentService(
-            IKeyValueService keyValueService,
-            ILogger logger,
-            IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
+    // Scheduled for removal in V12
+    [Obsolete("Please use the constructor that takes an IUserService instead")]
+    public MetricsConsentService(
+        IKeyValueService keyValueService,
+        ILogger logger,
+        IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
         : this(
             keyValueService,
             logger,
             backOfficeSecurityAccessor,
             StaticServiceProvider.Instance.GetRequiredService())
+    {
+    }
+
+    public MetricsConsentService(
+        IKeyValueService keyValueService,
+        ILogger logger,
+        IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+        IUserService userService)
+    {
+        _keyValueService = keyValueService;
+        _logger = logger;
+        _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
+        _userService = userService;
+    }
+
+    public TelemetryLevel GetConsentLevel()
+    {
+        var analyticsLevelString = _keyValueService.GetValue(Key);
+
+        if (analyticsLevelString is null ||
+            Enum.TryParse(analyticsLevelString, out TelemetryLevel analyticsLevel) is false)
         {
+            return TelemetryLevel.Basic;
         }
 
-        public MetricsConsentService(
-            IKeyValueService keyValueService,
-            ILogger logger,
-            IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
-            IUserService userService
-            )
+        return analyticsLevel;
+    }
+
+    public void SetConsentLevel(TelemetryLevel telemetryLevel)
+    {
+        IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
+        if (currentUser is null)
         {
-            _keyValueService = keyValueService;
-            _logger = logger;
-            _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
-            _userService = userService;
+            currentUser = _userService.GetUserById(Constants.Security.SuperUserId);
         }
 
-        public TelemetryLevel GetConsentLevel()
-        {
-            var analyticsLevelString = _keyValueService.GetValue(Key);
-
-            if (analyticsLevelString is null || Enum.TryParse(analyticsLevelString, out TelemetryLevel analyticsLevel) is false)
-            {
-                return TelemetryLevel.Basic;
-            }
-
-            return analyticsLevel;
-        }
-
-        public void SetConsentLevel(TelemetryLevel telemetryLevel)
-        {
-            var currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
-            if (currentUser is null)
-            {
-                currentUser = _userService.GetUserById(Constants.Security.SuperUserId);
-            }
-
-            _logger.LogInformation("Telemetry level set to {telemetryLevel} by {username}", telemetryLevel, currentUser?.Username);
-            _keyValueService.SetValue(Key, telemetryLevel.ToString());
-        }
+        _logger.LogInformation("Telemetry level set to {telemetryLevel} by {username}", telemetryLevel, currentUser?.Username);
+        _keyValueService.SetValue(Key, telemetryLevel.ToString());
     }
 }
diff --git a/src/Umbraco.Core/Services/MoveOperationStatusType.cs b/src/Umbraco.Core/Services/MoveOperationStatusType.cs
index 4de17b2fa5..26e70eb9e0 100644
--- a/src/Umbraco.Core/Services/MoveOperationStatusType.cs
+++ b/src/Umbraco.Core/Services/MoveOperationStatusType.cs
@@ -1,32 +1,30 @@
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+/// 
+///     A status type of the result of moving an item
+/// 
+/// 
+///     Anything less than 10 = Success!
+/// 
+public enum MoveOperationStatusType : byte
 {
+    /// 
+    ///     The move was successful.
+    /// 
+    Success = 0,
 
     /// 
-    /// A status type of the result of moving an item
+    ///     The parent being moved to doesn't exist
     /// 
-    /// 
-    /// Anything less than 10 = Success!
-    /// 
-    public enum MoveOperationStatusType : byte
-    {
-        /// 
-        /// The move was successful.
-        /// 
-        Success = 0,
+    FailedParentNotFound = 13,
 
-        /// 
-        /// The parent being moved to doesn't exist
-        /// 
-        FailedParentNotFound = 13,
+    /// 
+    ///     The move action has been cancelled by an event handler
+    /// 
+    FailedCancelledByEvent = 14,
 
-        /// 
-        /// The move action has been cancelled by an event handler
-        /// 
-        FailedCancelledByEvent = 14,
-
-        /// 
-        /// Trying to move an item to an invalid path (i.e. a child of itself)
-        /// 
-        FailedNotAllowedByPath = 15,
-    }
+    /// 
+    ///     Trying to move an item to an invalid path (i.e. a child of itself)
+    /// 
+    FailedNotAllowedByPath = 15,
 }
diff --git a/src/Umbraco.Core/Services/NodeCountService.cs b/src/Umbraco.Core/Services/NodeCountService.cs
index 7298d7f23a..cf7417058e 100644
--- a/src/Umbraco.Core/Services/NodeCountService.cs
+++ b/src/Umbraco.Core/Services/NodeCountService.cs
@@ -1,31 +1,29 @@
-using System;
 using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Cms.Core.Services;
 
-namespace Umbraco.Cms.Infrastructure.Services.Implement
+namespace Umbraco.Cms.Infrastructure.Services.Implement;
+
+public class NodeCountService : INodeCountService
 {
-    public class NodeCountService : INodeCountService
+    private readonly INodeCountRepository _nodeCountRepository;
+    private readonly ICoreScopeProvider _scopeProvider;
+
+    public NodeCountService(INodeCountRepository nodeCountRepository, ICoreScopeProvider scopeProvider)
     {
-        private readonly INodeCountRepository _nodeCountRepository;
-        private readonly ICoreScopeProvider _scopeProvider;
+        _nodeCountRepository = nodeCountRepository;
+        _scopeProvider = scopeProvider;
+    }
 
-        public NodeCountService(INodeCountRepository nodeCountRepository, ICoreScopeProvider scopeProvider)
-        {
-            _nodeCountRepository = nodeCountRepository;
-            _scopeProvider = scopeProvider;
-        }
+    public int GetNodeCount(Guid nodeType)
+    {
+        using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+        return _nodeCountRepository.GetNodeCount(nodeType);
+    }
 
-        public int GetNodeCount(Guid nodeType)
-        {
-            using var scope = _scopeProvider.CreateCoreScope(autoComplete: true);
-            return _nodeCountRepository.GetNodeCount(nodeType);
-        }
-
-        public int GetMediaCount()
-        {
-            using var scope = _scopeProvider.CreateCoreScope(autoComplete: true);
-            return _nodeCountRepository.GetMediaCount();
-        }
+    public int GetMediaCount()
+    {
+        using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+        return _nodeCountRepository.GetMediaCount();
     }
 }
diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs
index 39aa6a863d..822ba89079 100644
--- a/src/Umbraco.Core/Services/NotificationService.cs
+++ b/src/Umbraco.Core/Services/NotificationService.cs
@@ -1,10 +1,6 @@
-using System;
 using System.Collections.Concurrent;
-using System.Collections.Generic;
 using System.Globalization;
-using System.Linq;
 using System.Text;
-using System.Threading;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 using Umbraco.Cms.Core.Configuration.Models;
@@ -18,541 +14,625 @@ using Umbraco.Cms.Core.Persistence.Repositories;
 using Umbraco.Cms.Core.Scoping;
 using Umbraco.Extensions;
 
-namespace Umbraco.Cms.Core.Services
+namespace Umbraco.Cms.Core.Services;
+
+public class NotificationService : INotificationService
 {
-    public class NotificationService : INotificationService
+    // manage notifications
+    // ideally, would need to use IBackgroundTasks - but they are not part of Core!
+    private static readonly object Locker = new();
+
+    private readonly IContentService _contentService;
+    private readonly ContentSettings _contentSettings;
+    private readonly IEmailSender _emailSender;
+    private readonly GlobalSettings _globalSettings;
+    private readonly IIOHelper _ioHelper;
+    private readonly ILocalizationService _localizationService;
+    private readonly ILogger _logger;
+    private readonly INotificationsRepository _notificationsRepository;
+    private readonly ICoreScopeProvider _uowProvider;
+    private readonly IUserService _userService;
+
+    public NotificationService(
+        ICoreScopeProvider provider,
+        IUserService userService,
+        IContentService contentService,
+        ILocalizationService localizationService,
+        ILogger logger,
+        IIOHelper ioHelper,
+        INotificationsRepository notificationsRepository,
+        IOptions globalSettings,
+        IOptions contentSettings,
+        IEmailSender emailSender)
     {
-        private readonly ICoreScopeProvider _uowProvider;
-        private readonly IUserService _userService;
-        private readonly IContentService _contentService;
-        private readonly ILocalizationService _localizationService;
-        private readonly INotificationsRepository _notificationsRepository;
-        private readonly GlobalSettings _globalSettings;
-        private readonly ContentSettings _contentSettings;
-        private readonly IEmailSender _emailSender;
-        private readonly ILogger _logger;
-        private readonly IIOHelper _ioHelper;
+        _notificationsRepository = notificationsRepository;
+        _globalSettings = globalSettings.Value;
+        _contentSettings = contentSettings.Value;
+        _emailSender = emailSender;
+        _uowProvider = provider ?? throw new ArgumentNullException(nameof(provider));
+        _userService = userService ?? throw new ArgumentNullException(nameof(userService));
+        _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
+        _localizationService = localizationService;
+        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+        _ioHelper = ioHelper;
+    }
 
-        public NotificationService(ICoreScopeProvider provider, IUserService userService, IContentService contentService, ILocalizationService localizationService,
-            ILogger logger, IIOHelper ioHelper, INotificationsRepository notificationsRepository, IOptions globalSettings, IOptions contentSettings, IEmailSender emailSender)
+    /// 
+    ///     Sends the notifications for the specified user regarding the specified node and action.
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    /// 
+    public void SendNotifications(
+        IUser operatingUser,
+        IEnumerable entities,
+        string? action,
+        string? actionName,
+        Uri siteUri,
+        Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
+        Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody)
+    {
+        var entitiesL = entities.ToList();
+
+        // exit if there are no entities
+        if (entitiesL.Count == 0)
         {
-            _notificationsRepository = notificationsRepository;
-            _globalSettings = globalSettings.Value;
-            _contentSettings = contentSettings.Value;
-            _emailSender = emailSender;
-            _uowProvider = provider ?? throw new ArgumentNullException(nameof(provider));
-            _userService = userService ?? throw new ArgumentNullException(nameof(userService));
-            _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
-            _localizationService = localizationService;
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            _ioHelper = ioHelper;
+            return;
         }
 
-        /// 
-        /// Gets the previous version to the latest version of the content item if there is one
-        /// 
-        /// 
-        /// 
-        private IContentBase? GetPreviousVersion(int contentId)
+        // put all entity's paths into a list with the same indices
+        var paths = entitiesL.Select(x =>
+                x.Path.Split(Constants.CharArrays.Comma).Select(s => int.Parse(s, CultureInfo.InvariantCulture))
+                    .ToArray())
+            .ToArray();
+
+        // lazily get versions
+        var prevVersionDictionary = new Dictionary();
+
+        // see notes above
+        var id = Constants.Security.SuperUserId;
+        const int pagesz = 400; // load batches of 400 users
+        do
         {
-            // Regarding this: http://issues.umbraco.org/issue/U4-5180
-            // we know they are descending from the service so we know that newest is first
-            // we are only selecting the top 2 rows since that is all we need
-            var allVersions = _contentService.GetVersionIds(contentId, 2).ToList();
-            var prevVersionIndex = allVersions.Count > 1 ? 1 : 0;
-            return _contentService.GetVersion(allVersions[prevVersionIndex]);
-        }
-
-        /// 
-        /// Sends the notifications for the specified user regarding the specified node and action.
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        public void SendNotifications(IUser operatingUser, IEnumerable entities, string? action, string? actionName, Uri siteUri,
-            Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
-            Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody)
-        {
-            var entitiesL = entities.ToList();
-
-            //exit if there are no entities
-            if (entitiesL.Count == 0) return;
-
-            //put all entity's paths into a list with the same indices
-            var paths = entitiesL.Select(x => x.Path.Split(Constants.CharArrays.Comma).Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray()).ToArray();
-
-            // lazily get versions
-            var prevVersionDictionary = new Dictionary();
-
-            // see notes above
-            var id = Cms.Core.Constants.Security.SuperUserId;
-            const int pagesz = 400; // load batches of 400 users
-            do
+            // users are returned ordered by id, notifications are returned ordered by user id
+            var users = _userService.GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList();
+            var notifications = GetUsersNotifications(users.Select(x => x.Id), action, Enumerable.Empty(), Constants.ObjectTypes.Document)?.ToList();
+            if (notifications is null || notifications.Count == 0)
             {
-                // users are returned ordered by id, notifications are returned ordered by user id
-                var users = _userService.GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList();
-                var notifications = GetUsersNotifications(users.Select(x => x.Id), action, Enumerable.Empty(), Cms.Core.Constants.ObjectTypes.Document)?.ToList();
-                if (notifications is null || notifications.Count == 0) break;
+                break;
+            }
 
-                var i = 0;
-                foreach (var user in users)
+            var i = 0;
+            foreach (IUser user in users)
+            {
+                // continue if there's no notification for this user
+                if (notifications[i].UserId != user.Id)
                 {
-                    // continue if there's no notification for this user
-                    if (notifications[i].UserId != user.Id) continue; // next user
+                    continue; // next user
+                }
 
-                    for (var j = 0; j < entitiesL.Count; j++)
+                for (var j = 0; j < entitiesL.Count; j++)
+                {
+                    IContent content = entitiesL[j];
+                    var path = paths[j];
+
+                    // test if the notification applies to the path ie to this entity
+                    if (path.Contains(notifications[i].EntityId) == false)
                     {
-                        var content = entitiesL[j];
-                        var path = paths[j];
-
-                        // test if the notification applies to the path ie to this entity
-                        if (path.Contains(notifications[i].EntityId) == false) continue; // next entity
-
-                        if (prevVersionDictionary.ContainsKey(content.Id) == false)
-                        {
-                            prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id);
-                        }
-
-                        // queue notification
-                        var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, siteUri, createSubject, createBody);
-                        Enqueue(req);
+                        continue; // next entity
                     }
 
-                    // skip other notifications for this user, essentially this means moving i to the next index of notifications
-                    // for the next user.
-                    do
+                    if (prevVersionDictionary.ContainsKey(content.Id) == false)
                     {
-                        i++;
-                    } while (i < notifications.Count && notifications[i].UserId == user.Id);
-
-                    if (i >= notifications.Count) break; // break if no more notifications
-                }
-
-                // load more users if any
-                id = users.Count == pagesz ? users.Last().Id + 1 : -1;
-
-            } while (id > 0);
-        }
-
-        private IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType)
-        {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetUsersNotifications(userIds, action, nodeIds, objectType);
-            }
-        }
-        /// 
-        /// Gets the notifications for the user
-        /// 
-        /// 
-        /// 
-        public IEnumerable? GetUserNotifications(IUser user)
-        {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetUserNotifications(user);
-            }
-        }
-
-        /// 
-        /// Gets the notifications for the user based on the specified node path
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's parent (ancestors)
-        /// 
-        public IEnumerable? GetUserNotifications(IUser? user, string path)
-        {
-            if (user is null)
-            {
-                return null;
-            }
-
-            var userNotifications = GetUserNotifications(user);
-            return FilterUserNotificationsByPath(userNotifications, path);
-        }
-
-        /// 
-        /// Filters a userNotifications collection by a path
-        /// 
-        /// 
-        /// 
-        /// 
-        public IEnumerable? FilterUserNotificationsByPath(IEnumerable? userNotifications, string path)
-        {
-            var pathParts = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
-            return userNotifications?.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList();
-        }
-
-        /// 
-        /// Deletes notifications by entity
-        /// 
-        /// 
-        public IEnumerable? GetEntityNotifications(IEntity entity)
-        {
-            using (var scope = _uowProvider.CreateCoreScope(autoComplete: true))
-            {
-                return _notificationsRepository.GetEntityNotifications(entity);
-            }
-        }
-
-        /// 
-        /// Deletes notifications by entity
-        /// 
-        /// 
-        public void DeleteNotifications(IEntity entity)
-        {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(entity);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Deletes notifications by user
-        /// 
-        /// 
-        public void DeleteNotifications(IUser user)
-        {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(user);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Delete notifications by user and entity
-        /// 
-        /// 
-        /// 
-        public void DeleteNotifications(IUser user, IEntity entity)
-        {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                _notificationsRepository.DeleteNotifications(user, entity);
-                scope.Complete();
-            }
-        }
-
-        /// 
-        /// Sets the specific notifications for the user and entity
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// This performs a full replace
-        /// 
-        public IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions)
-        {
-            if (user is null)
-            {
-                return null;
-            }
-
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                var notifications = _notificationsRepository.SetNotifications(user, entity, actions);
-                scope.Complete();
-                return notifications;
-            }
-        }
-
-        /// 
-        /// Creates a new notification
-        /// 
-        /// 
-        /// 
-        /// The action letter - note: this is a string for future compatibility
-        /// 
-        public Notification CreateNotification(IUser user, IEntity entity, string action)
-        {
-            using (var scope = _uowProvider.CreateCoreScope())
-            {
-                var notification = _notificationsRepository.CreateNotification(user, entity, action);
-                scope.Complete();
-                return notification;
-            }
-        }
-
-        #region private methods
-
-        /// 
-        /// Sends the notification
-        /// 
-        /// 
-        /// 
-        /// 
-        /// 
-        /// The action readable name - currently an action is just a single letter, this is the name associated with the letter 
-        /// 
-        /// Callback to create the mail subject
-        /// Callback to create the mail body
-        private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContent content, IContentBase? oldDoc,
-            string? actionName,
-            Uri siteUri,
-            Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
-            Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody)
-        {
-            if (performingUser == null) throw new ArgumentNullException("performingUser");
-            if (mailingUser == null) throw new ArgumentNullException("mailingUser");
-            if (content == null) throw new ArgumentNullException("content");
-            if (siteUri == null) throw new ArgumentNullException("siteUri");
-            if (createSubject == null) throw new ArgumentNullException("createSubject");
-            if (createBody == null) throw new ArgumentNullException("createBody");
-
-            // build summary
-            var summary = new StringBuilder();
-
-            if (content.ContentType.VariesByNothing())
-            {
-                if (!_contentSettings.Notifications.DisableHtmlEmail)
-                {
-                    //create the HTML summary for invariant content
-
-                    //list all of the property values like we used to
-                    summary.Append("");
-                    foreach (var p in content.Properties)
-                    {
-                        // TODO: doesn't take into account variants
-
-                        var newText = p.GetValue() != null ? p.GetValue()?.ToString() : "";
-                        var oldText = newText;
-
-                        // check if something was changed and display the changes otherwise display the fields
-                        if (oldDoc?.Properties.Contains(p.PropertyType.Alias) ?? false)
-                        {
-                            var oldProperty = oldDoc.Properties[p.PropertyType.Alias];
-                            oldText = oldProperty?.GetValue() != null ? oldProperty.GetValue()?.ToString() : "";
-
-                            // replace HTML with char equivalent
-                            ReplaceHtmlSymbols(ref oldText);
-                            ReplaceHtmlSymbols(ref newText);
-                        }
-
-                        //show the values
-                        summary.Append("");
-                        summary.Append("");
-                        summary.Append("");
-                        summary.Append("");
-                    }
-                    summary.Append("
"); - summary.Append(p.PropertyType.Name); - summary.Append(""); - summary.Append(newText); - summary.Append("
"); - } - - } - else if (content.ContentType.VariesByCulture()) - { - //it's variant, so detect what cultures have changed - - if (!_contentSettings.Notifications.DisableHtmlEmail) - { - //Create the HTML based summary (ul of culture names) - - var culturesChanged = content.CultureInfos?.Values.Where(x => x.WasDirty()) - .Select(x => x.Culture) - .Select(_localizationService.GetLanguageByIsoCode) - .WhereNotNull() - .Select(x => x.CultureName); - summary.Append("
    "); - if (culturesChanged is not null) - { - foreach (var culture in culturesChanged) - { - summary.Append("
  • "); - summary.Append(culture); - summary.Append("
  • "); - } + prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id); } - summary.Append("
"); + // queue notification + NotificationRequest req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, siteUri, createSubject, createBody); + Enqueue(req); } - else + + // skip other notifications for this user, essentially this means moving i to the next index of notifications + // for the next user. + do { - //Create the text based summary (csv of culture names) - - var culturesChanged = string.Join(", ", content.CultureInfos!.Values.Where(x => x.WasDirty()) - .Select(x => x.Culture) - .Select(_localizationService.GetLanguageByIsoCode) - .WhereNotNull() - .Select(x => x.CultureName)); - - summary.Append("'"); - summary.Append(culturesChanged); - summary.Append("'"); + i++; } + while (i < notifications.Count && notifications[i].UserId == user.Id); + + if (i >= notifications.Count) + { + break; // break if no more notifications + } + } + + // load more users if any + id = users.Count == pagesz ? users.Last().Id + 1 : -1; + } + while (id > 0); + } + + /// + /// Gets the notifications for the user + /// + /// + /// + public IEnumerable? GetUserNotifications(IUser user) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true)) + { + return _notificationsRepository.GetUserNotifications(user); + } + } + + /// + /// Gets the notifications for the user based on the specified node path + /// + /// + /// + /// + /// + /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's + /// parent (ancestors) + /// + public IEnumerable? GetUserNotifications(IUser? user, string path) + { + if (user is null) + { + return null; + } + + IEnumerable? userNotifications = GetUserNotifications(user); + return FilterUserNotificationsByPath(userNotifications, path); + } + + /// + /// Deletes notifications by entity + /// + /// + public IEnumerable? GetEntityNotifications(IEntity entity) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true)) + { + return _notificationsRepository.GetEntityNotifications(entity); + } + } + + /// + /// Deletes notifications by entity + /// + /// + public void DeleteNotifications(IEntity entity) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + _notificationsRepository.DeleteNotifications(entity); + scope.Complete(); + } + } + + /// + /// Deletes notifications by user + /// + /// + public void DeleteNotifications(IUser user) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + _notificationsRepository.DeleteNotifications(user); + scope.Complete(); + } + } + + /// + /// Delete notifications by user and entity + /// + /// + /// + public void DeleteNotifications(IUser user, IEntity entity) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + _notificationsRepository.DeleteNotifications(user, entity); + scope.Complete(); + } + } + + /// + /// Sets the specific notifications for the user and entity + /// + /// + /// + /// + /// + /// This performs a full replace + /// + public IEnumerable? SetNotifications(IUser? user, IEntity entity, string[] actions) + { + if (user is null) + { + return null; + } + + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + IEnumerable notifications = _notificationsRepository.SetNotifications(user, entity, actions); + scope.Complete(); + return notifications; + } + } + + /// + /// Creates a new notification + /// + /// + /// + /// The action letter - note: this is a string for future compatibility + /// + public Notification CreateNotification(IUser user, IEntity entity, string action) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope()) + { + Notification notification = _notificationsRepository.CreateNotification(user, entity, action); + scope.Complete(); + return notification; + } + } + + /// + /// Filters a userNotifications collection by a path + /// + /// + /// + /// + public IEnumerable? FilterUserNotificationsByPath( + IEnumerable? userNotifications, + string path) + { + var pathParts = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return userNotifications + ?.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); + } + + /// + /// Gets the previous version to the latest version of the content item if there is one + /// + /// + /// + private IContentBase? GetPreviousVersion(int contentId) + { + // Regarding this: http://issues.umbraco.org/issue/U4-5180 + // we know they are descending from the service so we know that newest is first + // we are only selecting the top 2 rows since that is all we need + var allVersions = _contentService.GetVersionIds(contentId, 2).ToList(); + var prevVersionIndex = allVersions.Count > 1 ? 1 : 0; + return _contentService.GetVersion(allVersions[prevVersionIndex]); + } + + private IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType) + { + using (ICoreScope scope = _uowProvider.CreateCoreScope(autoComplete: true)) + { + return _notificationsRepository.GetUsersNotifications(userIds, action, nodeIds, objectType); + } + } + + /// + /// Replaces the HTML symbols with the character equivalent. + /// + /// The old string. + private static void ReplaceHtmlSymbols(ref string? oldString) + { + if (oldString.IsNullOrWhiteSpace()) + { + return; + } + + oldString = oldString!.Replace(" ", " "); + oldString = oldString.Replace("’", "'"); + oldString = oldString.Replace("&", "&"); + oldString = oldString.Replace("“", "“"); + oldString = oldString.Replace("”", "”"); + oldString = oldString.Replace(""", "\""); + } + + #region private methods + + /// + /// Sends the notification + /// + /// + /// + /// + /// + /// + /// The action readable name - currently an action is just a single letter, this is the name + /// associated with the letter + /// + /// + /// Callback to create the mail subject + /// Callback to create the mail body + private NotificationRequest CreateNotificationRequest( + IUser performingUser, + IUser mailingUser, + IContent content, + IContentBase? oldDoc, + string? actionName, + Uri siteUri, + Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject, + Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody) + { + if (performingUser == null) + { + throw new ArgumentNullException("performingUser"); + } + + if (mailingUser == null) + { + throw new ArgumentNullException("mailingUser"); + } + + if (content == null) + { + throw new ArgumentNullException("content"); + } + + if (siteUri == null) + { + throw new ArgumentNullException("siteUri"); + } + + if (createSubject == null) + { + throw new ArgumentNullException("createSubject"); + } + + if (createBody == null) + { + throw new ArgumentNullException("createBody"); + } + + // build summary + var summary = new StringBuilder(); + + if (content.ContentType.VariesByNothing()) + { + if (!_contentSettings.Notifications.DisableHtmlEmail) + { + // create the HTML summary for invariant content + + // list all of the property values like we used to + summary.Append(""); + foreach (IProperty p in content.Properties) + { + // TODO: doesn't take into account variants + var newText = p.GetValue() != null ? p.GetValue()?.ToString() : string.Empty; + var oldText = newText; + + // check if something was changed and display the changes otherwise display the fields + if (oldDoc?.Properties.Contains(p.PropertyType.Alias) ?? false) + { + IProperty? oldProperty = oldDoc.Properties[p.PropertyType.Alias]; + oldText = oldProperty?.GetValue() != null ? oldProperty.GetValue()?.ToString() : string.Empty; + + // replace HTML with char equivalent + ReplaceHtmlSymbols(ref oldText); + ReplaceHtmlSymbols(ref newText); + } + + // show the values + summary.Append(""); + summary.Append( + ""); + summary.Append(""); + summary.Append(""); + } + + summary.Append("
"); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(newText); + summary.Append("
"); + } + } + else if (content.ContentType.VariesByCulture()) + { + // it's variant, so detect what cultures have changed + if (!_contentSettings.Notifications.DisableHtmlEmail) + { + // Create the HTML based summary (ul of culture names) + IEnumerable? culturesChanged = content.CultureInfos?.Values.Where(x => x.WasDirty()) + .Select(x => x.Culture) + .Select(_localizationService.GetLanguageByIsoCode) + .WhereNotNull() + .Select(x => x.CultureName); + summary.Append("
    "); + if (culturesChanged is not null) + { + foreach (var culture in culturesChanged) + { + summary.Append("
  • "); + summary.Append(culture); + summary.Append("
  • "); + } + } + + summary.Append("
"); } else { - //not supported yet... - throw new NotSupportedException(); + // Create the text based summary (csv of culture names) + var culturesChanged = string.Join(", ", content.CultureInfos!.Values.Where(x => x.WasDirty()) + .Select(x => x.Culture) + .Select(_localizationService.GetLanguageByIsoCode) + .WhereNotNull() + .Select(x => x.CultureName)); + + summary.Append("'"); + summary.Append(culturesChanged); + summary.Append("'"); } + } + else + { + // not supported yet... + throw new NotSupportedException(); + } - var protocol = _globalSettings.UseHttps ? "https" : "http"; + var protocol = _globalSettings.UseHttps ? "https" : "http"; - var subjectVars = new NotificationEmailSubjectParams( - string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), - actionName, - content.Name); + var subjectVars = new NotificationEmailSubjectParams( + string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), + actionName, + content.Name); - var bodyVars = new NotificationEmailBodyParams( - mailingUser.Name, - actionName, - content.Name, - content.Id.ToString(CultureInfo.InvariantCulture), - string.Format("{2}://{0}/{1}", - string.Concat(siteUri.Authority), - // TODO: RE-enable this so we can have a nice URL - /*umbraco.library.NiceUrl(documentObject.Id))*/ - string.Concat(content.Id, ".aspx"), - protocol), - performingUser.Name, - string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), - summary.ToString()); + var bodyVars = new NotificationEmailBodyParams( + mailingUser.Name, + actionName, + content.Name, + content.Id.ToString(CultureInfo.InvariantCulture), + string.Format( + "{2}://{0}/{1}", + string.Concat(siteUri.Authority), - var fromMail = _contentSettings.Notifications.Email ?? _globalSettings.Smtp?.From; + // TODO: RE-enable this so we can have a nice URL + /*umbraco.library.NiceUrl(documentObject.Id))*/ + string.Concat(content.Id, ".aspx"), + protocol), + performingUser.Name, + string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), + summary.ToString()); - var subject = createSubject((mailingUser, subjectVars)); - var body = ""; - var isBodyHtml = false; + var fromMail = _contentSettings.Notifications.Email ?? _globalSettings.Smtp?.From; - if (_contentSettings.Notifications.DisableHtmlEmail) - { - body = createBody((user: mailingUser, body: bodyVars, false)); - } - else - { - isBodyHtml = true; - body = - string.Concat(@" + var subject = createSubject((mailingUser, subjectVars)); + var body = string.Empty; + var isBodyHtml = false; + + if (_contentSettings.Notifications.DisableHtmlEmail) + { + body = createBody((user: mailingUser, body: bodyVars, false)); + } + else + { + isBodyHtml = true; + body = + string.Concat( + @" -", createBody((user: mailingUser, body: bodyVars, true))); +", + createBody((user: mailingUser, body: bodyVars, true))); + } + + // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here + // adding the server name to make sure we don't replace external links + if (_globalSettings.UseHttps && string.IsNullOrEmpty(body) == false) + { + var serverName = siteUri.Host; + body = body.Replace( + $"http://{serverName}", + $"https://{serverName}"); + } + + // create the mail message + var mail = new EmailMessage(fromMail, mailingUser.Email, subject, body, isBodyHtml); + + return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email); + } + + private string ReplaceLinks(string text, Uri siteUri) + { + var sb = new StringBuilder(_globalSettings.UseHttps ? "https://" : "http://"); + sb.Append(siteUri.Authority); + sb.Append("/"); + var domain = sb.ToString(); + text = text.Replace("href=\"/", "href=\"" + domain); + text = text.Replace("src=\"/", "src=\"" + domain); + return text; + } + + private static readonly BlockingCollection Queue = new(); + private static volatile bool _running; + + private void Enqueue(NotificationRequest notification) + { + Queue.Add(notification); + if (_running) + { + return; + } + + lock (Locker) + { + if (_running) + { + return; } - // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here - // adding the server name to make sure we don't replace external links - if (_globalSettings.UseHttps && string.IsNullOrEmpty(body) == false) + Process(Queue); + _running = true; + } + } + + private void Process(BlockingCollection notificationRequests) => + ThreadPool.QueueUserWorkItem(state => + { + _logger.LogDebug("Begin processing notifications."); + while (true) { - var serverName = siteUri.Host; - body = body.Replace( - $"http://{serverName}", - $"https://{serverName}"); - } - - // create the mail message - var mail = new EmailMessage(fromMail, mailingUser.Email, subject, body, isBodyHtml); - - return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email); - } - - private string ReplaceLinks(string text, Uri siteUri) - { - var sb = new StringBuilder(_globalSettings.UseHttps ? "https://" : "http://"); - sb.Append(siteUri.Authority); - sb.Append("/"); - var domain = sb.ToString(); - text = text.Replace("href=\"/", "href=\"" + domain); - text = text.Replace("src=\"/", "src=\"" + domain); - return text; - } - - /// - /// Replaces the HTML symbols with the character equivalent. - /// - /// The old string. - private static void ReplaceHtmlSymbols(ref string? oldString) - { - if (oldString.IsNullOrWhiteSpace()) return; - oldString = oldString!.Replace(" ", " "); - oldString = oldString.Replace("’", "'"); - oldString = oldString.Replace("&", "&"); - oldString = oldString.Replace("“", "“"); - oldString = oldString.Replace("”", "”"); - oldString = oldString.Replace(""", "\""); - } - - // manage notifications - // ideally, would need to use IBackgroundTasks - but they are not part of Core! - - private static readonly object Locker = new object(); - private static readonly BlockingCollection Queue = new BlockingCollection(); - private static volatile bool _running; - - private void Enqueue(NotificationRequest notification) - { - Queue.Add(notification); - if (_running) return; - lock (Locker) - { - if (_running) return; - Process(Queue); - _running = true; - } - } - - private class NotificationRequest - { - public NotificationRequest(EmailMessage mail, string? action, string? userName, string? email) - { - Mail = mail; - Action = action; - UserName = userName; - Email = email; - } - - public EmailMessage Mail { get; } - - public string? Action { get; } - - public string? UserName { get; } - - public string? Email { get; } - } - - private void Process(BlockingCollection notificationRequests) - { - ThreadPool.QueueUserWorkItem(state => - { - _logger.LogDebug("Begin processing notifications."); - while (true) + // stay on for 8s + while (notificationRequests.TryTake(out NotificationRequest? request, 8 * 1000)) { - NotificationRequest? request; - while (notificationRequests.TryTake(out request, 8 * 1000)) // stay on for 8s + try { - try - { - _emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter().GetResult(); - _logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred sending notification"); - } + _emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter() + .GetResult(); + _logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email); } - lock (Locker) + catch (Exception ex) { - if (notificationRequests.Count > 0) continue; // last chance - _running = false; // going down - break; + _logger.LogError(ex, "An error occurred sending notification"); } } - _logger.LogDebug("Done processing notifications."); - }); + lock (Locker) + { + if (notificationRequests.Count > 0) + { + continue; // last chance + } + + _running = false; // going down + break; + } + } + + _logger.LogDebug("Done processing notifications."); + }); + + private class NotificationRequest + { + public NotificationRequest(EmailMessage mail, string? action, string? userName, string? email) + { + Mail = mail; + Action = action; + UserName = userName; + Email = email; } - #endregion + public EmailMessage Mail { get; } + + public string? Action { get; } + + public string? UserName { get; } + + public string? Email { get; } } + + #endregion } diff --git a/src/Umbraco.Core/Services/OperationResult.cs b/src/Umbraco.Core/Services/OperationResult.cs index a69dc6ee12..919077919c 100644 --- a/src/Umbraco.Core/Services/OperationResult.cs +++ b/src/Umbraco.Core/Services/OperationResult.cs @@ -1,246 +1,246 @@ -using System; using Umbraco.Cms.Core.Events; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +// TODO: no need for Attempt - the operation result SHOULD KNOW if it's a success or a failure! +// but then each WhateverResultType must + +/// +/// Represents the result of a service operation. +/// +/// The type of the result type. +/// +/// Type must be an enumeration, and its +/// underlying type must be byte. Values indicating success should be in the 0-127 +/// range, while values indicating failure should be in the 128-255 range. See +/// for a base implementation. +/// +public class OperationResult + where TResultType : struct { - // TODO: no need for Attempt - the operation result SHOULD KNOW if it's a success or a failure! - // but then each WhateverResultType must + static OperationResult() + { + // ensure that TResultType is an enum and the underlying type is byte + // so we can safely cast in Success and test against 128 for failures + Type type = typeof(TResultType); + if (type.IsEnum == false) + { + throw new InvalidOperationException($"Type {type} is not an enum."); + } + + if (Enum.GetUnderlyingType(type) != typeof(byte)) + { + throw new InvalidOperationException($"Enum {type} underlying type is not ."); + } + } /// - /// Represents the result of a service operation. + /// Initializes a new instance of the class. /// - /// The type of the result type. - /// Type must be an enumeration, and its - /// underlying type must be byte. Values indicating success should be in the 0-127 - /// range, while values indicating failure should be in the 128-255 range. See - /// for a base implementation. - public class OperationResult - where TResultType : struct + public OperationResult(TResultType result, EventMessages? eventMessages) { - /// - /// Initializes a new instance of the class. - /// - public OperationResult(TResultType result, EventMessages? eventMessages) - { - Result = result; - EventMessages = eventMessages; - } + Result = result; + EventMessages = eventMessages; + } - static OperationResult() - { - // ensure that TResultType is an enum and the underlying type is byte - // so we can safely cast in Success and test against 128 for failures - var type = typeof(TResultType); - if (type.IsEnum == false) - throw new InvalidOperationException($"Type {type} is not an enum."); - if (Enum.GetUnderlyingType(type) != typeof (byte)) - throw new InvalidOperationException($"Enum {type} underlying type is not ."); - } + /// + /// Gets a value indicating whether the operation was successful. + /// + public bool Success => ((byte)(object)Result & 128) == 0; // we *know* it's a byte - /// - /// Gets a value indicating whether the operation was successful. - /// - public bool Success => ((byte) (object) Result & 128) == 0; // we *know* it's a byte + /// + /// Gets the result of the operation. + /// + public TResultType Result { get; } - /// - /// Gets the result of the operation. - /// - public TResultType Result { get; } + /// + /// Gets the event messages produced by the operation. + /// + public EventMessages? EventMessages { get; } +} - /// - /// Gets the event messages produced by the operation. - /// - public EventMessages? EventMessages { get; } +/// +/// +/// Represents the result of a service operation for a given entity. +/// +/// The type of the result type. +/// The type of the entity. +/// +/// Type must be an enumeration, and its +/// underlying type must be byte. Values indicating success should be in the 0-127 +/// range, while values indicating failure should be in the 128-255 range. See +/// for a base implementation. +/// +public class OperationResult : OperationResult + where TResultType : struct +{ + /// + /// + /// Initializes a new instance of the class. + /// + /// The status of the operation. + /// Event messages produced by the operation. + public OperationResult(TResultType result, EventMessages eventMessages) + : base(result, eventMessages) + { } /// /// - /// Represents the result of a service operation for a given entity. + /// Initializes a new instance of the class. /// - /// The type of the result type. - /// The type of the entity. - /// Type must be an enumeration, and its - /// underlying type must be byte. Values indicating success should be in the 0-127 - /// range, while values indicating failure should be in the 128-255 range. See - /// for a base implementation. - public class OperationResult : OperationResult - where TResultType : struct - { - /// - /// - /// Initializes a new instance of the class. - /// - /// The status of the operation. - /// Event messages produced by the operation. - public OperationResult(TResultType result, EventMessages eventMessages) - : base(result, eventMessages) - { } + public OperationResult(TResultType result, EventMessages? eventMessages, TEntity? entity) + : base(result, eventMessages) => + Entity = entity; - /// - /// - /// Initializes a new instance of the class. - /// - public OperationResult(TResultType result, EventMessages? eventMessages, TEntity? entity) - : base(result, eventMessages) - { - Entity = entity; - } - - /// - /// Gets the entity. - /// - public TEntity? Entity { get; } - } + /// + /// Gets the entity. + /// + public TEntity? Entity { get; } +} +/// +/// +/// Represents the default operation result. +/// +public class OperationResult : OperationResult +{ /// /// - /// Represents the default operation result. + /// Initializes a new instance of the class with a status and event messages. /// - public class OperationResult : OperationResult + /// The status of the operation. + /// Event messages produced by the operation. + public OperationResult(OperationResultType result, EventMessages eventMessages) + : base(result, eventMessages) + { + } + + public static OperationResult Succeed(EventMessages eventMessages) => + new OperationResult(OperationResultType.Success, eventMessages); + + public static OperationResult Cancel(EventMessages eventMessages) => + new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages); + + // TODO: this exists to support services that still return Attempt + // these services should directly return an OperationResult, and then this static class should be deleted + public static class Attempt { - /// /// - /// Initializes a new instance of the class with a status and event messages. + /// Creates a successful operation attempt. /// - /// The status of the operation. - /// Event messages produced by the operation. - public OperationResult(OperationResultType result, EventMessages eventMessages) - : base(result, eventMessages) - { } + /// The event messages produced by the operation. + /// A new attempt instance. + public static Attempt Succeed(EventMessages eventMessages) => + Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages)); - public static OperationResult Succeed(EventMessages eventMessages) + public static Attempt?> + Succeed(EventMessages eventMessages) => Core.Attempt.Succeed( + new OperationResult(OperationResultType.Success, eventMessages)); + + public static Attempt?> + Succeed(EventMessages eventMessages, TValue value) => Core.Attempt.Succeed( + new OperationResult(OperationResultType.Success, eventMessages, value)); + + public static Attempt?> Succeed( + TStatusType statusType, + EventMessages eventMessages) + where TStatusType : struct => + Core.Attempt.Succeed(new OperationResult(statusType, eventMessages)); + + public static Attempt?> Succeed( + TStatusType statusType, EventMessages eventMessages, TValue value) + where TStatusType : struct => + Core.Attempt.Succeed(new OperationResult(statusType, eventMessages, value)); + + /// + /// Creates a successful operation attempt indicating that nothing was done. + /// + /// The event messages produced by the operation. + /// A new attempt instance. + public static Attempt NoOperation(EventMessages eventMessages) => + Core.Attempt.Succeed(new OperationResult(OperationResultType.NoOperation, eventMessages)); + + /// + /// Creates a failed operation attempt indicating that the operation has been cancelled. + /// + /// The event messages produced by the operation. + /// A new attempt instance. + public static Attempt Cancel(EventMessages eventMessages) => + Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages)); + + public static Attempt?> + Cancel(EventMessages eventMessages) => Core.Attempt.Fail( + new OperationResult( + OperationResultType.FailedCancelledByEvent, + eventMessages)); + + public static Attempt?> + Cancel(EventMessages eventMessages, TValue value) => Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages, value)); + + /// + /// Creates a failed operation attempt indicating that an exception was thrown during the operation. + /// + /// The event messages produced by the operation. + /// The exception that caused the operation to fail. + /// A new attempt instance. + public static Attempt Fail(EventMessages eventMessages, Exception exception) { - return new OperationResult(OperationResultType.Success, eventMessages); + eventMessages.Add(new EventMessage(string.Empty, exception.Message, EventMessageType.Error)); + return Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), + exception); } - public static OperationResult Cancel(EventMessages eventMessages) - { - return new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages); - } + public static Attempt?> + Fail(EventMessages eventMessages, Exception exception) => Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), + exception); - // TODO: this exists to support services that still return Attempt - // these services should directly return an OperationResult, and then this static class should be deleted - public static class Attempt - { - /// - /// Creates a successful operation attempt. - /// - /// The event messages produced by the operation. - /// A new attempt instance. - public static Attempt Succeed(EventMessages eventMessages) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages)); - public static Attempt?> Succeed(EventMessages eventMessages) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + Exception exception) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); - public static Attempt?> Succeed(EventMessages eventMessages, TValue value) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.Success, eventMessages, value)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages)); - public static Attempt?> Succeed(TStatusType statusType, EventMessages eventMessages) - where TStatusType : struct - { - return Core.Attempt.Succeed(new OperationResult(statusType, eventMessages)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + TValue value) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value)); - public static Attempt?> Succeed(TStatusType statusType, EventMessages eventMessages, TValue value) - where TStatusType : struct - { - return Core.Attempt.Succeed(new OperationResult(statusType, eventMessages, value)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + Exception exception) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); - /// - /// Creates a successful operation attempt indicating that nothing was done. - /// - /// The event messages produced by the operation. - /// A new attempt instance. - public static Attempt NoOperation(EventMessages eventMessages) - { - return Core.Attempt.Succeed(new OperationResult(OperationResultType.NoOperation, eventMessages)); - } + public static Attempt?> Fail( + TStatusType statusType, + EventMessages eventMessages, + TValue value, + Exception exception) + where TStatusType : struct => + Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value), exception); - /// - /// Creates a failed operation attempt indicating that the operation has been cancelled. - /// - /// The event messages produced by the operation. - /// A new attempt instance. - public static Attempt Cancel(EventMessages eventMessages) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages)); - } - - public static Attempt?> Cancel(EventMessages eventMessages) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages)); - } - - public static Attempt?> Cancel(EventMessages eventMessages, TValue value) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages, value)); - } - - /// - /// Creates a failed operation attempt indicating that an exception was thrown during the operation. - /// - /// The event messages produced by the operation. - /// The exception that caused the operation to fail. - /// A new attempt instance. - public static Attempt Fail(EventMessages eventMessages, Exception exception) - { - eventMessages.Add(new EventMessage("", exception.Message, EventMessageType.Error)); - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), exception); - } - - public static Attempt?> Fail(EventMessages eventMessages, Exception exception) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedExceptionThrown, eventMessages), exception); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages)); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages, Exception exception) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages)); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages, TValue value) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value)); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages, Exception exception) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages), exception); - } - - public static Attempt?> Fail(TStatusType statusType, EventMessages eventMessages, TValue value, Exception exception) - where TStatusType : struct - { - return Core.Attempt.Fail(new OperationResult(statusType, eventMessages, value), exception); - } - - public static Attempt?> Cannot(EventMessages eventMessages) - { - return Core.Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, eventMessages)); - } - } + public static Attempt?> + Cannot(EventMessages eventMessages) => Core.Attempt.Fail( + new OperationResult(OperationResultType.FailedCannot, eventMessages)); } } diff --git a/src/Umbraco.Core/Services/OperationResultType.cs b/src/Umbraco.Core/Services/OperationResultType.cs index 15b332e43c..c87b70c2a2 100644 --- a/src/Umbraco.Core/Services/OperationResultType.cs +++ b/src/Umbraco.Core/Services/OperationResultType.cs @@ -1,45 +1,44 @@ -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// A value indicating the result of an operation. +/// +public enum OperationResultType : byte { + // all "ResultType" enum's must be byte-based, and declare Failed = 128, and declare + // every failure codes as >128 - see OperationResult and OperationResultType for details. + /// - /// A value indicating the result of an operation. + /// The operation was successful. /// - public enum OperationResultType : byte - { - // all "ResultType" enum's must be byte-based, and declare Failed = 128, and declare - // every failure codes as >128 - see OperationResult and OperationResultType for details. + Success = 0, - /// - /// The operation was successful. - /// - Success = 0, + /// + /// The operation failed. + /// + /// All values above this value indicate a failure. + Failed = 128, - /// - /// The operation failed. - /// - /// All values above this value indicate a failure. - Failed = 128, + /// + /// The operation could not complete because of invalid preconditions (eg creating a reference + /// to an item that does not exist). + /// + FailedCannot = Failed | 2, - /// - /// The operation could not complete because of invalid preconditions (eg creating a reference - /// to an item that does not exist). - /// - FailedCannot = Failed | 2, + /// + /// The operation has been cancelled by an event handler. + /// + FailedCancelledByEvent = Failed | 4, - /// - /// The operation has been cancelled by an event handler. - /// - FailedCancelledByEvent = Failed | 4, + /// + /// The operation could not complete due to an exception. + /// + FailedExceptionThrown = Failed | 5, - /// - /// The operation could not complete due to an exception. - /// - FailedExceptionThrown = Failed | 5, + /// + /// No operation has been executed because it was not needed (eg deleting an item that doesn't exist). + /// + NoOperation = Failed | 6, // TODO: shouldn't it be a success? - /// - /// No operation has been executed because it was not needed (eg deleting an item that doesn't exist). - /// - NoOperation = Failed | 6, // TODO: shouldn't it be a success? - - // TODO: In the future, we might need to add more operations statuses, potentially like 'FailedByPermissions', etc... - } + // TODO: In the future, we might need to add more operations statuses, potentially like 'FailedByPermissions', etc... } diff --git a/src/Umbraco.Core/Services/Ordering.cs b/src/Umbraco.Core/Services/Ordering.cs index 513654428b..39c89e5c4a 100644 --- a/src/Umbraco.Core/Services/Ordering.cs +++ b/src/Umbraco.Core/Services/Ordering.cs @@ -1,81 +1,92 @@ using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Represents ordering information. +/// +public class Ordering { + private static readonly Ordering DefaultOrdering = new(null); + /// - /// Represents ordering information. + /// Initializes a new instance of the class. /// - public class Ordering + /// The name of the ordering field. + /// The ordering direction. + /// The (ISO) culture to consider when sorting multi-lingual fields. + /// A value indicating whether the ordering field is a custom user property. + /// + /// + /// The can be null, meaning: not sorting. If it is the empty string, it becomes + /// null. + /// + /// + /// The can be the empty string, meaning: invariant. If it is null, it becomes the + /// empty string. + /// + /// + public Ordering(string? orderBy, Direction direction = Direction.Ascending, string? culture = null, bool isCustomField = false) { - private static readonly Ordering DefaultOrdering = new Ordering(null); - - /// - /// Initializes a new instance of the class. - /// - /// The name of the ordering field. - /// The ordering direction. - /// The (ISO) culture to consider when sorting multi-lingual fields. - /// A value indicating whether the ordering field is a custom user property. - /// - /// The can be null, meaning: not sorting. If it is the empty string, it becomes null. - /// The can be the empty string, meaning: invariant. If it is null, it becomes the empty string. - /// - public Ordering(string? orderBy, Direction direction = Direction.Ascending, string? culture = null, bool isCustomField = false) - { - OrderBy = orderBy.IfNullOrWhiteSpace(null); // empty is null and means, not sorting - Direction = direction; - Culture = culture.IfNullOrWhiteSpace(string.Empty); // empty is "" and means, invariant - IsCustomField = isCustomField; - } - - /// - /// Creates a new instance of the class. - /// - /// The name of the ordering field. - /// The ordering direction. - /// The (ISO) culture to consider when sorting multi-lingual fields. - /// A value indicating whether the ordering field is a custom user property. - /// - /// The can be null, meaning: not sorting. If it is the empty string, it becomes null. - /// The can be the empty string, meaning: invariant. If it is null, it becomes the empty string. - /// - public static Ordering By(string orderBy, Direction direction = Direction.Ascending, string? culture = null, bool isCustomField = false) - => new Ordering(orderBy, direction, culture, isCustomField); - - /// - /// Gets the default instance. - /// - public static Ordering ByDefault() - => DefaultOrdering; - - /// - /// Gets the name of the ordering field. - /// - public string? OrderBy { get; } - - /// - /// Gets the ordering direction. - /// - public Direction Direction { get; } - - /// - /// Gets (ISO) culture to consider when sorting multi-lingual fields. - /// - public string? Culture { get; } - - /// - /// Gets a value indicating whether the ordering field is a custom user property. - /// - public bool IsCustomField { get; } - - /// - /// Gets a value indicating whether this ordering is the default ordering. - /// - public bool IsEmpty => this == DefaultOrdering || OrderBy == null; - - /// - /// Gets a value indicating whether the culture of this ordering is invariant. - /// - public bool IsInvariant => this == DefaultOrdering || Culture == string.Empty; + OrderBy = orderBy.IfNullOrWhiteSpace(null); // empty is null and means, not sorting + Direction = direction; + Culture = culture.IfNullOrWhiteSpace(string.Empty); // empty is "" and means, invariant + IsCustomField = isCustomField; } + + /// + /// Gets the name of the ordering field. + /// + public string? OrderBy { get; } + + /// + /// Gets the ordering direction. + /// + public Direction Direction { get; } + + /// + /// Gets (ISO) culture to consider when sorting multi-lingual fields. + /// + public string? Culture { get; } + + /// + /// Gets a value indicating whether the ordering field is a custom user property. + /// + public bool IsCustomField { get; } + + /// + /// Gets a value indicating whether this ordering is the default ordering. + /// + public bool IsEmpty => this == DefaultOrdering || OrderBy == null; + + /// + /// Gets a value indicating whether the culture of this ordering is invariant. + /// + public bool IsInvariant => this == DefaultOrdering || Culture == string.Empty; + + /// + /// Creates a new instance of the class. + /// + /// The name of the ordering field. + /// The ordering direction. + /// The (ISO) culture to consider when sorting multi-lingual fields. + /// A value indicating whether the ordering field is a custom user property. + /// + /// + /// The can be null, meaning: not sorting. If it is the empty string, it becomes + /// null. + /// + /// + /// The can be the empty string, meaning: invariant. If it is null, it becomes the + /// empty string. + /// + /// + public static Ordering By(string orderBy, Direction direction = Direction.Ascending, string? culture = null, bool isCustomField = false) + => new(orderBy, direction, culture, isCustomField); + + /// + /// Gets the default instance. + /// + public static Ordering ByDefault() + => DefaultOrdering; } diff --git a/src/Umbraco.Core/Services/ProcessInstructionsResult.cs b/src/Umbraco.Core/Services/ProcessInstructionsResult.cs index 9a368dab7e..39751dad61 100644 --- a/src/Umbraco.Core/Services/ProcessInstructionsResult.cs +++ b/src/Umbraco.Core/Services/ProcessInstructionsResult.cs @@ -1,26 +1,29 @@ -using System; +namespace Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Services +/// +/// Defines a result object for the +/// operation. +/// +public class ProcessInstructionsResult { - /// - /// Defines a result object for the operation. - /// - public class ProcessInstructionsResult + private ProcessInstructionsResult() { - private ProcessInstructionsResult() + } + + public int NumberOfInstructionsProcessed { get; private set; } + + public int LastId { get; private set; } + + public bool InstructionsWerePruned { get; private set; } + + public static ProcessInstructionsResult AsCompleted(int numberOfInstructionsProcessed, int lastId) => + new() { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId }; + + public static ProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => + new() { - } - - public int NumberOfInstructionsProcessed { get; private set; } - - public int LastId { get; private set; } - - public bool InstructionsWerePruned { get; private set; } - - public static ProcessInstructionsResult AsCompleted(int numberOfInstructionsProcessed, int lastId) => - new ProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId }; - - public static ProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => - new ProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId, InstructionsWerePruned = true }; - }; + NumberOfInstructionsProcessed = numberOfInstructionsProcessed, + LastId = lastId, + InstructionsWerePruned = true, + }; } diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index c5a4312776..d93cbd4a7c 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -1,198 +1,218 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; +using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class PropertyValidationService : IPropertyValidationService { - public class PropertyValidationService : IPropertyValidationService + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ILocalizedTextService _textService; + private readonly IValueEditorCache _valueEditorCache; + + public PropertyValidationService( + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + ILocalizedTextService textService, + IValueEditorCache valueEditorCache) { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly ILocalizedTextService _textService; - private readonly IValueEditorCache _valueEditorCache; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _textService = textService; + _valueEditorCache = valueEditorCache; + } - public PropertyValidationService( - PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, - ILocalizedTextService textService, - IValueEditorCache valueEditorCache) + /// + public IEnumerable ValidatePropertyValue( + IPropertyType propertyType, + object? postedValue) + { + if (propertyType is null) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _textService = textService; - _valueEditorCache = valueEditorCache; + throw new ArgumentNullException(nameof(propertyType)); } - /// - public IEnumerable ValidatePropertyValue( - IPropertyType propertyType, - object? postedValue) + IDataType? dataType = _dataTypeService.GetDataType(propertyType.DataTypeId); + if (dataType == null) { - if (propertyType is null) throw new ArgumentNullException(nameof(propertyType)); - var dataType = _dataTypeService.GetDataType(propertyType.DataTypeId); - if (dataType == null) throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId); - - var editor = _propertyEditors[propertyType.PropertyEditorAlias]; - if (editor == null) throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias); - - return ValidatePropertyValue(editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage); + throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId); } - /// - public IEnumerable ValidatePropertyValue( - IDataEditor editor, - IDataType dataType, - object? postedValue, - bool isRequired, - string? validationRegExp, - string? isRequiredMessage, - string? validationRegExpMessage) + IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (editor == null) { - // Retrieve default messages used for required and regex validatation. We'll replace these - // if set with custom ones if they've been provided for a given property. - var requiredDefaultMessages = new[] - { - _textService.Localize("validation", "invalidNull"), - _textService.Localize("validation", "invalidEmpty") - }; - var formatDefaultMessages = new[] - { - _textService.Localize("validation", "invalidPattern"), - }; + throw new InvalidOperationException("No property editor found by alias " + + propertyType.PropertyEditorAlias); + } - IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType); - foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp)) + return ValidatePropertyValue(editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage); + } + + /// + public IEnumerable ValidatePropertyValue( + IDataEditor editor, + IDataType dataType, + object? postedValue, + bool isRequired, + string? validationRegExp, + string? isRequiredMessage, + string? validationRegExpMessage) + { + // Retrieve default messages used for required and regex validatation. We'll replace these + // if set with custom ones if they've been provided for a given property. + var requiredDefaultMessages = new[] + { + _textService.Localize("validation", "invalidNull"), _textService.Localize("validation", "invalidEmpty"), + }; + var formatDefaultMessages = new[] { _textService.Localize("validation", "invalidPattern") }; + + IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType); + foreach (ValidationResult validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp)) + { + // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). + if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && + requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) { - // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). - if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) - { - validationResult.ErrorMessage = isRequiredMessage; - } - if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) - { - validationResult.ErrorMessage = validationRegExpMessage; - } - yield return validationResult; - } - } - - /// - public bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact) - { - // select invalid properties - invalidProperties = content.Properties.Where(x => - { - var propertyTypeVaries = x.PropertyType.VariesByCulture(); - - if (impact is null) - { - return false; - } - // impacts invariant = validate invariant property, invariant culture - if (impact.ImpactsOnlyInvariantCulture) - return !(propertyTypeVaries || IsPropertyValid(x, null)); - - // impacts all = validate property, all cultures (incl. invariant) - if (impact.ImpactsAllCultures) - return !IsPropertyValid(x); - - // impacts explicit culture = validate variant property, explicit culture - if (propertyTypeVaries) - return !IsPropertyValid(x, impact.Culture); - - // and, for explicit culture, we may also have to validate invariant property, invariant culture - // if either - // - it is impacted (default culture), or - // - there is no published version of the content - maybe non-default culture, but no published version - - var alsoInvariant = impact.ImpactsAlsoInvariantProperties || !content.Published; - return alsoInvariant && !IsPropertyValid(x, null); - - }).ToArray(); - - return invalidProperties.Length == 0; - } - - /// - public bool IsPropertyValid(IProperty property, string? culture = "*", string? segment = "*") - { - //NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there. - // The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing. - - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); - - IPropertyValue? pvalue = null; - - // if validating invariant/neutral, and it is supported, validate - // (including ensuring that the value exists, if mandatory) - if ((culture == null || culture == "*") && (segment == null || segment == "*") && property.PropertyType.SupportsVariation(null, null)) - { - // validate pvalue (which is the invariant value) - pvalue = property.Values.FirstOrDefault(x => x.Culture == null && x.Segment == null); - if (!IsValidPropertyValue(property, pvalue?.EditedValue)) - return false; + validationResult.ErrorMessage = isRequiredMessage; } - // if validating only invariant/neutral, we are good - if (culture == null && segment == null) - return true; - - // if nothing else to validate, we are good - if ((culture == null || culture == "*") && (segment == null || segment == "*") && !property.PropertyType.VariesByCulture()) - return true; - - // for anything else, validate the existing values (including mandatory), - // but we cannot validate mandatory globally (we don't know the possible cultures and segments) - - // validate vvalues (which are the variant values) - - // if we don't have vvalues (property.Values is empty or only contains pvalue), validate null - if (property.Values.Count == (pvalue == null ? 0 : 1)) - return culture == "*" || IsValidPropertyValue(property, null); - - // else validate vvalues (but don't revalidate pvalue) - var pvalues = property.Values.Where(x => - x != pvalue && // don't revalidate pvalue - property.PropertyType.SupportsVariation(x.Culture, x.Segment, true) && // the value variation is ok - (culture == "*" || (x.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches - (segment == "*" || (x.Segment?.InvariantEquals(segment) ?? false))) // the segment matches - .ToList(); - - return pvalues.Count == 0 || pvalues.All(x => IsValidPropertyValue(property, x.EditedValue)); - } - - /// - /// Boolean indicating whether the passed in value is valid - /// - /// - /// - /// True is property value is valid, otherwise false - private bool IsValidPropertyValue(IProperty property, object? value) - { - return IsPropertyValueValid(property.PropertyType, value); - } - - /// - /// Determines whether a value is valid for this property type. - /// - private bool IsPropertyValueValid(IPropertyType propertyType, object? value) - { - var editor = _propertyEditors[propertyType.PropertyEditorAlias]; - if (editor == null) + if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && + formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) { - // nothing much we can do validation wise if the property editor has been removed. - // the property will be displayed as a label, so flagging it as invalid would be pointless. - return true; + validationResult.ErrorMessage = validationRegExpMessage; } - var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId)?.Configuration; - var valueEditor = editor.GetValueEditor(configuration); - return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); + + yield return validationResult; } } + + /// + public bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact) + { + // select invalid properties + invalidProperties = content.Properties.Where(x => + { + var propertyTypeVaries = x.PropertyType.VariesByCulture(); + + if (impact is null) + { + return false; + } + + // impacts invariant = validate invariant property, invariant culture + if (impact.ImpactsOnlyInvariantCulture) + { + return !(propertyTypeVaries || IsPropertyValid(x, null)); + } + + // impacts all = validate property, all cultures (incl. invariant) + if (impact.ImpactsAllCultures) + { + return !IsPropertyValid(x); + } + + // impacts explicit culture = validate variant property, explicit culture + if (propertyTypeVaries) + { + return !IsPropertyValid(x, impact.Culture); + } + + // and, for explicit culture, we may also have to validate invariant property, invariant culture + // if either + // - it is impacted (default culture), or + // - there is no published version of the content - maybe non-default culture, but no published version + var alsoInvariant = impact.ImpactsAlsoInvariantProperties || !content.Published; + return alsoInvariant && !IsPropertyValid(x, null); + }).ToArray(); + + return invalidProperties.Length == 0; + } + + /// + public bool IsPropertyValid(IProperty property, string? culture = "*", string? segment = "*") + { + // NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there. + // The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing. + culture = culture?.NullOrWhiteSpaceAsNull(); + segment = segment?.NullOrWhiteSpaceAsNull(); + + IPropertyValue? pvalue = null; + + // if validating invariant/neutral, and it is supported, validate + // (including ensuring that the value exists, if mandatory) + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + property.PropertyType.SupportsVariation(null, null)) + { + // validate pvalue (which is the invariant value) + pvalue = property.Values.FirstOrDefault(x => x.Culture == null && x.Segment == null); + if (!IsValidPropertyValue(property, pvalue?.EditedValue)) + { + return false; + } + } + + // if validating only invariant/neutral, we are good + if (culture == null && segment == null) + { + return true; + } + + // if nothing else to validate, we are good + if ((culture == null || culture == "*") && (segment == null || segment == "*") && + !property.PropertyType.VariesByCulture()) + { + return true; + } + + // for anything else, validate the existing values (including mandatory), + // but we cannot validate mandatory globally (we don't know the possible cultures and segments) + + // validate vvalues (which are the variant values) + + // if we don't have vvalues (property.Values is empty or only contains pvalue), validate null + if (property.Values.Count == (pvalue == null ? 0 : 1)) + { + return culture == "*" || IsValidPropertyValue(property, null); + } + + // else validate vvalues (but don't revalidate pvalue) + var pvalues = property.Values.Where(x => + x != pvalue && // don't revalidate pvalue + property.PropertyType.SupportsVariation(x.Culture, x.Segment, true) && // the value variation is ok + (culture == "*" || (x.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches + (segment == "*" || (x.Segment?.InvariantEquals(segment) ?? false))) // the segment matches + .ToList(); + + return pvalues.Count == 0 || pvalues.All(x => IsValidPropertyValue(property, x.EditedValue)); + } + + /// + /// Boolean indicating whether the passed in value is valid + /// + /// + /// + /// True is property value is valid, otherwise false + private bool IsValidPropertyValue(IProperty property, object? value) => + IsPropertyValueValid(property.PropertyType, value); + + /// + /// Determines whether a value is valid for this property type. + /// + private bool IsPropertyValueValid(IPropertyType propertyType, object? value) + { + IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (editor == null) + { + // nothing much we can do validation wise if the property editor has been removed. + // the property will be displayed as a label, so flagging it as invalid would be pointless. + return true; + } + + var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId)?.Configuration; + IDataValueEditor valueEditor = editor.GetValueEditor(configuration); + return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); + } } diff --git a/src/Umbraco.Core/Services/PublicAccessService.cs b/src/Umbraco.Core/Services/PublicAccessService.cs index b6216e4b58..6f3de02c55 100644 --- a/src/Umbraco.Core/Services/PublicAccessService.cs +++ b/src/Umbraco.Core/Services/PublicAccessService.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -10,228 +7,243 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +internal class PublicAccessService : RepositoryService, IPublicAccessService { - internal class PublicAccessService : RepositoryService, IPublicAccessService + private readonly IPublicAccessRepository _publicAccessRepository; + + public PublicAccessService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IPublicAccessRepository publicAccessRepository) + : base(provider, loggerFactory, eventMessagesFactory) => + _publicAccessRepository = publicAccessRepository; + + /// + /// Gets all defined entries and associated rules + /// + /// + public IEnumerable GetAll() { - private readonly IPublicAccessRepository _publicAccessRepository; - - public PublicAccessService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IPublicAccessRepository publicAccessRepository) - : base(provider, loggerFactory, eventMessagesFactory) + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - _publicAccessRepository = publicAccessRepository; - } - - /// - /// Gets all defined entries and associated rules - /// - /// - public IEnumerable GetAll() - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _publicAccessRepository.GetMany(); - } - } - - /// - /// Gets the entry defined for the content item's path - /// - /// - /// Returns null if no entry is found - public PublicAccessEntry? GetEntryForContent(IContent content) - { - return GetEntryForContent(content.Path.EnsureEndsWith("," + content.Id)); - } - - /// - /// Gets the entry defined for the content item based on a content path - /// - /// - /// Returns null if no entry is found - /// - /// NOTE: This method get's called *very* often! This will return the results from cache - /// - public PublicAccessEntry? GetEntryForContent(string contentPath) - { - //Get all ids in the path for the content item and ensure they all - // parse to ints that are not -1. - var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out int val) ? val : -1) - .Where(x => x != -1) - .ToList(); - - //start with the deepest id - ids.Reverse(); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - //This will retrieve from cache! - var entries = _publicAccessRepository.GetMany().ToList(); - foreach (var id in ids) - { - var found = entries.FirstOrDefault(x => x.ProtectedNodeId == id); - if (found != null) return found; - } - } - - return null; - } - - /// - /// Returns true if the content has an entry for it's path - /// - /// - /// - public Attempt IsProtected(IContent content) - { - var result = GetEntryForContent(content); - return Attempt.If(result != null, result); - } - - /// - /// Returns true if the content has an entry based on a content path - /// - /// - /// - public Attempt IsProtected(string contentPath) - { - var result = GetEntryForContent(contentPath); - return Attempt.If(result != null, result); - } - - /// - /// Adds a rule - /// - /// - /// - /// - /// - public Attempt?> AddRule(IContent content, string ruleType, string ruleValue) - { - var evtMsgs = EventMessagesFactory.Get(); - PublicAccessEntry? entry; - using (var scope = ScopeProvider.CreateCoreScope()) - { - entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); - if (entry == null) - return OperationResult.Attempt.Cannot(evtMsgs); // causes rollback - - var existingRule = entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); - if (existingRule == null) - { - entry.AddRule(ruleValue, ruleType); - } - else - { - //If they are both the same already then there's nothing to update, exit - return OperationResult.Attempt.Succeed(evtMsgs, entry); - } - - var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotifiation)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs, entry); - } - - _publicAccessRepository.Save(entry); - - scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - } - - return OperationResult.Attempt.Succeed(evtMsgs, entry); - } - - /// - /// Removes a rule - /// - /// - /// - /// - public Attempt RemoveRule(IContent content, string ruleType, string ruleValue) - { - var evtMsgs = EventMessagesFactory.Get(); - PublicAccessEntry? entry; - using (var scope = ScopeProvider.CreateCoreScope()) - { - entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); - if (entry == null) return Attempt.Fail(); // causes rollback // causes rollback - - var existingRule = entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); - if (existingRule == null) return Attempt.Fail(); // causes rollback // causes rollback - - entry.RemoveRule(existingRule); - - var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotifiation)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } - - _publicAccessRepository.Save(entry); - scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - } - - return OperationResult.Attempt.Succeed(evtMsgs); - } - - /// - /// Saves the entry - /// - /// - public Attempt Save(PublicAccessEntry entry) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotifiation)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } - - _publicAccessRepository.Save(entry); - scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); - } - - return OperationResult.Attempt.Succeed(evtMsgs); - } - - /// - /// Deletes the entry and all associated rules - /// - /// - public Attempt Delete(PublicAccessEntry entry) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var deletingNotification = new PublicAccessEntryDeletingNotification(entry, evtMsgs); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } - - _publicAccessRepository.Delete(entry); - scope.Complete(); - - scope.Notifications.Publish(new PublicAccessEntryDeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); - } - - return OperationResult.Attempt.Succeed(evtMsgs); + return _publicAccessRepository.GetMany(); } } + + /// + /// Gets the entry defined for the content item's path + /// + /// + /// Returns null if no entry is found + public PublicAccessEntry? GetEntryForContent(IContent content) => + GetEntryForContent(content.Path.EnsureEndsWith("," + content.Id)); + + /// + /// Gets the entry defined for the content item based on a content path + /// + /// + /// Returns null if no entry is found + /// + /// NOTE: This method get's called *very* often! This will return the results from cache + /// + public PublicAccessEntry? GetEntryForContent(string contentPath) + { + // Get all ids in the path for the content item and ensure they all + // parse to ints that are not -1. + var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val) ? val : -1) + .Where(x => x != -1) + .ToList(); + + // start with the deepest id + ids.Reverse(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + // This will retrieve from cache! + var entries = _publicAccessRepository.GetMany().ToList(); + foreach (var id in ids) + { + PublicAccessEntry? found = entries.FirstOrDefault(x => x.ProtectedNodeId == id); + if (found != null) + { + return found; + } + } + } + + return null; + } + + /// + /// Returns true if the content has an entry for it's path + /// + /// + /// + public Attempt IsProtected(IContent content) + { + PublicAccessEntry? result = GetEntryForContent(content); + return Attempt.If(result != null, result); + } + + /// + /// Returns true if the content has an entry based on a content path + /// + /// + /// + public Attempt IsProtected(string contentPath) + { + PublicAccessEntry? result = GetEntryForContent(contentPath); + return Attempt.If(result != null, result); + } + + /// + /// Adds a rule + /// + /// + /// + /// + /// + public Attempt?> AddRule(IContent content, string ruleType, string ruleValue) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + PublicAccessEntry? entry; + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); + if (entry == null) + { + return OperationResult.Attempt.Cannot(evtMsgs); // causes rollback + } + + PublicAccessRule? existingRule = + entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); + if (existingRule == null) + { + entry.AddRule(ruleValue, ruleType); + } + else + { + // If they are both the same already then there's nothing to update, exit + return OperationResult.Attempt.Succeed(evtMsgs, entry); + } + + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs, entry); + } + + _publicAccessRepository.Save(entry); + + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + } + + return OperationResult.Attempt.Succeed(evtMsgs, entry); + } + + /// + /// Removes a rule + /// + /// + /// + /// + public Attempt RemoveRule(IContent content, string ruleType, string ruleValue) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + PublicAccessEntry? entry; + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + entry = _publicAccessRepository.GetMany().FirstOrDefault(x => x.ProtectedNodeId == content.Id); + if (entry == null) + { + return Attempt.Fail(); // causes rollback // causes rollback + } + + PublicAccessRule? existingRule = + entry.Rules.FirstOrDefault(x => x.RuleType == ruleType && x.RuleValue == ruleValue); + if (existingRule == null) + { + return Attempt.Fail(); // causes rollback // causes rollback + } + + entry.RemoveRule(existingRule); + + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs); + } + + _publicAccessRepository.Save(entry); + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + } + + return OperationResult.Attempt.Succeed(evtMsgs); + } + + /// + /// Saves the entry + /// + /// + public Attempt Save(PublicAccessEntry entry) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var savingNotifiation = new PublicAccessEntrySavingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotifiation)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs); + } + + _publicAccessRepository.Save(entry); + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntrySavedNotification(entry, evtMsgs).WithStateFrom(savingNotifiation)); + } + + return OperationResult.Attempt.Succeed(evtMsgs); + } + + /// + /// Deletes the entry and all associated rules + /// + /// + public Attempt Delete(PublicAccessEntry entry) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var deletingNotification = new PublicAccessEntryDeletingNotification(entry, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs); + } + + _publicAccessRepository.Delete(entry); + scope.Complete(); + + scope.Notifications.Publish( + new PublicAccessEntryDeletedNotification(entry, evtMsgs).WithStateFrom(deletingNotification)); + } + + return OperationResult.Attempt.Succeed(evtMsgs); + } } diff --git a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs index d8a7f201de..eb42dcda73 100644 --- a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs +++ b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs @@ -1,105 +1,125 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for the IPublicAccessService +/// +public static class PublicAccessServiceExtensions { - /// - /// Extension methods for the IPublicAccessService - /// - public static class PublicAccessServiceExtensions + public static bool RenameMemberGroupRoleRules(this IPublicAccessService publicAccessService, string? oldRolename, string? newRolename) { - public static bool RenameMemberGroupRoleRules(this IPublicAccessService publicAccessService, string? oldRolename, string? newRolename) + var hasChange = false; + if (oldRolename == newRolename) { - var hasChange = false; - if (oldRolename == newRolename) return false; - - var allEntries = publicAccessService.GetAll(); - - foreach (var entry in allEntries) - { - //get rules that match - var roleRules = entry.Rules - .Where(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) - .Where(x => x.RuleValue == oldRolename); - var save = false; - foreach (var roleRule in roleRules) - { - //a rule is being updated so flag this entry to be saved - roleRule.RuleValue = newRolename ?? String.Empty; - save = true; - } - if (save) - { - hasChange = true; - publicAccessService.Save(entry); - } - } - - return hasChange; + return false; } - public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, string username, IEnumerable currentMemberRoles) + IEnumerable allEntries = publicAccessService.GetAll(); + + foreach (PublicAccessEntry entry in allEntries) { - var content = contentService.GetById(documentId); - if (content == null) return true; - - var entry = publicAccessService.GetEntryForContent(content); - if (entry == null) return true; - - return HasAccess(entry, username, currentMemberRoles); - } - - /// - /// Checks if the member with the specified username has access to the path which is also based on the passed in roles for the member - /// - /// - /// - /// - /// A callback to retrieve the roles for this member - /// - public static async Task HasAccessAsync(this IPublicAccessService publicAccessService, string path, string username, Func>> rolesCallback) - { - if (rolesCallback == null) throw new ArgumentNullException("roles"); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username"); - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); - - var entry = publicAccessService.GetEntryForContent(path.EnsureEndsWith(path)); - if (entry == null) return true; - - var roles = await rolesCallback(); - - return HasAccess(entry, username, roles); - } - - private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) - { - if (entry is null) + // get rules that match + IEnumerable roleRules = entry.Rules + .Where(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) + .Where(x => x.RuleValue == oldRolename); + var save = false; + foreach (PublicAccessRule roleRule in roleRules) { - throw new ArgumentNullException(nameof(entry)); + // a rule is being updated so flag this entry to be saved + roleRule.RuleValue = newRolename ?? string.Empty; + save = true; } - if (string.IsNullOrEmpty(username)) + if (save) { - throw new ArgumentException($"'{nameof(username)}' cannot be null or empty.", nameof(username)); + hasChange = true; + publicAccessService.Save(entry); } - - if (roles is null) - { - throw new ArgumentNullException(nameof(roles)); - } - - return entry.Rules.Any(x => - (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) - || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue)) - ); } + + return hasChange; + } + + public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, string username, IEnumerable currentMemberRoles) + { + IContent? content = contentService.GetById(documentId); + if (content == null) + { + return true; + } + + PublicAccessEntry? entry = publicAccessService.GetEntryForContent(content); + if (entry == null) + { + return true; + } + + return HasAccess(entry, username, currentMemberRoles); + } + + /// + /// Checks if the member with the specified username has access to the path which is also based on the passed in roles + /// for the member + /// + /// + /// + /// + /// A callback to retrieve the roles for this member + /// + public static async Task HasAccessAsync(this IPublicAccessService publicAccessService, string path, string username, Func>> rolesCallback) + { + if (rolesCallback == null) + { + throw new ArgumentNullException("roles"); + } + + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "username"); + } + + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "path"); + } + + PublicAccessEntry? entry = publicAccessService.GetEntryForContent(path.EnsureEndsWith(path)); + if (entry == null) + { + return true; + } + + IEnumerable roles = await rolesCallback(); + + return HasAccess(entry, username, roles); + } + + private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) + { + if (entry is null) + { + throw new ArgumentNullException(nameof(entry)); + } + + if (string.IsNullOrEmpty(username)) + { + throw new ArgumentException($"'{nameof(username)}' cannot be null or empty.", nameof(username)); + } + + if (roles is null) + { + throw new ArgumentNullException(nameof(roles)); + } + + return entry.Rules.Any(x => + (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && + username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) + || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue))); } } diff --git a/src/Umbraco.Core/Services/PublishResult.cs b/src/Umbraco.Core/Services/PublishResult.cs index 0ab820e7a6..f689249afc 100644 --- a/src/Umbraco.Core/Services/PublishResult.cs +++ b/src/Umbraco.Core/Services/PublishResult.cs @@ -1,37 +1,36 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Represents the result of publishing a document. +/// +public class PublishResult : OperationResult { + /// + /// Initializes a new instance of the class. + /// + public PublishResult(PublishResultType resultType, EventMessages? eventMessages, IContent? content) + : base(resultType, eventMessages, content) + { + } /// - /// Represents the result of publishing a document. + /// Initializes a new instance of the class. /// - public class PublishResult : OperationResult + public PublishResult(EventMessages eventMessages, IContent content) + : base(PublishResultType.SuccessPublish, eventMessages, content) { - /// - /// Initializes a new instance of the class. - /// - public PublishResult(PublishResultType resultType, EventMessages? eventMessages, IContent? content) - : base(resultType, eventMessages, content) - { } - - /// - /// Initializes a new instance of the class. - /// - public PublishResult(EventMessages eventMessages, IContent content) - : base(PublishResultType.SuccessPublish, eventMessages, content) - { } - - /// - /// Gets the document. - /// - public IContent? Content => Entity; - - /// - /// Gets or sets the invalid properties, if the status failed due to validation. - /// - public IEnumerable? InvalidProperties { get; set; } } + + /// + /// Gets the document. + /// + public IContent? Content => Entity; + + /// + /// Gets or sets the invalid properties, if the status failed due to validation. + /// + public IEnumerable? InvalidProperties { get; set; } } diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 43fab58218..b8ebd5edd4 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -1,151 +1,152 @@ -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// A value indicating the result of publishing or unpublishing a document. +/// +public enum PublishResultType : byte { + // all "ResultType" enum's must be byte-based, and declare Failed = 128, and declare + // every failure codes as >128 - see OperationResult and OperationResultType for details. + #region Success - Publish + /// - /// A value indicating the result of publishing or unpublishing a document. + /// The document was successfully published. /// - public enum PublishResultType : byte - { - // all "ResultType" enum's must be byte-based, and declare Failed = 128, and declare - // every failure codes as >128 - see OperationResult and OperationResultType for details. + SuccessPublish = 0, - #region Success - Publish + /// + /// The specified document culture was successfully published. + /// + SuccessPublishCulture = 1, - /// - /// The document was successfully published. - /// - SuccessPublish = 0, + /// + /// The document was already published. + /// + SuccessPublishAlready = 2, - /// - /// The specified document culture was successfully published. - /// - SuccessPublishCulture = 1, + #endregion - /// - /// The document was already published. - /// - SuccessPublishAlready = 2, + #region Success - Unpublish - #endregion + /// + /// The document was successfully unpublished. + /// + SuccessUnpublish = 3, - #region Success - Unpublish + /// + /// The document was already unpublished. + /// + SuccessUnpublishAlready = 4, - /// - /// The document was successfully unpublished. - /// - SuccessUnpublish = 3, + /// + /// The specified document culture was unpublished, the document item itself remains published. + /// + SuccessUnpublishCulture = 5, - /// - /// The document was already unpublished. - /// - SuccessUnpublishAlready = 4, + /// + /// The specified document culture was unpublished, and was a mandatory culture, therefore the document itself was + /// unpublished. + /// + SuccessUnpublishMandatoryCulture = 6, - /// - /// The specified document culture was unpublished, the document item itself remains published. - /// - SuccessUnpublishCulture = 5, + /// + /// The specified document culture was unpublished, and was the last published culture in the document, therefore the + /// document itself was unpublished. + /// + SuccessUnpublishLastCulture = 8, - /// - /// The specified document culture was unpublished, and was a mandatory culture, therefore the document itself was unpublished. - /// - SuccessUnpublishMandatoryCulture = 6, + #endregion - /// - /// The specified document culture was unpublished, and was the last published culture in the document, therefore the document itself was unpublished. - /// - SuccessUnpublishLastCulture = 8, + #region Success - Mixed - #endregion + /// + /// Specified document cultures were successfully published and unpublished (in the same operation). + /// + SuccessMixedCulture = 7, - #region Success - Mixed + #endregion - /// - /// Specified document cultures were successfully published and unpublished (in the same operation). - /// - SuccessMixedCulture = 7, + #region Failed - Publish - #endregion + /// + /// The operation failed. + /// + /// All values above this value indicate a failure. + FailedPublish = 128, - #region Failed - Publish + /// + /// The document could not be published because its ancestor path is not published. + /// + FailedPublishPathNotPublished = FailedPublish | 1, - /// - /// The operation failed. - /// - /// All values above this value indicate a failure. - FailedPublish = 128, + /// + /// The document has expired so we cannot force it to be + /// published again as part of a bulk publish operation. + /// + FailedPublishHasExpired = FailedPublish | 2, - /// - /// The document could not be published because its ancestor path is not published. - /// - FailedPublishPathNotPublished = FailedPublish | 1, + /// + /// The document is scheduled to be released in the future and therefore we cannot force it to + /// be published during a bulk publish operation. + /// + FailedPublishAwaitingRelease = FailedPublish | 3, - /// - /// The document has expired so we cannot force it to be - /// published again as part of a bulk publish operation. - /// - FailedPublishHasExpired = FailedPublish | 2, + /// + /// A document culture has expired so we cannot force it to be + /// published again as part of a bulk publish operation. + /// + FailedPublishCultureHasExpired = FailedPublish | 4, - /// - /// The document is scheduled to be released in the future and therefore we cannot force it to - /// be published during a bulk publish operation. - /// - FailedPublishAwaitingRelease = FailedPublish | 3, + /// + /// A document culture is scheduled to be released in the future and therefore we cannot force it to + /// be published during a bulk publish operation. + /// + FailedPublishCultureAwaitingRelease = FailedPublish | 5, - /// - /// A document culture has expired so we cannot force it to be - /// published again as part of a bulk publish operation. - /// - FailedPublishCultureHasExpired = FailedPublish | 4, + /// + /// The document could not be published because it is in the trash. + /// + FailedPublishIsTrashed = FailedPublish | 6, - /// - /// A document culture is scheduled to be released in the future and therefore we cannot force it to - /// be published during a bulk publish operation. - /// - FailedPublishCultureAwaitingRelease = FailedPublish | 5, + /// + /// The publish action has been cancelled by an event handler. + /// + FailedPublishCancelledByEvent = FailedPublish | 7, - /// - /// The document could not be published because it is in the trash. - /// - FailedPublishIsTrashed = FailedPublish | 6, + /// + /// The document could not be published because it contains invalid data (has not passed validation requirements). + /// + FailedPublishContentInvalid = FailedPublish | 8, - /// - /// The publish action has been cancelled by an event handler. - /// - FailedPublishCancelledByEvent = FailedPublish | 7, + /// + /// The document could not be published because it has no publishing flags or values or if its a variant document, no + /// cultures were specified to be published. + /// + FailedPublishNothingToPublish = FailedPublish | 9, - /// - /// The document could not be published because it contains invalid data (has not passed validation requirements). - /// - FailedPublishContentInvalid = FailedPublish | 8, + /// + /// The document could not be published because some mandatory cultures are missing. + /// + FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing - /// - /// The document could not be published because it has no publishing flags or values or if its a variant document, no cultures were specified to be published. - /// - FailedPublishNothingToPublish = FailedPublish | 9, + /// + /// The document could not be published because it has been modified by another user. + /// + FailedPublishConcurrencyViolation = FailedPublish | 11, - /// - /// The document could not be published because some mandatory cultures are missing. - /// - FailedPublishMandatoryCultureMissing = FailedPublish | 10, // in ContentService.SavePublishing + #endregion - /// - /// The document could not be published because it has been modified by another user. - /// - FailedPublishConcurrencyViolation = FailedPublish | 11, + #region Failed - Unpublish - #endregion + /// + /// The document could not be unpublished. + /// + FailedUnpublish = FailedPublish | 11, // in ContentService.SavePublishing - #region Failed - Unpublish + /// + /// The unpublish action has been cancelled by an event handler. + /// + FailedUnpublishCancelledByEvent = FailedPublish | 12, - /// - /// The document could not be unpublished. - /// - FailedUnpublish = FailedPublish | 11, // in ContentService.SavePublishing - - /// - /// The unpublish action has been cancelled by an event handler. - /// - FailedUnpublishCancelledByEvent = FailedPublish | 12, - - #endregion - } + #endregion } diff --git a/src/Umbraco.Core/Services/RedirectUrlService.cs b/src/Umbraco.Core/Services/RedirectUrlService.cs index 14c3e834bf..e68eed31e7 100644 --- a/src/Umbraco.Core/Services/RedirectUrlService.cs +++ b/src/Umbraco.Core/Services/RedirectUrlService.cs @@ -1,122 +1,128 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +internal class RedirectUrlService : RepositoryService, IRedirectUrlService { - internal class RedirectUrlService : RepositoryService, IRedirectUrlService + private readonly IRedirectUrlRepository _redirectUrlRepository; + + public RedirectUrlService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IRedirectUrlRepository redirectUrlRepository) + : base(provider, loggerFactory, eventMessagesFactory) => + _redirectUrlRepository = redirectUrlRepository; + + public void Register(string url, Guid contentKey, string? culture = null) { - private readonly IRedirectUrlRepository _redirectUrlRepository; - - public RedirectUrlService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IRedirectUrlRepository redirectUrlRepository) - : base(provider, loggerFactory, eventMessagesFactory) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - _redirectUrlRepository = redirectUrlRepository; - } - - public void Register(string url, Guid contentKey, string? culture = null) - { - using (var scope = ScopeProvider.CreateCoreScope()) + IRedirectUrl? redir = _redirectUrlRepository.Get(url, contentKey, culture); + if (redir != null) { - var redir = _redirectUrlRepository.Get(url, contentKey, culture); - if (redir != null) - redir.CreateDateUtc = DateTime.UtcNow; - else - redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture}; - _redirectUrlRepository.Save(redir); - scope.Complete(); + redir.CreateDateUtc = DateTime.UtcNow; } - } - - public void Delete(IRedirectUrl redirectUrl) - { - using (var scope = ScopeProvider.CreateCoreScope()) + else { - _redirectUrlRepository.Delete(redirectUrl); - scope.Complete(); - } - } - - public void Delete(Guid id) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.Delete(id); - scope.Complete(); - } - } - - public void DeleteContentRedirectUrls(Guid contentKey) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.DeleteContentUrls(contentKey); - scope.Complete(); - } - } - - public void DeleteAll() - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _redirectUrlRepository.DeleteAll(); - scope.Complete(); - } - } - - public IRedirectUrl? GetMostRecentRedirectUrl(string url) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetMostRecentUrl(url); - } - } - - public IEnumerable GetContentRedirectUrls(Guid contentKey) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetContentUrls(contentKey); - } - } - - public IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetAllUrls(pageIndex, pageSize, out total); - } - } - - public IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetAllUrls(rootContentId, pageIndex, pageSize, out total); - } - } - - public IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.SearchUrls(searchTerm, pageIndex, pageSize, out total); - } - } - - public IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture) - { - if (string.IsNullOrWhiteSpace(culture)) return GetMostRecentRedirectUrl(url); - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _redirectUrlRepository.GetMostRecentUrl(url, culture); + redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey, Culture = culture }; } + _redirectUrlRepository.Save(redir); + scope.Complete(); + } + } + + public void Delete(IRedirectUrl redirectUrl) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _redirectUrlRepository.Delete(redirectUrl); + scope.Complete(); + } + } + + public void Delete(Guid id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _redirectUrlRepository.Delete(id); + scope.Complete(); + } + } + + public void DeleteContentRedirectUrls(Guid contentKey) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _redirectUrlRepository.DeleteContentUrls(contentKey); + scope.Complete(); + } + } + + public void DeleteAll() + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _redirectUrlRepository.DeleteAll(); + scope.Complete(); + } + } + + public IRedirectUrl? GetMostRecentRedirectUrl(string url) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetMostRecentUrl(url); + } + } + + public IEnumerable GetContentRedirectUrls(Guid contentKey) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetContentUrls(contentKey); + } + } + + public IEnumerable GetAllRedirectUrls(long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetAllUrls(pageIndex, pageSize, out total); + } + } + + public IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetAllUrls(rootContentId, pageIndex, pageSize, out total); + } + } + + public IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.SearchUrls(searchTerm, pageIndex, pageSize, out total); + } + } + + public IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture) + { + if (string.IsNullOrWhiteSpace(culture)) + { + return GetMostRecentRedirectUrl(url); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _redirectUrlRepository.GetMostRecentUrl(url, culture); } } } diff --git a/src/Umbraco.Core/Services/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs index 966e4ec7df..20cd72e7cc 100644 --- a/src/Umbraco.Core/Services/RelationService.cs +++ b/src/Umbraco.Core/Services/RelationService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -11,605 +8,612 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class RelationService : RepositoryService, IRelationService { - public class RelationService : RepositoryService, IRelationService + private readonly IAuditRepository _auditRepository; + private readonly IEntityService _entityService; + private readonly IRelationRepository _relationRepository; + private readonly IRelationTypeRepository _relationTypeRepository; + + public RelationService(ICoreScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IEntityService entityService, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, IAuditRepository auditRepository) + : base(uowProvider, loggerFactory, eventMessagesFactory) { - private readonly IEntityService _entityService; - private readonly IRelationRepository _relationRepository; - private readonly IRelationTypeRepository _relationTypeRepository; - private readonly IAuditRepository _auditRepository; - - public RelationService(ICoreScopeProvider uowProvider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IEntityService entityService, - IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, IAuditRepository auditRepository) - : base(uowProvider, loggerFactory, eventMessagesFactory) - { - _relationRepository = relationRepository; - _relationTypeRepository = relationTypeRepository; - _auditRepository = auditRepository; - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - } - - /// - public IRelation? GetById(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.Get(id); - } - } - - /// - public IRelationType? GetRelationTypeById(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationTypeRepository.Get(id); - } - } - - /// - public IRelationType? GetRelationTypeById(Guid id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationTypeRepository.Get(id); - } - } - - /// - public IRelationType? GetRelationTypeByAlias(string alias) => GetRelationType(alias); - - /// - public IEnumerable GetAllRelations(params int[] ids) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.GetMany(ids); - } - } - - /// - public IEnumerable? GetAllRelationsByRelationType(IRelationType relationType) - { - return GetAllRelationsByRelationType(relationType.Id); - } - - /// - public IEnumerable? GetAllRelationsByRelationType(int relationTypeId) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.RelationTypeId == relationTypeId); - return _relationRepository.Get(query); - } - } - - /// - public IEnumerable GetAllRelationTypes(params int[] ids) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationTypeRepository.GetMany(ids); - } - } - - /// - public IEnumerable? GetByParentId(int id) => GetByParentId(id, null); - - /// - public IEnumerable GetByParentId(int id, string? relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - if (relationTypeAlias.IsNullOrWhiteSpace()) - { - var qry1 = Query().Where(x => x.ParentId == id); - return _relationRepository.Get(qry1) ?? Enumerable.Empty(); - } - - var relationType = GetRelationType(relationTypeAlias!); - if (relationType == null) - return Enumerable.Empty(); - - var qry2 = Query().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(qry2) ?? Enumerable.Empty(); - } - } - - /// - public IEnumerable? GetByParent(IUmbracoEntity parent) => GetByParentId(parent.Id); - - /// - public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => GetByParentId(parent.Id, relationTypeAlias); - - /// - public IEnumerable GetByChildId(int id) => GetByChildId(id, null); - - /// - public IEnumerable GetByChildId(int id, string? relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - if (relationTypeAlias.IsNullOrWhiteSpace()) - { - var qry1 = Query().Where(x => x.ChildId == id); - return _relationRepository.Get(qry1) ?? Enumerable.Empty(); - } - - var relationType = GetRelationType(relationTypeAlias!); - if (relationType == null) - return Enumerable.Empty(); - - var qry2 = Query().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(qry2) ?? Enumerable.Empty(); - } - } - - /// - public IEnumerable GetByChild(IUmbracoEntity child) => GetByChildId(child.Id); - - /// - public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) => GetByChildId(child.Id, relationTypeAlias); - - /// - public IEnumerable GetByParentOrChildId(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ChildId == id || x.ParentId == id); - return _relationRepository.Get(query); - } - } - - public IEnumerable GetByParentOrChildId(int id, string relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var relationType = GetRelationType(relationTypeAlias); - if (relationType == null) - return Enumerable.Empty(); - - var query = Query().Where(x => (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query); - } - } - - /// - public IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId && - x.ChildId == childId && - x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query)?.FirstOrDefault(); - } - } - - /// - public IEnumerable GetByRelationTypeName(string relationTypeName) - { - List? relationTypeIds; - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - //This is a silly query - but i guess it's needed in case someone has more than one relation with the same Name (not alias), odd. - var query = Query().Where(x => x.Name == relationTypeName); - var relationTypes = _relationTypeRepository.Get(query); - relationTypeIds = relationTypes?.Select(x => x.Id).ToList(); - } - - return relationTypeIds is null || relationTypeIds.Count == 0 - ? Enumerable.Empty() - : GetRelationsByListOfTypeIds(relationTypeIds); - } - - /// - public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) - { - var relationType = GetRelationType(relationTypeAlias); - - return relationType == null - ? Enumerable.Empty() - : GetRelationsByListOfTypeIds(new[] { relationType.Id }); - } - - /// - public IEnumerable? GetByRelationTypeId(int relationTypeId) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.RelationTypeId == relationTypeId); - return _relationRepository.Get(query); - } - } - - /// - public IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query()?.Where(x => x.RelationTypeId == relationTypeId); - return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering); - } - } - - /// - public IUmbracoEntity? GetChildEntityFromRelation(IRelation relation) - { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - return _entityService.Get(relation.ChildId, objectType); - } - - /// - public IUmbracoEntity? GetParentEntityFromRelation(IRelation relation) - { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - return _entityService.Get(relation.ParentId, objectType); - } - - /// - public Tuple? GetEntitiesFromRelation(IRelation relation) - { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - - var child = _entityService.Get(relation.ChildId, childObjectType); - var parent = _entityService.Get(relation.ParentId, parentObjectType); - - if (parent is null || child is null) - { - return null; - } - - return new Tuple(parent, child); - } - - /// - public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) - { - // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll - // method to lookup batches of entities for each parent object type - - foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ChildObjectType))) - { - var objectType = groupedRelations.Key; - var ids = groupedRelations.Select(x => x.ChildId).ToArray(); - foreach (var e in _entityService.GetAll(objectType, ids)) - yield return e; - } - } - - /// - public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations) - { - // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll - // method to lookup batches of entities for each parent object type - - foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ParentObjectType))) - { - var objectType = groupedRelations.Key; - var ids = groupedRelations.Select(x => x.ParentId).ToArray(); - foreach (var e in _entityService.GetAll(objectType, ids)) - yield return e; - } - } - - /// - public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); - } - } - - /// - public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); - } - } - - /// - public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) - { - //TODO: Argh! N+1 - - foreach (var relation in relations) - { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - - var child = _entityService.Get(relation.ChildId, childObjectType); - var parent = _entityService.Get(relation.ParentId, parentObjectType); - - if (parent is not null && child is not null) - { - yield return new Tuple(parent, child); - } - } - } - - /// - public IRelation Relate(int parentId, int childId, IRelationType relationType) - { - // Ensure that the RelationType has an identity before using it to relate two entities - if (relationType.HasIdentity == false) - { - Save(relationType); - } - - //TODO: We don't check if this exists first, it will throw some sort of data integrity exception if it already exists, is that ok? - - var relation = new Relation(parentId, childId, relationType); - - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var savingNotification = new RelationSavingNotification(relation, eventMessages); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return relation; // TODO: returning sth that does not exist here?! - } - - _relationRepository.Save(relation); - scope.Notifications.Publish(new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); - scope.Complete(); - return relation; - } - } - - /// - public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) - { - return Relate(parent.Id, child.Id, relationType); - } - - /// - public IRelation Relate(int parentId, int childId, string relationTypeAlias) - { - var relationType = GetRelationTypeByAlias(relationTypeAlias); - if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) - throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); - - return Relate(parentId, childId, relationType); - } - - /// - public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) - { - var relationType = GetRelationTypeByAlias(relationTypeAlias); - if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) - throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); - - return Relate(parent.Id, child.Id, relationType); - } - - /// - public bool HasRelations(IRelationType relationType) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query)?.Any() ?? false; - } - } - - /// - public bool IsRelated(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == id || x.ChildId == id); - return _relationRepository.Get(query)?.Any() ?? false; - } - } - - /// - public bool AreRelated(int parentId, int childId) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId); - return _relationRepository.Get(query)?.Any() ?? false; - } - } - - /// - public bool AreRelated(int parentId, int childId, string relationTypeAlias) - { - var relType = GetRelationTypeByAlias(relationTypeAlias); - if (relType == null) - return false; - - return AreRelated(parentId, childId, relType); - } - - - /// - public bool AreRelated(int parentId, int childId, IRelationType relationType) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId && x.RelationTypeId == relationType.Id); - return _relationRepository.Get(query)?.Any() ?? false; - } - } - - /// - public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) - { - return AreRelated(parent.Id, child.Id); - } - - /// - public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) - { - return AreRelated(parent.Id, child.Id, relationTypeAlias); - } - - - /// - public void Save(IRelation relation) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var savingNotification = new RelationSavingNotification(relation, eventMessages); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - _relationRepository.Save(relation); - scope.Complete(); - scope.Notifications.Publish(new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); - } - } - - public void Save(IEnumerable relations) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - IRelation[] relationsA = relations.ToArray(); - - EventMessages messages = EventMessagesFactory.Get(); - var savingNotification = new RelationSavingNotification(relationsA, messages); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - _relationRepository.Save(relationsA); - scope.Complete(); - scope.Notifications.Publish(new RelationSavedNotification(relationsA, messages).WithStateFrom(savingNotification)); - } - } - - /// - public void Save(IRelationType relationType) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var savingNotification = new RelationTypeSavingNotification(relationType, eventMessages); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - _relationTypeRepository.Save(relationType); - Audit(AuditType.Save, Cms.Core.Constants.Security.SuperUserId, relationType.Id, $"Saved relation type: {relationType.Name}"); - scope.Complete(); - scope.Notifications.Publish(new RelationTypeSavedNotification(relationType, eventMessages).WithStateFrom(savingNotification)); - } - } - - /// - public void Delete(IRelation relation) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var deletingNotification = new RelationDeletingNotification(relation, eventMessages); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return; - } - - _relationRepository.Delete(relation); - scope.Complete(); - scope.Notifications.Publish(new RelationDeletedNotification(relation, eventMessages).WithStateFrom(deletingNotification)); - } - } - - /// - public void Delete(IRelationType relationType) - { - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - EventMessages eventMessages = EventMessagesFactory.Get(); - var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return; - } - - _relationTypeRepository.Delete(relationType); - scope.Complete(); - scope.Notifications.Publish(new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification)); - } - } - - /// - public void DeleteRelationsOfType(IRelationType relationType) - { - var relations = new List(); - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - IQuery? query = Query().Where(x => x.RelationTypeId == relationType.Id); - var allRelations = _relationRepository.Get(query)?.ToList(); - if (allRelations is not null) - { - relations.AddRange(allRelations); - } - - //TODO: N+1, we should be able to do this in a single call - - foreach (IRelation relation in relations) - { - _relationRepository.Delete(relation); - } - - scope.Complete(); - - scope.Notifications.Publish(new RelationDeletedNotification(relations, EventMessagesFactory.Get())); - } - } - - #region Private Methods - - private IRelationType? GetRelationType(string relationTypeAlias) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == relationTypeAlias); - return _relationTypeRepository.Get(query)?.FirstOrDefault(); - } - } - - private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) - { - var relations = new List(); - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - foreach (var relationTypeId in relationTypeIds) - { - var id = relationTypeId; - var query = Query().Where(x => x.RelationTypeId == id); - var relation = _relationRepository.Get(query); - if (relation is not null) - { - relations.AddRange(relation); - } - } - } - return relations; - } - - private void Audit(AuditType type, int userId, int objectId, string? message = null) - { - _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.RelationType), message)); - } - #endregion + _relationRepository = relationRepository; + _relationTypeRepository = relationTypeRepository; + _auditRepository = auditRepository; + _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); } + + /// + public IRelation? GetById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationRepository.Get(id); + } + } + + /// + public IRelationType? GetRelationTypeById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationTypeRepository.Get(id); + } + } + + /// + public IRelationType? GetRelationTypeById(Guid id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationTypeRepository.Get(id); + } + } + + /// + public IRelationType? GetRelationTypeByAlias(string alias) => GetRelationType(alias); + + /// + public IEnumerable GetAllRelations(params int[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationRepository.GetMany(ids); + } + } + + /// + public IEnumerable GetAllRelationsByRelationType(IRelationType relationType) => + GetAllRelationsByRelationType(relationType.Id); + + /// + public IEnumerable GetAllRelationsByRelationType(int relationTypeId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.Get(query); + } + } + + /// + public IEnumerable GetAllRelationTypes(params int[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationTypeRepository.GetMany(ids); + } + } + + /// + public IEnumerable GetByParentId(int id) => GetByParentId(id, null); + + /// + public IEnumerable GetByParentId(int id, string? relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + if (relationTypeAlias.IsNullOrWhiteSpace()) + { + IQuery qry1 = Query().Where(x => x.ParentId == id); + return _relationRepository.Get(qry1); + } + + IRelationType? relationType = GetRelationType(relationTypeAlias!); + if (relationType == null) + { + return Enumerable.Empty(); + } + + IQuery qry2 = + Query().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); + } + } + + /// + public IEnumerable GetByParent(IUmbracoEntity parent) => GetByParentId(parent.Id); + + /// + public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => + GetByParentId(parent.Id, relationTypeAlias); + + /// + public IEnumerable GetByChildId(int id) => GetByChildId(id, null); + + /// + public IEnumerable GetByChildId(int id, string? relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + if (relationTypeAlias.IsNullOrWhiteSpace()) + { + IQuery qry1 = Query().Where(x => x.ChildId == id); + return _relationRepository.Get(qry1); + } + + IRelationType? relationType = GetRelationType(relationTypeAlias!); + if (relationType == null) + { + return Enumerable.Empty(); + } + + IQuery qry2 = + Query().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); + } + } + + /// + public IEnumerable GetByChild(IUmbracoEntity child) => GetByChildId(child.Id); + + /// + public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) => + GetByChildId(child.Id, relationTypeAlias); + + /// + public IEnumerable GetByParentOrChildId(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ChildId == id || x.ParentId == id); + return _relationRepository.Get(query); + } + } + + public IEnumerable GetByParentOrChildId(int id, string relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IRelationType? relationType = GetRelationType(relationTypeAlias); + if (relationType == null) + { + return Enumerable.Empty(); + } + + IQuery query = Query().Where(x => + (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query); + } + } + + /// + public IRelation? GetByParentAndChildId(int parentId, int childId, IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ParentId == parentId && + x.ChildId == childId && + x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query).FirstOrDefault(); + } + } + + /// + public IEnumerable GetByRelationTypeName(string relationTypeName) + { + List? relationTypeIds; + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + // This is a silly query - but i guess it's needed in case someone has more than one relation with the same Name (not alias), odd. + IQuery query = Query().Where(x => x.Name == relationTypeName); + IEnumerable relationTypes = _relationTypeRepository.Get(query); + relationTypeIds = relationTypes.Select(x => x.Id).ToList(); + } + + return relationTypeIds.Count == 0 + ? Enumerable.Empty() + : GetRelationsByListOfTypeIds(relationTypeIds); + } + + /// + public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) + { + IRelationType? relationType = GetRelationType(relationTypeAlias); + + return relationType == null + ? Enumerable.Empty() + : GetRelationsByListOfTypeIds(new[] { relationType.Id }); + } + + /// + public IEnumerable GetByRelationTypeId(int relationTypeId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.Get(query); + } + } + + /// + public IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering = null) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery? query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering); + } + } + + /// + public IUmbracoEntity? GetChildEntityFromRelation(IRelation relation) + { + UmbracoObjectTypes objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + return _entityService.Get(relation.ChildId, objectType); + } + + /// + public IUmbracoEntity? GetParentEntityFromRelation(IRelation relation) + { + UmbracoObjectTypes objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + return _entityService.Get(relation.ParentId, objectType); + } + + /// + public Tuple? GetEntitiesFromRelation(IRelation relation) + { + UmbracoObjectTypes childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + UmbracoObjectTypes parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + + IEntitySlim? child = _entityService.Get(relation.ChildId, childObjectType); + IEntitySlim? parent = _entityService.Get(relation.ParentId, parentObjectType); + + if (parent is null || child is null) + { + return null; + } + + return new Tuple(parent, child); + } + + /// + public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) + { + // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll + // method to lookup batches of entities for each parent object type + foreach (IGrouping groupedRelations in relations.GroupBy(x => + ObjectTypes.GetUmbracoObjectType(x.ChildObjectType))) + { + UmbracoObjectTypes objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ChildId).ToArray(); + foreach (IEntitySlim e in _entityService.GetAll(objectType, ids)) + { + yield return e; + } + } + } + + /// + public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations) + { + // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll + // method to lookup batches of entities for each parent object type + foreach (IGrouping groupedRelations in relations.GroupBy(x => + ObjectTypes.GetUmbracoObjectType(x.ParentObjectType))) + { + UmbracoObjectTypes objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ParentId).ToArray(); + foreach (IEntitySlim e in _entityService.GetAll(objectType, ids)) + { + yield return e; + } + } + } + + /// + public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); + } + } + + /// + public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); + } + } + + /// + public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) + { + // TODO: Argh! N+1 + foreach (IRelation relation in relations) + { + UmbracoObjectTypes childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + UmbracoObjectTypes parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); + + IEntitySlim? child = _entityService.Get(relation.ChildId, childObjectType); + IEntitySlim? parent = _entityService.Get(relation.ParentId, parentObjectType); + + if (parent is not null && child is not null) + { + yield return new Tuple(parent, child); + } + } + } + + /// + public IRelation Relate(int parentId, int childId, IRelationType relationType) + { + // Ensure that the RelationType has an identity before using it to relate two entities + if (relationType.HasIdentity == false) + { + Save(relationType); + } + + // TODO: We don't check if this exists first, it will throw some sort of data integrity exception if it already exists, is that ok? + var relation = new Relation(parentId, childId, relationType); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return relation; // TODO: returning sth that does not exist here?! + } + + _relationRepository.Save(relation); + scope.Notifications.Publish( + new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); + scope.Complete(); + return relation; + } + } + + /// + public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) => + Relate(parent.Id, child.Id, relationType); + + /// + public IRelation Relate(int parentId, int childId, string relationTypeAlias) + { + IRelationType? relationType = GetRelationTypeByAlias(relationTypeAlias); + if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) + { + throw new ArgumentNullException( + string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); + } + + return Relate(parentId, childId, relationType); + } + + /// + public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) + { + IRelationType? relationType = GetRelationTypeByAlias(relationTypeAlias); + if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) + { + throw new ArgumentNullException( + string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); + } + + return Relate(parent.Id, child.Id, relationType); + } + + /// + public bool HasRelations(IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query).Any(); + } + } + + /// + public bool IsRelated(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ParentId == id || x.ChildId == id); + return _relationRepository.Get(query).Any(); + } + } + + /// + public bool AreRelated(int parentId, int childId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ParentId == parentId && x.ChildId == childId); + return _relationRepository.Get(query).Any(); + } + } + + /// + public bool AreRelated(int parentId, int childId, string relationTypeAlias) + { + IRelationType? relType = GetRelationTypeByAlias(relationTypeAlias); + if (relType == null) + { + return false; + } + + return AreRelated(parentId, childId, relType); + } + + /// + public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) => AreRelated(parent.Id, child.Id); + + /// + public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) => + AreRelated(parent.Id, child.Id, relationTypeAlias); + + /// + public void Save(IRelation relation) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + _relationRepository.Save(relation); + scope.Complete(); + scope.Notifications.Publish( + new RelationSavedNotification(relation, eventMessages).WithStateFrom(savingNotification)); + } + } + + public void Save(IEnumerable relations) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + IRelation[] relationsA = relations.ToArray(); + + EventMessages messages = EventMessagesFactory.Get(); + var savingNotification = new RelationSavingNotification(relationsA, messages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + _relationRepository.Save(relationsA); + scope.Complete(); + scope.Notifications.Publish( + new RelationSavedNotification(relationsA, messages).WithStateFrom(savingNotification)); + } + } + + /// + public void Save(IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new RelationTypeSavingNotification(relationType, eventMessages); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + _relationTypeRepository.Save(relationType); + Audit(AuditType.Save, Constants.Security.SuperUserId, relationType.Id, $"Saved relation type: {relationType.Name}"); + scope.Complete(); + scope.Notifications.Publish( + new RelationTypeSavedNotification(relationType, eventMessages).WithStateFrom(savingNotification)); + } + } + + /// + public void Delete(IRelation relation) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new RelationDeletingNotification(relation, eventMessages); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { + scope.Complete(); + return; + } + + _relationRepository.Delete(relation); + scope.Complete(); + scope.Notifications.Publish( + new RelationDeletedNotification(relation, eventMessages).WithStateFrom(deletingNotification)); + } + } + + /// + public void Delete(IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new RelationTypeDeletingNotification(relationType, eventMessages); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { + scope.Complete(); + return; + } + + _relationTypeRepository.Delete(relationType); + scope.Complete(); + scope.Notifications.Publish( + new RelationTypeDeletedNotification(relationType, eventMessages).WithStateFrom(deletingNotification)); + } + } + + /// + public void DeleteRelationsOfType(IRelationType relationType) + { + var relations = new List(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + IQuery query = Query().Where(x => x.RelationTypeId == relationType.Id); + var allRelations = _relationRepository.Get(query).ToList(); + relations.AddRange(allRelations); + + // TODO: N+1, we should be able to do this in a single call + foreach (IRelation relation in relations) + { + _relationRepository.Delete(relation); + } + + scope.Complete(); + + scope.Notifications.Publish(new RelationDeletedNotification(relations, EventMessagesFactory.Get())); + } + } + + public bool AreRelated(int parentId, int childId, IRelationType relationType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => + x.ParentId == parentId && x.ChildId == childId && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(query).Any(); + } + } + + #region Private Methods + + private IRelationType? GetRelationType(string relationTypeAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.Alias == relationTypeAlias); + return _relationTypeRepository.Get(query).FirstOrDefault(); + } + } + + private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) + { + var relations = new List(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + foreach (var relationTypeId in relationTypeIds) + { + var id = relationTypeId; + IQuery query = Query().Where(x => x.RelationTypeId == id); + IEnumerable relation = _relationRepository.Get(query); + relations.AddRange(relation); + } + } + + return relations; + } + + private void Audit(AuditType type, int userId, int objectId, string? message = null) => + _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.RelationType.GetName(), message)); + + #endregion } diff --git a/src/Umbraco.Core/Services/RepositoryService.cs b/src/Umbraco.Core/Services/RepositoryService.cs index 85e78672ee..2c7bb39085 100644 --- a/src/Umbraco.Core/Services/RepositoryService.cs +++ b/src/Umbraco.Core/Services/RepositoryService.cs @@ -1,29 +1,27 @@ -using System; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Represents a service that works on top of repositories. +/// +public abstract class RepositoryService : IService { - /// - /// Represents a service that works on top of repositories. - /// - public abstract class RepositoryService : IService + protected RepositoryService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) { - protected IEventMessagesFactory EventMessagesFactory { get; } - - protected ICoreScopeProvider ScopeProvider { get; } - - protected ILoggerFactory LoggerFactory { get; } - - protected RepositoryService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) - { - EventMessagesFactory = eventMessagesFactory ?? throw new ArgumentNullException(nameof(eventMessagesFactory)); - ScopeProvider = provider ?? throw new ArgumentNullException(nameof(provider)); - LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - } - - protected IQuery Query() => ScopeProvider.CreateQuery(); + EventMessagesFactory = eventMessagesFactory ?? throw new ArgumentNullException(nameof(eventMessagesFactory)); + ScopeProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } + + protected IEventMessagesFactory EventMessagesFactory { get; } + + protected ICoreScopeProvider ScopeProvider { get; } + + protected ILoggerFactory LoggerFactory { get; } + + protected IQuery Query() => ScopeProvider.CreateQuery(); } diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index b698579b65..61ff978894 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -1,41 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Sections; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class SectionService : ISectionService { - public class SectionService : ISectionService + private readonly SectionCollection _sectionCollection; + private readonly IUserService _userService; + + public SectionService( + IUserService userService, + SectionCollection sectionCollection) { - private readonly IUserService _userService; - private readonly SectionCollection _sectionCollection; - - public SectionService( - IUserService userService, - SectionCollection sectionCollection) - { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _sectionCollection = sectionCollection ?? throw new ArgumentNullException(nameof(sectionCollection)); - } - - /// - /// The cache storage for all applications - /// - public IEnumerable GetSections() - => _sectionCollection; - - /// - public IEnumerable GetAllowedSections(int userId) - { - var user = _userService.GetUserById(userId); - if (user == null) - throw new InvalidOperationException("No user found with id " + userId); - - return GetSections().Where(x => user.AllowedSections.Contains(x.Alias)); - } - - /// - public ISection? GetByAlias(string appAlias) - => GetSections().FirstOrDefault(t => t.Alias.Equals(appAlias, StringComparison.OrdinalIgnoreCase)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _sectionCollection = sectionCollection ?? throw new ArgumentNullException(nameof(sectionCollection)); } + + /// + /// The cache storage for all applications + /// + public IEnumerable GetSections() + => _sectionCollection; + + /// + public IEnumerable GetAllowedSections(int userId) + { + IUser? user = _userService.GetUserById(userId); + if (user == null) + { + throw new InvalidOperationException("No user found with id " + userId); + } + + return GetSections().Where(x => user.AllowedSections.Contains(x.Alias)); + } + + /// + public ISection? GetByAlias(string appAlias) + => GetSections().FirstOrDefault(t => t.Alias.Equals(appAlias, StringComparison.OrdinalIgnoreCase)); } diff --git a/src/Umbraco.Core/Services/ServerRegistrationService.cs b/src/Umbraco.Core/Services/ServerRegistrationService.cs index c92977aab0..070e9e8e1f 100644 --- a/src/Umbraco.Core/Services/ServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/ServerRegistrationService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; @@ -10,163 +7,174 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services.Implement +namespace Umbraco.Cms.Core.Services.Implement; + +/// +/// Manages server registrations in the database. +/// +public sealed class ServerRegistrationService : RepositoryService, IServerRegistrationService { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IServerRegistrationRepository _serverRegistrationRepository; + + private ServerRole _currentServerRole = ServerRole.Unknown; + /// - /// Manages server registrations in the database. + /// Initializes a new instance of the class. /// - public sealed class ServerRegistrationService : RepositoryService, IServerRegistrationService + public ServerRegistrationService( + ICoreScopeProvider scopeProvider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IServerRegistrationRepository serverRegistrationRepository, + IHostingEnvironment hostingEnvironment) + : base(scopeProvider, loggerFactory, eventMessagesFactory) { - private readonly IServerRegistrationRepository _serverRegistrationRepository; - private readonly IHostingEnvironment _hostingEnvironment; - - private ServerRole _currentServerRole = ServerRole.Unknown; - - /// - /// Initializes a new instance of the class. - /// - public ServerRegistrationService( - ICoreScopeProvider scopeProvider, - ILoggerFactory loggerFactory, - IEventMessagesFactory eventMessagesFactory, - IServerRegistrationRepository serverRegistrationRepository, - IHostingEnvironment hostingEnvironment) - : base(scopeProvider, loggerFactory, eventMessagesFactory) - { - _serverRegistrationRepository = serverRegistrationRepository; - _hostingEnvironment = hostingEnvironment; - } - - /// - /// Touches a server to mark it as active; deactivate stale servers. - /// - /// The server URL. - /// The time after which a server is considered stale. - public void TouchServer(string serverAddress, TimeSpan staleTimeout) - { - var serverIdentity = GetCurrentServerIdentity(); - using (var scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Cms.Core.Constants.Locks.Servers); - - _serverRegistrationRepository.ClearCache(); // ensure we have up-to-date cache - - var regs = _serverRegistrationRepository.GetMany()?.ToArray(); - var hasSchedulingPublisher = regs?.Any(x => ((ServerRegistration) x).IsSchedulingPublisher); - var server = regs?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); - - if (server == null) - { - server = new ServerRegistration(serverAddress, serverIdentity, DateTime.Now); - } - else - { - server.ServerAddress = serverAddress; // should not really change but it might! - server.UpdateDate = DateTime.Now; - } - - server.IsActive = true; - if (hasSchedulingPublisher == false) - server.IsSchedulingPublisher = true; - - _serverRegistrationRepository.Save(server); - _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); // triggers a cache reload - - // reload - cheap, cached - - regs = _serverRegistrationRepository.GetMany()?.ToArray(); - - // default role is single server, but if registrations contain more - // than one active server, then role is scheduling publisher or subscriber - _currentServerRole = regs?.Count(x => x.IsActive) > 1 - ? (server.IsSchedulingPublisher ? ServerRole.SchedulingPublisher : ServerRole.Subscriber) - : ServerRole.Single; - - scope.Complete(); - } - } - - /// - /// Deactivates a server. - /// - /// The server unique identity. - public void DeactiveServer(string serverIdentity) - { - // because the repository caches "all" and has queries disabled... - - using (var scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Cms.Core.Constants.Locks.Servers); - - _serverRegistrationRepository.ClearCache(); // ensure we have up-to-date cache // ensure we have up-to-date cache - - var server = _serverRegistrationRepository.GetMany()?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); - if (server == null) return; - server.IsActive = server.IsSchedulingPublisher = false; - _serverRegistrationRepository.Save(server); // will trigger a cache reload // will trigger a cache reload - - scope.Complete(); - } - } - - /// - /// Deactivates stale servers. - /// - /// The time after which a server is considered stale. - public void DeactiveStaleServers(TimeSpan staleTimeout) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Cms.Core.Constants.Locks.Servers); - _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); - scope.Complete(); - } - } - - /// - /// Return all active servers. - /// - /// A value indicating whether to force-refresh the cache. - /// All active servers. - /// By default this method will rely on the repository's cache, which is updated each - /// time the current server is touched, and the period depends on the configuration. Use the - /// parameter to force a cache refresh and reload active servers - /// from the database. - public IEnumerable? GetActiveServers(bool refresh = false) => GetServers(refresh).Where(x => x.IsActive); - - /// - /// Return all servers (active and inactive). - /// - /// A value indicating whether to force-refresh the cache. - /// All servers. - /// By default this method will rely on the repository's cache, which is updated each - /// time the current server is touched, and the period depends on the configuration. Use the - /// parameter to force a cache refresh and reload all servers - /// from the database. - public IEnumerable GetServers(bool refresh = false) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - scope.ReadLock(Cms.Core.Constants.Locks.Servers); - if (refresh) - { - _serverRegistrationRepository.ClearCache(); - } - - return _serverRegistrationRepository.GetMany().ToArray(); // fast, cached // fast, cached - } - } - - /// - /// Gets the role of the current server. - /// - /// The role of the current server. - public ServerRole GetCurrentServerRole() => _currentServerRole; - - /// - /// Gets the local server identity. - /// - private string GetCurrentServerIdentity() => Environment.MachineName // eg DOMAIN\SERVER - + "/" + _hostingEnvironment.ApplicationId; // eg /LM/S3SVC/11/ROOT; + _serverRegistrationRepository = serverRegistrationRepository; + _hostingEnvironment = hostingEnvironment; } + + /// + /// Touches a server to mark it as active; deactivate stale servers. + /// + /// The server URL. + /// The time after which a server is considered stale. + public void TouchServer(string serverAddress, TimeSpan staleTimeout) + { + var serverIdentity = GetCurrentServerIdentity(); + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.Servers); + + _serverRegistrationRepository.ClearCache(); // ensure we have up-to-date cache + + IServerRegistration[]? regs = _serverRegistrationRepository.GetMany()?.ToArray(); + var hasSchedulingPublisher = regs?.Any(x => ((ServerRegistration)x).IsSchedulingPublisher); + IServerRegistration? server = + regs?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); + + if (server == null) + { + server = new ServerRegistration(serverAddress, serverIdentity, DateTime.Now); + } + else + { + server.ServerAddress = serverAddress; // should not really change but it might! + server.UpdateDate = DateTime.Now; + } + + server.IsActive = true; + if (hasSchedulingPublisher == false) + { + server.IsSchedulingPublisher = true; + } + + _serverRegistrationRepository.Save(server); + _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); // triggers a cache reload + + // reload - cheap, cached + regs = _serverRegistrationRepository.GetMany().ToArray(); + + // default role is single server, but if registrations contain more + // than one active server, then role is scheduling publisher or subscriber + _currentServerRole = regs.Count(x => x.IsActive) > 1 + ? server.IsSchedulingPublisher ? ServerRole.SchedulingPublisher : ServerRole.Subscriber + : ServerRole.Single; + + scope.Complete(); + } + } + + /// + /// Deactivates a server. + /// + /// The server unique identity. + public void DeactiveServer(string serverIdentity) + { + // because the repository caches "all" and has queries disabled... + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.Servers); + + _serverRegistrationRepository + .ClearCache(); // ensure we have up-to-date cache // ensure we have up-to-date cache + + IServerRegistration? server = _serverRegistrationRepository.GetMany() + ?.FirstOrDefault(x => x.ServerIdentity?.InvariantEquals(serverIdentity) ?? false); + if (server == null) + { + return; + } + + server.IsActive = server.IsSchedulingPublisher = false; + _serverRegistrationRepository.Save(server); // will trigger a cache reload // will trigger a cache reload + + scope.Complete(); + } + } + + /// + /// Deactivates stale servers. + /// + /// The time after which a server is considered stale. + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + scope.WriteLock(Constants.Locks.Servers); + _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); + scope.Complete(); + } + } + + /// + /// Return all active servers. + /// + /// A value indicating whether to force-refresh the cache. + /// All active servers. + /// + /// By default this method will rely on the repository's cache, which is updated each + /// time the current server is touched, and the period depends on the configuration. Use the + /// parameter to force a cache refresh and reload active servers + /// from the database. + /// + public IEnumerable? GetActiveServers(bool refresh = false) => + GetServers(refresh).Where(x => x.IsActive); + + /// + /// Return all servers (active and inactive). + /// + /// A value indicating whether to force-refresh the cache. + /// All servers. + /// + /// By default this method will rely on the repository's cache, which is updated each + /// time the current server is touched, and the period depends on the configuration. Use the + /// parameter to force a cache refresh and reload all servers + /// from the database. + /// + public IEnumerable GetServers(bool refresh = false) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.Servers); + if (refresh) + { + _serverRegistrationRepository.ClearCache(); + } + + return _serverRegistrationRepository.GetMany().ToArray(); // fast, cached // fast, cached + } + } + + /// + /// Gets the role of the current server. + /// + /// The role of the current server. + public ServerRole GetCurrentServerRole() => _currentServerRole; + + /// + /// Gets the local server identity. + /// + private string GetCurrentServerIdentity() => Environment.MachineName // eg DOMAIN\SERVER + + "/" + _hostingEnvironment.ApplicationId; // eg /LM/S3SVC/11/ROOT; } diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 20774bd7a2..0e24f27be5 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -1,275 +1,301 @@ -using System; +namespace Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Services +/// +/// Represents the Umbraco Service context, which provides access to all services. +/// +public class ServiceContext { + private readonly Lazy? _auditService; + private readonly Lazy? _consentService; + private readonly Lazy? _contentService; + private readonly Lazy? _contentTypeBaseServiceProvider; + private readonly Lazy? _contentTypeService; + private readonly Lazy? _dataTypeService; + private readonly Lazy? _domainService; + private readonly Lazy? _entityService; + private readonly Lazy? _externalLoginService; + private readonly Lazy? _fileService; + private readonly Lazy? _keyValueService; + private readonly Lazy? _localizationService; + private readonly Lazy? _localizedTextService; + private readonly Lazy? _macroService; + private readonly Lazy? _mediaService; + private readonly Lazy? _mediaTypeService; + private readonly Lazy? _memberGroupService; + private readonly Lazy? _memberService; + private readonly Lazy? _memberTypeService; + private readonly Lazy? _notificationService; + private readonly Lazy? _packagingService; + private readonly Lazy? _publicAccessService; + private readonly Lazy? _redirectUrlService; + private readonly Lazy? _relationService; + private readonly Lazy? _serverRegistrationService; + private readonly Lazy? _tagService; + private readonly Lazy? _userService; + /// - /// Represents the Umbraco Service context, which provides access to all services. + /// Initializes a new instance of the class with lazy services. /// - public class ServiceContext + public ServiceContext( + Lazy? publicAccessService, + Lazy? domainService, + Lazy? auditService, + Lazy? localizedTextService, + Lazy? tagService, + Lazy? contentService, + Lazy? userService, + Lazy? memberService, + Lazy? mediaService, + Lazy? contentTypeService, + Lazy? mediaTypeService, + Lazy? dataTypeService, + Lazy? fileService, + Lazy? localizationService, + Lazy? packagingService, + Lazy? serverRegistrationService, + Lazy? entityService, + Lazy? relationService, + Lazy? macroService, + Lazy? memberTypeService, + Lazy? memberGroupService, + Lazy? notificationService, + Lazy? externalLoginService, + Lazy? redirectUrlService, + Lazy? consentService, + Lazy? keyValueService, + Lazy? contentTypeBaseServiceProvider) { - private readonly Lazy? _publicAccessService; - private readonly Lazy? _domainService; - private readonly Lazy? _auditService; - private readonly Lazy? _localizedTextService; - private readonly Lazy? _tagService; - private readonly Lazy? _contentService; - private readonly Lazy? _userService; - private readonly Lazy? _memberService; - private readonly Lazy? _mediaService; - private readonly Lazy? _contentTypeService; - private readonly Lazy? _mediaTypeService; - private readonly Lazy? _dataTypeService; - private readonly Lazy? _fileService; - private readonly Lazy? _localizationService; - private readonly Lazy? _packagingService; - private readonly Lazy? _serverRegistrationService; - private readonly Lazy? _entityService; - private readonly Lazy? _relationService; - private readonly Lazy? _macroService; - private readonly Lazy? _memberTypeService; - private readonly Lazy? _memberGroupService; - private readonly Lazy? _notificationService; - private readonly Lazy? _externalLoginService; - private readonly Lazy? _redirectUrlService; - private readonly Lazy? _consentService; - private readonly Lazy? _keyValueService; - private readonly Lazy? _contentTypeBaseServiceProvider; + _publicAccessService = publicAccessService; + _domainService = domainService; + _auditService = auditService; + _localizedTextService = localizedTextService; + _tagService = tagService; + _contentService = contentService; + _userService = userService; + _memberService = memberService; + _mediaService = mediaService; + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _dataTypeService = dataTypeService; + _fileService = fileService; + _localizationService = localizationService; + _packagingService = packagingService; + _serverRegistrationService = serverRegistrationService; + _entityService = entityService; + _relationService = relationService; + _macroService = macroService; + _memberTypeService = memberTypeService; + _memberGroupService = memberGroupService; + _notificationService = notificationService; + _externalLoginService = externalLoginService; + _redirectUrlService = redirectUrlService; + _consentService = consentService; + _keyValueService = keyValueService; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + } - /// - /// Initializes a new instance of the class with lazy services. - /// - public ServiceContext(Lazy? publicAccessService, Lazy? domainService, Lazy? auditService, Lazy? localizedTextService, Lazy? tagService, Lazy? contentService, Lazy? userService, Lazy? memberService, Lazy? mediaService, Lazy? contentTypeService, Lazy? mediaTypeService, Lazy? dataTypeService, Lazy? fileService, Lazy? localizationService, Lazy? packagingService, Lazy? serverRegistrationService, Lazy? entityService, Lazy? relationService, Lazy? macroService, Lazy? memberTypeService, Lazy? memberGroupService, Lazy? notificationService, Lazy? externalLoginService, Lazy? redirectUrlService, Lazy? consentService, Lazy? keyValueService, Lazy? contentTypeBaseServiceProvider) + /// + /// Gets the + /// + public IPublicAccessService? PublicAccessService => _publicAccessService?.Value; + + /// + /// Gets the + /// + public IDomainService? DomainService => _domainService?.Value; + + /// + /// Gets the + /// + public IAuditService? AuditService => _auditService?.Value; + + /// + /// Gets the + /// + public ILocalizedTextService? TextService => _localizedTextService?.Value; + + /// + /// Gets the + /// + public INotificationService? NotificationService => _notificationService?.Value; + + /// + /// Gets the + /// + public IServerRegistrationService? ServerRegistrationService => _serverRegistrationService?.Value; + + /// + /// Gets the + /// + public ITagService? TagService => _tagService?.Value; + + /// + /// Gets the + /// + public IMacroService? MacroService => _macroService?.Value; + + /// + /// Gets the + /// + public IEntityService? EntityService => _entityService?.Value; + + /// + /// Gets the + /// + public IRelationService? RelationService => _relationService?.Value; + + /// + /// Gets the + /// + public IContentService? ContentService => _contentService?.Value; + + /// + /// Gets the + /// + public IContentTypeService? ContentTypeService => _contentTypeService?.Value; + + /// + /// Gets the + /// + public IMediaTypeService? MediaTypeService => _mediaTypeService?.Value; + + /// + /// Gets the + /// + public IDataTypeService? DataTypeService => _dataTypeService?.Value; + + /// + /// Gets the + /// + public IFileService? FileService => _fileService?.Value; + + /// + /// Gets the + /// + public ILocalizationService? LocalizationService => _localizationService?.Value; + + /// + /// Gets the + /// + public IMediaService? MediaService => _mediaService?.Value; + + /// + /// Gets the + /// + public IPackagingService? PackagingService => _packagingService?.Value; + + /// + /// Gets the + /// + public IUserService? UserService => _userService?.Value; + + /// + /// Gets the + /// + public IMemberService? MemberService => _memberService?.Value; + + /// + /// Gets the MemberTypeService + /// + public IMemberTypeService? MemberTypeService => _memberTypeService?.Value; + + /// + /// Gets the MemberGroupService + /// + public IMemberGroupService? MemberGroupService => _memberGroupService?.Value; + + /// + /// Gets the ExternalLoginService. + /// + public IExternalLoginService? ExternalLoginService => _externalLoginService?.Value; + + /// + /// Gets the RedirectUrlService. + /// + public IRedirectUrlService? RedirectUrlService => _redirectUrlService?.Value; + + /// + /// Gets the ConsentService. + /// + public IConsentService? ConsentService => _consentService?.Value; + + /// + /// Gets the KeyValueService. + /// + public IKeyValueService? KeyValueService => _keyValueService?.Value; + + /// + /// Gets the ContentTypeServiceBaseFactory. + /// + public IContentTypeBaseServiceProvider? ContentTypeBaseServices => _contentTypeBaseServiceProvider?.Value; + + /// + /// Creates a partial service context with only some services (for tests). + /// + /// + /// Using a true constructor for this confuses DI containers. + /// + public static ServiceContext CreatePartial( + IContentService? contentService = null, + IMediaService? mediaService = null, + IContentTypeService? contentTypeService = null, + IMediaTypeService? mediaTypeService = null, + IDataTypeService? dataTypeService = null, + IFileService? fileService = null, + ILocalizationService? localizationService = null, + IPackagingService? packagingService = null, + IEntityService? entityService = null, + IRelationService? relationService = null, + IMemberGroupService? memberGroupService = null, + IMemberTypeService? memberTypeService = null, + IMemberService? memberService = null, + IUserService? userService = null, + ITagService? tagService = null, + INotificationService? notificationService = null, + ILocalizedTextService? localizedTextService = null, + IAuditService? auditService = null, + IDomainService? domainService = null, + IMacroService? macroService = null, + IPublicAccessService? publicAccessService = null, + IExternalLoginService? externalLoginService = null, + IServerRegistrationService? serverRegistrationService = null, + IRedirectUrlService? redirectUrlService = null, + IConsentService? consentService = null, + IKeyValueService? keyValueService = null, + IContentTypeBaseServiceProvider? contentTypeBaseServiceProvider = null) + { + Lazy? Lazy(T? service) { - _publicAccessService = publicAccessService; - _domainService = domainService; - _auditService = auditService; - _localizedTextService = localizedTextService; - _tagService = tagService; - _contentService = contentService; - _userService = userService; - _memberService = memberService; - _mediaService = mediaService; - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _dataTypeService = dataTypeService; - _fileService = fileService; - _localizationService = localizationService; - _packagingService = packagingService; - _serverRegistrationService = serverRegistrationService; - _entityService = entityService; - _relationService = relationService; - _macroService = macroService; - _memberTypeService = memberTypeService; - _memberGroupService = memberGroupService; - _notificationService = notificationService; - _externalLoginService = externalLoginService; - _redirectUrlService = redirectUrlService; - _consentService = consentService; - _keyValueService = keyValueService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + return service == null ? null : new Lazy(() => service); } - /// - /// Creates a partial service context with only some services (for tests). - /// - /// - /// Using a true constructor for this confuses DI containers. - /// - public static ServiceContext CreatePartial( - IContentService? contentService = null, - IMediaService? mediaService = null, - IContentTypeService? contentTypeService = null, - IMediaTypeService? mediaTypeService = null, - IDataTypeService? dataTypeService = null, - IFileService? fileService = null, - ILocalizationService? localizationService = null, - IPackagingService? packagingService = null, - IEntityService? entityService = null, - IRelationService? relationService = null, - IMemberGroupService? memberGroupService = null, - IMemberTypeService? memberTypeService = null, - IMemberService? memberService = null, - IUserService? userService = null, - ITagService? tagService = null, - INotificationService? notificationService = null, - ILocalizedTextService? localizedTextService = null, - IAuditService? auditService = null, - IDomainService? domainService = null, - IMacroService? macroService = null, - IPublicAccessService? publicAccessService = null, - IExternalLoginService? externalLoginService = null, - IServerRegistrationService? serverRegistrationService = null, - IRedirectUrlService? redirectUrlService = null, - IConsentService? consentService = null, - IKeyValueService? keyValueService = null, - IContentTypeBaseServiceProvider? contentTypeBaseServiceProvider = null) - { - Lazy? Lazy(T? service) => service == null ? null : new Lazy(() => service); - - return new ServiceContext( - Lazy(publicAccessService), - Lazy(domainService), - Lazy(auditService), - Lazy(localizedTextService), - Lazy(tagService), - Lazy(contentService), - Lazy(userService), - Lazy(memberService), - Lazy(mediaService), - Lazy(contentTypeService), - Lazy(mediaTypeService), - Lazy(dataTypeService), - Lazy(fileService), - Lazy(localizationService), - Lazy(packagingService), - Lazy(serverRegistrationService), - Lazy(entityService), - Lazy(relationService), - Lazy(macroService), - Lazy(memberTypeService), - Lazy(memberGroupService), - Lazy(notificationService), - Lazy(externalLoginService), - Lazy(redirectUrlService), - Lazy(consentService), - Lazy(keyValueService), - Lazy(contentTypeBaseServiceProvider) - ); - } - - /// - /// Gets the - /// - public IPublicAccessService? PublicAccessService => _publicAccessService?.Value; - - /// - /// Gets the - /// - public IDomainService? DomainService => _domainService?.Value; - - /// - /// Gets the - /// - public IAuditService? AuditService => _auditService?.Value; - - /// - /// Gets the - /// - public ILocalizedTextService? TextService => _localizedTextService?.Value; - - /// - /// Gets the - /// - public INotificationService? NotificationService => _notificationService?.Value; - - /// - /// Gets the - /// - public IServerRegistrationService? ServerRegistrationService => _serverRegistrationService?.Value; - - /// - /// Gets the - /// - public ITagService? TagService => _tagService?.Value; - - /// - /// Gets the - /// - public IMacroService? MacroService => _macroService?.Value; - - /// - /// Gets the - /// - public IEntityService? EntityService => _entityService?.Value; - - /// - /// Gets the - /// - public IRelationService? RelationService => _relationService?.Value; - - /// - /// Gets the - /// - public IContentService? ContentService => _contentService?.Value; - - /// - /// Gets the - /// - public IContentTypeService? ContentTypeService => _contentTypeService?.Value; - - /// - /// Gets the - /// - public IMediaTypeService? MediaTypeService => _mediaTypeService?.Value; - - /// - /// Gets the - /// - public IDataTypeService? DataTypeService => _dataTypeService?.Value; - - /// - /// Gets the - /// - public IFileService? FileService => _fileService?.Value; - - /// - /// Gets the - /// - public ILocalizationService? LocalizationService => _localizationService?.Value; - - /// - /// Gets the - /// - public IMediaService? MediaService => _mediaService?.Value; - - /// - /// Gets the - /// - public IPackagingService? PackagingService => _packagingService?.Value; - - /// - /// Gets the - /// - public IUserService? UserService => _userService?.Value; - - /// - /// Gets the - /// - public IMemberService? MemberService => _memberService?.Value; - - /// - /// Gets the MemberTypeService - /// - public IMemberTypeService? MemberTypeService => _memberTypeService?.Value; - - /// - /// Gets the MemberGroupService - /// - public IMemberGroupService? MemberGroupService => _memberGroupService?.Value; - - /// - /// Gets the ExternalLoginService. - /// - public IExternalLoginService? ExternalLoginService => _externalLoginService?.Value; - - /// - /// Gets the RedirectUrlService. - /// - public IRedirectUrlService? RedirectUrlService => _redirectUrlService?.Value; - - /// - /// Gets the ConsentService. - /// - public IConsentService? ConsentService => _consentService?.Value; - - /// - /// Gets the KeyValueService. - /// - public IKeyValueService? KeyValueService => _keyValueService?.Value; - - /// - /// Gets the ContentTypeServiceBaseFactory. - /// - public IContentTypeBaseServiceProvider? ContentTypeBaseServices => _contentTypeBaseServiceProvider?.Value; + return new ServiceContext( + Lazy(publicAccessService), + Lazy(domainService), + Lazy(auditService), + Lazy(localizedTextService), + Lazy(tagService), + Lazy(contentService), + Lazy(userService), + Lazy(memberService), + Lazy(mediaService), + Lazy(contentTypeService), + Lazy(mediaTypeService), + Lazy(dataTypeService), + Lazy(fileService), + Lazy(localizationService), + Lazy(packagingService), + Lazy(serverRegistrationService), + Lazy(entityService), + Lazy(relationService), + Lazy(macroService), + Lazy(memberTypeService), + Lazy(memberGroupService), + Lazy(notificationService), + Lazy(externalLoginService), + Lazy(redirectUrlService), + Lazy(consentService), + Lazy(keyValueService), + Lazy(contentTypeBaseServiceProvider)); } } diff --git a/src/Umbraco.Core/Services/TagService.cs b/src/Umbraco.Core/Services/TagService.cs index 65e4a32f9e..c75863f6de 100644 --- a/src/Umbraco.Core/Services/TagService.cs +++ b/src/Umbraco.Core/Services/TagService.cs @@ -1,172 +1,167 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & +/// saved media or members +/// +/// +/// If there is unpublished content with tags, those tags will not be contained +/// +public class TagService : RepositoryService, ITagService { - /// - /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members - /// - /// - /// If there is unpublished content with tags, those tags will not be contained - /// - public class TagService : RepositoryService, ITagService + private readonly ITagRepository _tagRepository; + + public TagService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, ITagRepository tagRepository) + : base(provider, loggerFactory, eventMessagesFactory) => + _tagRepository = tagRepository; + + /// + public TaggedEntity? GetTaggedEntityById(int id) { - private readonly ITagRepository _tagRepository; - - public TagService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - ITagRepository tagRepository) - : base(provider, loggerFactory, eventMessagesFactory) + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - _tagRepository = tagRepository; + return _tagRepository.GetTaggedEntityById(id); } + } - /// - public TaggedEntity? GetTaggedEntityById(int id) + /// + public TaggedEntity? GetTaggedEntityByKey(Guid key) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntityById(id); - } + return _tagRepository.GetTaggedEntityByKey(key); } + } - /// - public TaggedEntity? GetTaggedEntityByKey(Guid key) + /// + public IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntityByKey(key); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); } + } - /// - public IEnumerable GetTaggedContentByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedContentByTag(string tag, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); } + } - /// - public IEnumerable GetTaggedContentByTag(string tag, string? group = null, string? culture = null) + /// + public IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, tag, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); } + } - /// - public IEnumerable GetTaggedMediaByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedMediaByTag(string tag, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Media, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); } + } - /// - public IEnumerable GetTaggedMediaByTag(string tag, string? group = null, string? culture = null) + /// + public IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Media, tag, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); } + } - /// - public IEnumerable GetTaggedMembersByTagGroup(string group, string? culture = null) + /// + public IEnumerable GetTaggedMembersByTag(string tag, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Member, group, culture); - } + return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); } + } - /// - public IEnumerable GetTaggedMembersByTag(string tag, string? group = null, string? culture = null) + /// + public IEnumerable GetAllTags(string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTaggedEntitiesByTag(TaggableObjectTypes.Member, tag, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); } + } - /// - public IEnumerable GetAllTags(string? group = null, string? culture = null) + /// + public IEnumerable GetAllContentTags(string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.All, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture); } + } - /// - public IEnumerable GetAllContentTags(string? group = null, string? culture = null) + /// + public IEnumerable GetAllMediaTags(string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Content, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture); } + } - /// - public IEnumerable GetAllMediaTags(string? group = null, string? culture = null) + /// + public IEnumerable GetAllMemberTags(string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Media, group, culture); - } + return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture); } + } - /// - public IEnumerable GetAllMemberTags(string? group = null, string? culture = null) + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntityType(TaggableObjectTypes.Member, group, culture); - } + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } + } - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, string? culture = null) + /// + public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); - } + return _tagRepository.GetTagsForEntity(contentId, group, culture); } + } - /// - public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntity(contentId, group, culture); - } + return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); } + } - /// - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, string? culture = null) + /// + public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForProperty(contentId, propertyTypeAlias, group, culture); - } - } - - /// - public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) - { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _tagRepository.GetTagsForEntity(contentId, group, culture); - } + return _tagRepository.GetTagsForEntity(contentId, group, culture); } } } diff --git a/src/Umbraco.Core/Services/TrackedReferencesService.cs b/src/Umbraco.Core/Services/TrackedReferencesService.cs index ab5a09ce8b..32dc9c18cc 100644 --- a/src/Umbraco.Core/Services/TrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs @@ -2,58 +2,60 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class TrackedReferencesService : ITrackedReferencesService { - public class TrackedReferencesService : ITrackedReferencesService + private readonly IEntityService _entityService; + private readonly ICoreScopeProvider _scopeProvider; + private readonly ITrackedReferencesRepository _trackedReferencesRepository; + + public TrackedReferencesService( + ITrackedReferencesRepository trackedReferencesRepository, + ICoreScopeProvider scopeProvider, + IEntityService entityService) { - private readonly ITrackedReferencesRepository _trackedReferencesRepository; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IEntityService _entityService; + _trackedReferencesRepository = trackedReferencesRepository; + _scopeProvider = scopeProvider; + _entityService = entityService; + } - public TrackedReferencesService(ITrackedReferencesRepository trackedReferencesRepository, ICoreScopeProvider scopeProvider, IEntityService entityService) - { - _trackedReferencesRepository = trackedReferencesRepository; - _scopeProvider = scopeProvider; - _entityService = entityService; - } + /// + /// Gets a paged result of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + public PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + IEnumerable items = _trackedReferencesRepository.GetPagedRelationsForItem(id, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); - /// - /// Gets a paged result of items which are in relation with the current item. - /// Basically, shows the items which depend on the current item. - /// - public PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - var items = _trackedReferencesRepository.GetPagedRelationsForItem(id, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; + } - return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; - } + /// + /// Gets a paged result of items used in any kind of relation from selected integer ids. + /// + public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + IEnumerable items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); - /// - /// Gets a paged result of items used in any kind of relation from selected integer ids. - /// - public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; + } - return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; - } + /// + /// Gets a paged result of the descending items that have any references, given a parent id. + /// + public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - /// - /// Gets a paged result of the descending items that have any references, given a parent id. - /// - public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - - var items = _trackedReferencesRepository.GetPagedDescendantsInReferences( - parentId, - pageIndex, - pageSize, - filterMustBeIsDependency, - out var totalItems); - return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; - } + IEnumerable items = _trackedReferencesRepository.GetPagedDescendantsInReferences( + parentId, + pageIndex, + pageSize, + filterMustBeIsDependency, + out var totalItems); + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; } } diff --git a/src/Umbraco.Core/Services/TreeService.cs b/src/Umbraco.Core/Services/TreeService.cs index f325712d77..3b2b5f3618 100644 --- a/src/Umbraco.Core/Services/TreeService.cs +++ b/src/Umbraco.Core/Services/TreeService.cs @@ -1,45 +1,41 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Trees; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Implements . +/// +public class TreeService : ITreeService { + private readonly TreeCollection _treeCollection; + /// - /// Implements . + /// Initializes a new instance of the class. /// - public class TreeService : ITreeService - { - private readonly TreeCollection _treeCollection; + /// + public TreeService(TreeCollection treeCollection) => _treeCollection = treeCollection; - /// - /// Initializes a new instance of the class. - /// - /// - public TreeService(TreeCollection treeCollection) - { - _treeCollection = treeCollection; - } + /// + public Tree? GetByAlias(string treeAlias) => _treeCollection.FirstOrDefault(x => x.TreeAlias == treeAlias); - /// - public Tree? GetByAlias(string treeAlias) => _treeCollection.FirstOrDefault(x => x.TreeAlias == treeAlias); + /// + public IEnumerable GetAll(TreeUse use = TreeUse.Main) - /// - public IEnumerable GetAll(TreeUse use = TreeUse.Main) - // use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees - => _treeCollection.Where(x => x.TreeUse.HasFlagAny(use)); + // use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees + => _treeCollection.Where(x => x.TreeUse.HasFlagAny(use)); - /// - public IEnumerable GetBySection(string sectionAlias, TreeUse use = TreeUse.Main) - // use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees - => _treeCollection.Where(x => x.SectionAlias.InvariantEquals(sectionAlias) && x.TreeUse.HasFlagAny(use)).OrderBy(x => x.SortOrder).ToList(); + /// + public IEnumerable GetBySection(string sectionAlias, TreeUse use = TreeUse.Main) - /// - public IDictionary> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main) - { - return GetBySection(sectionAlias, use).GroupBy(x => x.TreeGroup).ToDictionary( - x => x.Key ?? "", - x => (IEnumerable) x.ToArray()); - } - } + // use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees + => _treeCollection.Where(x => x.SectionAlias.InvariantEquals(sectionAlias) && x.TreeUse.HasFlagAny(use)) + .OrderBy(x => x.SortOrder).ToList(); + + /// + public IDictionary> + GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main) => + GetBySection(sectionAlias, use).GroupBy(x => x.TreeGroup).ToDictionary( + x => x.Key ?? string.Empty, + x => (IEnumerable)x.ToArray()); } diff --git a/src/Umbraco.Core/Services/TwoFactorLoginService.cs b/src/Umbraco.Core/Services/TwoFactorLoginService.cs index 7a4feb91fb..de79284ac9 100644 --- a/src/Umbraco.Core/Services/TwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/TwoFactorLoginService.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -12,216 +8,212 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +public class TwoFactorLoginService : ITwoFactorLoginService2 { - /// - public class TwoFactorLoginService : ITwoFactorLoginService2 + private readonly IOptions _backOfficeIdentityOptions; + private readonly IOptions _identityOptions; + private readonly ILogger _logger; + private readonly ICoreScopeProvider _scopeProvider; + private readonly ITwoFactorLoginRepository _twoFactorLoginRepository; + private readonly IDictionary _twoFactorSetupGenerators; + + /// + /// Initializes a new instance of the class. + /// + public TwoFactorLoginService( + ITwoFactorLoginRepository twoFactorLoginRepository, + ICoreScopeProvider scopeProvider, + IEnumerable twoFactorSetupGenerators, + IOptions identityOptions, + IOptions backOfficeIdentityOptions, + ILogger logger) { - private readonly ITwoFactorLoginRepository _twoFactorLoginRepository; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IOptions _identityOptions; - private readonly IOptions _backOfficeIdentityOptions; - private readonly IDictionary _twoFactorSetupGenerators; - private readonly ILogger _logger; + _twoFactorLoginRepository = twoFactorLoginRepository; + _scopeProvider = scopeProvider; + _identityOptions = identityOptions; + _backOfficeIdentityOptions = backOfficeIdentityOptions; + _logger = logger; + _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x => x.ProviderName); + } - /// - /// Initializes a new instance of the class. - /// - public TwoFactorLoginService( - ITwoFactorLoginRepository twoFactorLoginRepository, - ICoreScopeProvider scopeProvider, - IEnumerable twoFactorSetupGenerators, - IOptions identityOptions, - IOptions backOfficeIdentityOptions, - ILogger logger) - { - _twoFactorLoginRepository = twoFactorLoginRepository; - _scopeProvider = scopeProvider; - _identityOptions = identityOptions; - _backOfficeIdentityOptions = backOfficeIdentityOptions; - _logger = logger; - _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x =>x.ProviderName); - } - - [Obsolete("Use ctor with all params - This will be removed in v11")] - public TwoFactorLoginService( - ITwoFactorLoginRepository twoFactorLoginRepository, - ICoreScopeProvider scopeProvider, - IEnumerable twoFactorSetupGenerators, - IOptions identityOptions, - IOptions backOfficeIdentityOptions) - : this(twoFactorLoginRepository, + [Obsolete("Use ctor with all params - This will be removed in v11")] + public TwoFactorLoginService( + ITwoFactorLoginRepository twoFactorLoginRepository, + ICoreScopeProvider scopeProvider, + IEnumerable twoFactorSetupGenerators, + IOptions identityOptions, + IOptions backOfficeIdentityOptions) + : this( + twoFactorLoginRepository, scopeProvider, twoFactorSetupGenerators, identityOptions, backOfficeIdentityOptions, StaticServiceProvider.Instance.GetRequiredService>()) - { + { + } + /// + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); + } + + /// + public async Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey) => + await GetEnabledProviderNamesAsync(userOrMemberKey); + + public async Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code) + { + var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); + + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); } - /// - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) + var isValid = secret is not null && generator.ValidateTwoFactorPIN(secret, code); + + if (!isValid) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); + return false; } - /// - public async Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey) + return await DisableAsync(userOrMemberKey, providerName); + } + + public async Task ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code) + { + try { - return await GetEnabledProviderNamesAsync(userOrMemberKey); - } - - public async Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code) - { - var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); - - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) - { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); - } - - var isValid = secret is not null && generator.ValidateTwoFactorPIN(secret, code); - - if (!isValid) + var isValid = ValidateTwoFactorSetup(providerName, secret, code); + if (isValid == false) { return false; } - return await DisableAsync(userOrMemberKey, providerName); + var twoFactorLogin = new TwoFactorLogin + { + Confirmed = true, + Secret = secret, + UserOrMemberKey = userOrMemberKey, + ProviderName = providerName, + }; + + await SaveAsync(twoFactorLogin); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not log in with the provided one-time-password"); } - public async Task ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code) + return false; + } + + /// + public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) => + (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); + + /// + public async Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) + .FirstOrDefault(x => x.ProviderName == providerName)?.Secret; + } + + /// + public async Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName) + { + var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); + + // Dont allow to generate a new secrets if user already has one + if (!string.IsNullOrEmpty(secret)) { + return default; + } - try - { - var isValid = ValidateTwoFactorSetup(providerName, secret, code); - if (isValid == false) - { - return false; - } + secret = GenerateSecret(); - var twoFactorLogin = new TwoFactorLogin() - { - Confirmed = true, - Secret = secret, - UserOrMemberKey = userOrMemberKey, - ProviderName = providerName - }; + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + } - await SaveAsync(twoFactorLogin); + return await generator.GetSetupDataAsync(userOrMemberKey, secret); + } - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not log in with the provided one-time-password"); - } + /// + public IEnumerable GetAllProviderNames() => _twoFactorSetupGenerators.Keys; + /// + public async Task DisableAsync(Guid userOrMemberKey, string providerName) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); + } + + /// + public bool ValidateTwoFactorSetup(string providerName, string secret, string code) + { + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + } + + return generator.ValidateTwoFactorSetup(secret, code); + } + + /// + public Task SaveAsync(TwoFactorLogin twoFactorLogin) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + _twoFactorLoginRepository.Save(twoFactorLogin); + + return Task.CompletedTask; + } + + /// + /// Generates a new random unique secret. + /// + /// The random secret + protected virtual string GenerateSecret() => Guid.NewGuid().ToString(); + + private async Task> GetEnabledProviderNamesAsync(Guid userOrMemberKey) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) + .Select(x => x.ProviderName).ToArray(); + + return providersOnUser.Where(IsKnownProviderName); + } + + /// + /// The provider needs to be registered as either a member provider or backoffice provider to show up. + /// + private bool IsKnownProviderName(string? providerName) + { + if (providerName is null) + { return false; } - private async Task> GetEnabledProviderNamesAsync(Guid userOrMemberKey) + if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) - .Select(x => x.ProviderName).ToArray(); - - return providersOnUser.Where(IsKnownProviderName)!; + return true; } - /// - /// The provider needs to be registered as either a member provider or backoffice provider to show up. - /// - private bool IsKnownProviderName(string? providerName) + if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) { - if (providerName is null) - { - return false; - } - if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) - { - return true; - } - - if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) - { - return true; - } - - return false; + return true; } - /// - public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) - { - return (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); - } - - /// - public async Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x => x.ProviderName == providerName)?.Secret; - } - - /// - public async Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName) - { - var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); - - // Dont allow to generate a new secrets if user already has one - if (!string.IsNullOrEmpty(secret)) - { - return default; - } - - secret = GenerateSecret(); - - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) - { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); - } - - return await generator.GetSetupDataAsync(userOrMemberKey, secret); - } - - /// - public IEnumerable GetAllProviderNames() => _twoFactorSetupGenerators.Keys; - - /// - public async Task DisableAsync(Guid userOrMemberKey, string providerName) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); - } - - /// - public bool ValidateTwoFactorSetup(string providerName, string secret, string code) - { - if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider? generator)) - { - throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); - } - - return generator.ValidateTwoFactorSetup(secret, code); - } - - /// - public Task SaveAsync(TwoFactorLogin twoFactorLogin) - { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - _twoFactorLoginRepository.Save(twoFactorLogin); - - return Task.CompletedTask; - } - - /// - /// Generates a new random unique secret. - /// - /// The random secret - protected virtual string GenerateSecret() => Guid.NewGuid().ToString(); + return false; } } diff --git a/src/Umbraco.Core/Services/UpgradeService.cs b/src/Umbraco.Core/Services/UpgradeService.cs index e2003f8370..7a5269d2bf 100644 --- a/src/Umbraco.Core/Services/UpgradeService.cs +++ b/src/Umbraco.Core/Services/UpgradeService.cs @@ -1,21 +1,15 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +public class UpgradeService : IUpgradeService { - public class UpgradeService : IUpgradeService - { - private readonly IUpgradeCheckRepository _upgradeCheckRepository; + private readonly IUpgradeCheckRepository _upgradeCheckRepository; - public UpgradeService(IUpgradeCheckRepository upgradeCheckRepository) - { - _upgradeCheckRepository = upgradeCheckRepository; - } + public UpgradeService(IUpgradeCheckRepository upgradeCheckRepository) => + _upgradeCheckRepository = upgradeCheckRepository; - public async Task CheckUpgrade(SemVersion version) - { - return await _upgradeCheckRepository.CheckUpgradeAsync(version); - } - } + public async Task CheckUpgrade(SemVersion version) => + await _upgradeCheckRepository.CheckUpgradeAsync(version); } diff --git a/src/Umbraco.Core/Services/UserDataService.cs b/src/Umbraco.Core/Services/UserDataService.cs index a3c6bd11b4..14b2e581f9 100644 --- a/src/Umbraco.Core/Services/UserDataService.cs +++ b/src/Umbraco.Core/Services/UserDataService.cs @@ -1,51 +1,45 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Runtime.InteropServices; -using System.Threading; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +[Obsolete("Use the IUserDataService interface instead")] +public class UserDataService : IUserDataService { - [Obsolete("Use the IUserDataService interface instead")] - public class UserDataService : IUserDataService + private readonly ILocalizationService _localizationService; + private readonly IUmbracoVersion _version; + + public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) { - private readonly IUmbracoVersion _version; - private readonly ILocalizationService _localizationService; - - - public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) - { - _version = version; - _localizationService = localizationService; - } - - public IEnumerable GetUserData() => - new List - { - new("Server OS", RuntimeInformation.OSDescription), - new("Server Framework", RuntimeInformation.FrameworkDescription), - new("Default Language", _localizationService.GetDefaultLanguageIsoCode()), - new("Umbraco Version", _version.SemanticVersion.ToSemanticStringWithoutBuild()), - new("Current Culture", Thread.CurrentThread.CurrentCulture.ToString()), - new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()), - new("Current Webserver", GetCurrentWebServer()) - }; - - private string GetCurrentWebServer() => IsRunningInProcessIIS() ? "IIS" : "Kestrel"; - - public bool IsRunningInProcessIIS() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return false; - } - - string processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); - return (processName.Contains("w3wp") || processName.Contains("iisexpress")); - } + _version = version; + _localizationService = localizationService; } + + public IEnumerable GetUserData() => + new List + { + new("Server OS", RuntimeInformation.OSDescription), + new("Server Framework", RuntimeInformation.FrameworkDescription), + new("Default Language", _localizationService.GetDefaultLanguageIsoCode()), + new("Umbraco Version", _version.SemanticVersion.ToSemanticStringWithoutBuild()), + new("Current Culture", Thread.CurrentThread.CurrentCulture.ToString()), + new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()), + new("Current Webserver", GetCurrentWebServer()), + }; + + public bool IsRunningInProcessIIS() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return false; + } + + var processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); + return processName.Contains("w3wp") || processName.Contains("iisexpress"); + } + + private string GetCurrentWebServer() => IsRunningInProcessIIS() ? "IIS" : "Kestrel"; } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index f0b5cc6a32..88e2708b2c 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; using System.Data.Common; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,1159 +13,1311 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services; + +/// +/// Represents the UserService, which is an easy access to operations involving , +/// and eventually Backoffice Users. +/// +internal class UserService : RepositoryService, IUserService { - /// - /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. - /// - internal class UserService : RepositoryService, IUserService + private readonly GlobalSettings _globalSettings; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IUserGroupRepository _userGroupRepository; + private readonly IUserRepository _userRepository; + + public UserService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IRuntimeState runtimeState, + IUserRepository userRepository, + IUserGroupRepository userGroupRepository, + IOptions globalSettings) + : base(provider, loggerFactory, eventMessagesFactory) { - private readonly IRuntimeState _runtimeState; - private readonly IUserRepository _userRepository; - private readonly IUserGroupRepository _userGroupRepository; - private readonly GlobalSettings _globalSettings; - private readonly ILogger _logger; + _runtimeState = runtimeState; + _userRepository = userRepository; + _userGroupRepository = userGroupRepository; + _globalSettings = globalSettings.Value; + _logger = loggerFactory.CreateLogger(); + } - public UserService(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IRuntimeState runtimeState, - IUserRepository userRepository, IUserGroupRepository userGroupRepository, IOptions globalSettings) - : base(provider, loggerFactory, eventMessagesFactory) + private bool IsUpgrading => + _runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade; + + /// + /// Checks in a set of permissions associated with a user for those related to a given nodeId + /// + /// The set of permissions + /// The node Id + /// The permissions to return + /// True if permissions for the given path are found + public static bool TryGetAssignedPermissionsForNode( + IList permissions, + int nodeId, + out string assignedPermissions) + { + if (permissions.Any(x => x.EntityId == nodeId)) { - _runtimeState = runtimeState; - _userRepository = userRepository; - _userGroupRepository = userGroupRepository; - _globalSettings = globalSettings.Value; - _logger = loggerFactory.CreateLogger(); - } + EntityPermission found = permissions.First(x => x.EntityId == nodeId); + var assignedPermissionsArray = found.AssignedPermissions.ToList(); - private bool IsUpgrading => _runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade; - - #region Implementation of IMembershipUserService - - /// - /// Checks if a User with the username exists - /// - /// Username to check - /// True if the User exists otherwise False - public bool Exists(string username) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + // Working with permissions assigned directly to a user AND to their groups, so maybe several per node + // and we need to get the most permissive set + foreach (EntityPermission permission in permissions.Where(x => x.EntityId == nodeId).Skip(1)) { - return _userRepository.ExistsByUserName(username); + AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); } + + assignedPermissions = string.Join(string.Empty, assignedPermissionsArray); + return true; } - /// - /// Creates a new User - /// - /// The user will be saved in the database and returned with an Id - /// Username of the user to create - /// Email of the user to create - /// - public IUser CreateUserWithIdentity(string username, string email) + assignedPermissions = string.Empty; + return false; + } + + #region Implementation of IMembershipUserService + + /// + /// Checks if a User with the username exists + /// + /// Username to check + /// True if the User exists otherwise False + public bool Exists(string username) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - return CreateUserWithIdentity(username, email, string.Empty); + return _userRepository.ExistsByUserName(username); + } + } + + /// + /// Creates a new User + /// + /// The user will be saved in the database and returned with an Id + /// Username of the user to create + /// Email of the user to create + /// + /// + /// + public IUser CreateUserWithIdentity(string username, string email) => + CreateUserWithIdentity(username, email, string.Empty); + + /// + /// Creates and persists a new + /// + /// Username of the to create + /// Email of the to create + /// + /// This value should be the encoded/encrypted/hashed value for the password that will be + /// stored in the database + /// + /// Not used for users + /// + /// + /// + IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) => CreateUserWithIdentity(username, email, passwordValue); + + /// + /// Creates and persists a new + /// + /// Username of the to create + /// Email of the to create + /// + /// This value should be the encoded/encrypted/hashed value for the password that will be + /// stored in the database + /// + /// Alias of the Type + /// Is the member approved + /// + /// + /// + IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) => CreateUserWithIdentity(username, email, passwordValue, isApproved); + + /// + /// Gets a User by its integer id + /// + /// Id + /// + /// + /// + public IUser? GetById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.Get(id); + } + } + + /// + /// Creates and persists a Member + /// + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// + /// Username of the Member to create + /// Email of the Member to create + /// + /// This value should be the encoded/encrypted/hashed value for the password that will be + /// stored in the database + /// + /// Is the user approved + /// + /// + /// + private IUser CreateUserWithIdentity(string username, string email, string passwordValue, bool isApproved = true) + { + if (username == null) + { + throw new ArgumentNullException(nameof(username)); } - /// - /// Creates and persists a new - /// - /// Username of the to create - /// Email of the to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Not used for users - /// - IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) + if (string.IsNullOrWhiteSpace(username)) { - return CreateUserWithIdentity(username, email, passwordValue); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(username)); } - /// - /// Creates and persists a new - /// - /// Username of the to create - /// Email of the to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Alias of the Type - /// Is the member approved - /// - IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) + EventMessages evtMsgs = EventMessagesFactory.Get(); + + // TODO: PUT lock here!! + User user; + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - return CreateUserWithIdentity(username, email, passwordValue, isApproved); - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Is the user approved - /// - private IUser CreateUserWithIdentity(string username, string email, string passwordValue, bool isApproved = true) - { - if (username == null) throw new ArgumentNullException(nameof(username)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(username)); - - var evtMsgs = EventMessagesFactory.Get(); - - // TODO: PUT lock here!! - - User user; - using (var scope = ScopeProvider.CreateCoreScope()) + var loginExists = _userRepository.ExistsByLogin(username); + if (loginExists) { - var loginExists = _userRepository.ExistsByLogin(username); - if (loginExists) - throw new ArgumentException("Login already exists"); // causes rollback + throw new ArgumentException("Login already exists"); // causes rollback + } - user = new User(_globalSettings) - { - Email = email, - Language = _globalSettings.DefaultUILanguage, - Name = username, - RawPasswordValue = passwordValue, - Username = username, - IsLockedOut = false, - IsApproved = isApproved - }; + user = new User(_globalSettings) + { + Email = email, + Language = _globalSettings.DefaultUILanguage, + Name = username, + RawPasswordValue = passwordValue, + Username = username, + IsLockedOut = false, + IsApproved = isApproved, + }; - var savingNotification = new UserSavingNotification(user, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return user; - } - - _userRepository.Save(user); - - scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); + var savingNotification = new UserSavingNotification(user, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { scope.Complete(); + return user; } - return user; + _userRepository.Save(user); + + scope.Notifications.Publish(new UserSavedNotification(user, evtMsgs).WithStateFrom(savingNotification)); + scope.Complete(); } - /// - /// Gets a User by its integer id - /// - /// Id - /// - public IUser? GetById(int id) + return user; + } + + /// + /// Gets an by its provider key + /// + /// Id to use for retrieval + /// + /// + /// + public IUser? GetByProviderKey(object id) + { + Attempt asInt = id.TryConvertTo(); + return asInt.Success ? GetById(asInt.Result) : null; + } + + /// + /// Get an by email + /// + /// Email to use for retrieval + /// + /// + /// + public IUser? GetByEmail(string email) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + IQuery query = Query().Where(x => x.Email.Equals(email)); + return _userRepository.Get(query)?.FirstOrDefault(); + } + } + + /// + /// Get an by username + /// + /// Username to use for retrieval + /// + /// + /// + public IUser? GetByUsername(string? username) + { + if (username is null) + { + return null; + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + try { - return _userRepository.Get(id); + return _userRepository.GetByUsername(username, true); } - } - - /// - /// Gets an by its provider key - /// - /// Id to use for retrieval - /// - public IUser? GetByProviderKey(object id) - { - var asInt = id.TryConvertTo(); - return asInt.Success ? GetById(asInt.Result) : null; - } - - /// - /// Get an by email - /// - /// Email to use for retrieval - /// - public IUser? GetByEmail(string email) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + catch (DbException) { - var query = Query().Where(x => x.Email.Equals(email)); - return _userRepository.Get(query)?.FirstOrDefault(); - } - } - - /// - /// Get an by username - /// - /// Username to use for retrieval - /// - public IUser? GetByUsername(string? username) - { - if (username is null) - { - return null; - } - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - try + // TODO: refactor users/upgrade + // currently kinda accepting anything on upgrade, but that won't deal with all cases + // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should + // be better BUT requires that the app restarts after the upgrade! + if (IsUpgrading) { - return _userRepository.GetByUsername(username, includeSecurityData: true); - } - catch (DbException) - { - // TODO: refactor users/upgrade - // currently kinda accepting anything on upgrade, but that won't deal with all cases - // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should - // be better BUT requires that the app restarts after the upgrade! - if (IsUpgrading) - { - //NOTE: this will not be cached - return _userRepository.GetByUsername(username, includeSecurityData: false); - } - - throw; - } - } - } - - /// - /// Disables an - /// - /// to disable - public void Delete(IUser membershipUser) - { - //disable - membershipUser.IsApproved = false; - - Save(membershipUser); - } - - /// - /// Deletes or disables a User - /// - /// to delete - /// True to permanently delete the user, False to disable the user - public void Delete(IUser user, bool deletePermanently) - { - if (deletePermanently == false) - { - Delete(user); - } - else - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var deletingNotification = new UserDeletingNotification(user, evtMsgs); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return; - } - - _userRepository.Delete(user); - - scope.Notifications.Publish(new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); - scope.Complete(); - } - } - } - - // explicit implementation because we don't need it now but due to the way that the members membership provider is put together - // this method must exist in this service as an implementation (legacy) - void IMembershipMemberService.SetLastLogin(string username, DateTime date) - { - _logger.LogWarning("This method is not implemented. Using membership providers users is not advised, use ASP.NET Identity instead. See issue #9224 for more information."); - } - - /// - /// Saves an - /// - /// to Save - public void Save(IUser entity) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var savingNotification = new UserSavingNotification(entity, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; + // NOTE: this will not be cached + return _userRepository.GetByUsername(username, false); } - if (string.IsNullOrWhiteSpace(entity.Username)) - throw new ArgumentException("Empty username.", nameof(entity)); - - if (string.IsNullOrWhiteSpace(entity.Name)) - throw new ArgumentException("Empty name.", nameof(entity)); - - try - { - _userRepository.Save(entity); - scope.Notifications.Publish(new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); - - scope.Complete(); - } - catch (DbException ex) - { - // if we are upgrading and an exception occurs, log and swallow it - if (IsUpgrading == false) throw; - - _logger.LogWarning(ex, "An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored"); - - // we don't want the uow to rollback its scope! - scope.Complete(); - } + throw; } } + } - /// - /// Saves a list of objects - /// - /// to save - public void Save(IEnumerable entities) + /// + /// Disables an + /// + /// to disable + public void Delete(IUser membershipUser) + { + // disable + membershipUser.IsApproved = false; + + Save(membershipUser); + } + + /// + /// Deletes or disables a User + /// + /// to delete + /// True to permanently delete the user, False to disable the user + public void Delete(IUser user, bool deletePermanently) + { + if (deletePermanently == false) { - var evtMsgs = EventMessagesFactory.Get(); - - var entitiesA = entities.ToArray(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - foreach (var user in entitiesA) - { - if (string.IsNullOrWhiteSpace(user.Username)) - throw new ArgumentException("Empty username.", nameof(entities)); - - if (string.IsNullOrWhiteSpace(user.Name)) - throw new ArgumentException("Empty name.", nameof(entities)); - - _userRepository.Save(user); - - } - - scope.Notifications.Publish(new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); - - //commit the whole lot in one go - scope.Complete(); - } + Delete(user); } - - /// - /// This is just the default user group that the membership provider will use - /// - /// - public string GetDefaultMemberType() + else { - return Cms.Core.Constants.Security.WriterGroupAlias; - } + EventMessages evtMsgs = EventMessagesFactory.Get(); - /// - /// Finds a list of objects by a partial email string - /// - /// Partial email string to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query?.Where(member => member.Email.Equals(emailStringToMatch)); - break; - case StringPropertyMatchType.Contains: - query?.Where(member => member.Email.Contains(emailStringToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query?.Where(member => member.Email.StartsWith(emailStringToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query?.Where(member => member.Email.EndsWith(emailStringToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query?.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } - - return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Email); - } - } - - /// - /// Finds a list of objects by a partial username - /// - /// Partial username to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query?.Where(member => member.Username.Equals(login)); - break; - case StringPropertyMatchType.Contains: - query?.Where(member => member.Username.Contains(login)); - break; - case StringPropertyMatchType.StartsWith: - query?.Where(member => member.Username.StartsWith(login)); - break; - case StringPropertyMatchType.EndsWith: - query?.Where(member => member.Username.EndsWith(login)); - break; - case StringPropertyMatchType.Wildcard: - query?.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } - - return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Username); - } - } - - /// - /// Gets the total number of Users based on the count type - /// - /// - /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members - /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science - /// but that is how MS have made theirs so we'll follow that principal. - /// - /// to count by - /// with number of Users for passed in type - public int GetCount(MemberCountType countType) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - IQuery? query; - - switch (countType) - { - case MemberCountType.All: - query = Query(); - break; - case MemberCountType.LockedOut: - query = Query()?.Where(x => x.IsLockedOut); - break; - case MemberCountType.Approved: - query = Query()?.Where(x => x.IsApproved); - break; - default: - throw new ArgumentOutOfRangeException(nameof(countType)); - } - - return _userRepository.GetCountByQuery(query); - } - } - - public Guid CreateLoginSession(int userId, string requestingIpAddress) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - var session = _userRepository.CreateLoginSession(userId, requestingIpAddress); - scope.Complete(); - return session; - } - } - - public int ClearLoginSessions(int userId) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - var count = _userRepository.ClearLoginSessions(userId); - scope.Complete(); - return count; - } - } - - public void ClearLoginSession(Guid sessionId) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - _userRepository.ClearLoginSession(sessionId); - scope.Complete(); - } - } - - public bool ValidateLoginSession(int userId, Guid sessionId) - { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - var result = _userRepository.ValidateLoginSession(userId, sessionId); - scope.Complete(); - return result; - } - } - - public IDictionary GetUserStates() - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetUserStates(); - } - } - - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[]? userState = null, string[]? userGroups = null, string? filter = null) - { - IQuery? filterQuery = null; - if (filter.IsNullOrWhiteSpace() == false) - { - filterQuery = Query()?.Where(x => (x.Name != null && x.Name.Contains(filter!)) || x.Username.Contains(filter!)); - } - - return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, userState, userGroups, null, filterQuery); - } - - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[]? userState = null, string[]? includeUserGroups = null, string[]? excludeUserGroups = null, IQuery? filter = null) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - Expression> sort; - switch (orderBy.ToUpperInvariant()) - { - case "USERNAME": - sort = member => member.Username; - break; - case "LANGUAGE": - sort = member => member.Language; - break; - case "NAME": - sort = member => member.Name; - break; - case "EMAIL": - sort = member => member.Email; - break; - case "ID": - sort = member => member.Id; - break; - case "CREATEDATE": - sort = member => member.CreateDate; - break; - case "UPDATEDATE": - sort = member => member.UpdateDate; - break; - case "ISAPPROVED": - sort = member => member.IsApproved; - break; - case "ISLOCKEDOUT": - sort = member => member.IsLockedOut; - break; - case "LASTLOGINDATE": - sort = member => member.LastLoginDate; - break; - default: - throw new IndexOutOfRangeException("The orderBy parameter " + orderBy + " is not valid"); - } - - return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, sort, orderDirection, includeUserGroups, excludeUserGroups, userState, filter); - } - } - - /// - /// Gets a list of paged objects - /// - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Name); - } - } - - public IEnumerable GetNextUsers(int id, int count) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetNextUsers(id, count); - } - } - - /// - /// Gets a list of objects associated with a given group - /// - /// Id of group - /// - public IEnumerable GetAllInGroup(int? groupId) - { - if (groupId is null) - { - return Array.Empty(); - } - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetAllInGroup(groupId.Value); - } - } - - /// - /// Gets a list of objects not associated with a given group - /// - /// Id of group - /// - public IEnumerable GetAllNotInGroup(int groupId) - { - using (var scope = ScopeProvider.CreateCoreScope()) - { - return _userRepository.GetAllNotInGroup(groupId); - } - } - - #endregion - - #region Implementation of IUserService - - /// - /// Gets an IProfile by User Id. - /// - /// Id of the User to retrieve - /// - public IProfile? GetProfileById(int id) - { - //This is called a TON. Go get the full user from cache which should already be IProfile - var fullUser = GetUserById(id); - if (fullUser == null) return null; - var asProfile = fullUser as IProfile; - return asProfile ?? new UserProfile(fullUser.Id, fullUser.Name); - } - - /// - /// Gets a profile by username - /// - /// Username - /// - public IProfile? GetProfileByUserName(string username) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetProfile(username); - } - } - - /// - /// Gets a user by Id - /// - /// Id of the user to retrieve - /// - public IUser? GetUserById(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - try - { - return _userRepository.Get(id); - } - catch (DbException) - { - // TODO: refactor users/upgrade - // currently kinda accepting anything on upgrade, but that won't deal with all cases - // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should - // be better BUT requires that the app restarts after the upgrade! - if (IsUpgrading) - { - //NOTE: this will not be cached - return _userRepository.Get(id, includeSecurityData: false); - } - - throw; - } - } - } - - public IEnumerable GetUsersById(params int[]? ids) - { - if (ids?.Length <= 0) return Enumerable.Empty(); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userRepository.GetMany(ids); - } - } - - /// - /// Replaces the same permission set for a single group to any number of entities - /// - /// If no 'entityIds' are specified all permissions will be removed for the specified group. - /// Id of the group - /// Permissions as enumerable list of If nothing is specified all permissions are removed. - /// Specify the nodes to replace permissions for. - public void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) - { - if (entityIds.Length == 0) - return; - - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); - scope.Complete(); - - var assigned = permissions?.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); - if (assigned is not null) - { - var entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); - scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); - } - } - } - - /// - /// Assigns the same permission set for a single user group to any number of entities - /// - /// Id of the user group - /// - /// Specify the nodes to replace permissions for - public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) - { - if (entityIds.Length == 0) - return; - - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); - scope.Complete(); - - var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; - var entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); - scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); - } - } - - /// - /// Gets all UserGroups or those specified as parameters - /// - /// Optional Ids of UserGroups to retrieve - /// An enumerable list of - public IEnumerable GetAllUserGroups(params int[] ids) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.GetMany(ids).OrderBy(x => x.Name); - } - } - - public IEnumerable GetUserGroupsByAlias(params string[] aliases) - { - if (aliases.Length == 0) return Enumerable.Empty(); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => aliases.SqlIn(x.Alias)); - var contents = _userGroupRepository.Get(query); - return contents?.WhereNotNull().ToArray() ?? Enumerable.Empty(); - } - } - - /// - /// Gets a UserGroup by its Alias - /// - /// Alias of the UserGroup to retrieve - /// - public IUserGroup? GetUserGroupByAlias(string alias) - { - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value cannot be null or whitespace.", "alias"); - - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == alias); - var contents = _userGroupRepository.Get(query); - return contents?.FirstOrDefault(); - } - } - - /// - /// Gets a UserGroup by its Id - /// - /// Id of the UserGroup to retrieve - /// - public IUserGroup? GetUserGroupById(int id) - { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.Get(id); - } - } - - /// - /// Saves a UserGroup - /// - /// UserGroup to save - /// - /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in - /// than all users will be removed from this group and only these users will be added - /// - /// Default is True otherwise set to False to not raise events - public void Save(IUserGroup userGroup, int[]? userIds = null) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - // we need to figure out which users have been added / removed, for audit purposes - var empty = new IUser[0]; - var addedUsers = empty; - var removedUsers = empty; - - if (userIds != null) - { - var groupUsers = userGroup.HasIdentity ? _userRepository.GetAllInGroup(userGroup.Id).ToArray() : empty; - var xGroupUsers = groupUsers.ToDictionary(x => x.Id, x => x); - var groupIds = groupUsers.Select(x => x.Id).ToArray(); - var addedUserIds = userIds.Except(groupIds); - - addedUsers = addedUserIds.Count() > 0 ? _userRepository.GetMany(addedUserIds.ToArray()).Where(x => x.Id != 0).ToArray() : new IUser[] { }; - removedUsers = groupIds.Except(userIds).Select(x => xGroupUsers[x]).Where(x => x.Id != 0).ToArray(); - } - - var userGroupWithUsers = new UserGroupWithUsers(userGroup, addedUsers, removedUsers); - - // this is the default/expected notification for the IUserGroup entity being saved - var savingNotification = new UserGroupSavingNotification(userGroup, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - scope.Complete(); - return; - } - - // this is an additional notification for special auditing - var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); - if (scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) - { - scope.Complete(); - return; - } - - _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); - - scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); - scope.Notifications.Publish(new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); - - scope.Complete(); - } - } - - /// - /// Deletes a UserGroup - /// - /// UserGroup to delete - public void DeleteUserGroup(IUserGroup userGroup) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateCoreScope()) - { - var deletingNotification = new UserGroupDeletingNotification(userGroup, evtMsgs); + var deletingNotification = new UserDeletingNotification(user, evtMsgs); if (scope.Notifications.PublishCancelable(deletingNotification)) { scope.Complete(); return; } - _userGroupRepository.Delete(userGroup); - - scope.Notifications.Publish(new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); + _userRepository.Delete(user); + scope.Notifications.Publish( + new UserDeletedNotification(user, evtMsgs).WithStateFrom(deletingNotification)); scope.Complete(); } } + } - /// - /// Removes a specific section from all users - /// - /// This is useful when an entire section is removed from config - /// Alias of the section to remove - public void DeleteSectionFromAllUserGroups(string sectionAlias) + // explicit implementation because we don't need it now but due to the way that the members membership provider is put together + // this method must exist in this service as an implementation (legacy) + void IMembershipMemberService.SetLastLogin(string username, DateTime date) => _logger.LogWarning( + "This method is not implemented. Using membership providers users is not advised, use ASP.NET Identity instead. See issue #9224 for more information."); + + /// + /// Saves an + /// + /// to Save + public void Save(IUser entity) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope()) + var savingNotification = new UserSavingNotification(entity, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) { - var assignedGroups = _userGroupRepository.GetGroupsAssignedToSection(sectionAlias); - foreach (var group in assignedGroups) + scope.Complete(); + return; + } + + if (string.IsNullOrWhiteSpace(entity.Username)) + { + throw new ArgumentException("Empty username.", nameof(entity)); + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new ArgumentException("Empty name.", nameof(entity)); + } + + try + { + _userRepository.Save(entity); + scope.Notifications.Publish( + new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); + + scope.Complete(); + } + catch (DbException ex) + { + // if we are upgrading and an exception occurs, log and swallow it + if (IsUpgrading == false) { - //now remove the section for each user and commit - //now remove the section for each user and commit - group.RemoveAllowedSection(sectionAlias); - _userGroupRepository.Save(group); + throw; } + _logger.LogWarning( + ex, + "An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored"); + + // we don't want the uow to rollback its scope! scope.Complete(); } } + } - /// - /// Get explicitly assigned permissions for a user and optional node ids - /// - /// User to retrieve permissions for - /// Specifying nothing will return all permissions for all nodes - /// An enumerable list of - public EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds) + /// + /// Saves a list of objects + /// + /// to save + public void Save(IEnumerable entities) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + IUser[] entitiesA = entities.ToArray(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) { - return _userGroupRepository.GetPermissions(user?.Groups.ToArray(), true, nodeIds); + scope.Complete(); + return; + } + + foreach (IUser user in entitiesA) + { + if (string.IsNullOrWhiteSpace(user.Username)) + { + throw new ArgumentException("Empty username.", nameof(entities)); + } + + if (string.IsNullOrWhiteSpace(user.Name)) + { + throw new ArgumentException("Empty name.", nameof(entities)); + } + + _userRepository.Save(user); + } + + scope.Notifications.Publish( + new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); + + // commit the whole lot in one go + scope.Complete(); + } + } + + /// + /// This is just the default user group that the membership provider will use + /// + /// + public string GetDefaultMemberType() => Constants.Security.WriterGroupAlias; + + /// + /// Finds a list of objects by a partial email string + /// + /// Partial email string to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// + /// The type of match to make as . Default is + /// + /// + /// + /// + /// + public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query?.Where(member => member.Email.Equals(emailStringToMatch)); + break; + case StringPropertyMatchType.Contains: + query?.Where(member => member.Email.Contains(emailStringToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query?.Where(member => member.Email.StartsWith(emailStringToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query?.Where(member => member.Email.EndsWith(emailStringToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query?.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } + + return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Email); + } + } + + /// + /// Finds a list of objects by a partial username + /// + /// Partial username to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// + /// The type of match to make as . Default is + /// + /// + /// + /// + /// + public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query?.Where(member => member.Username.Equals(login)); + break; + case StringPropertyMatchType.Contains: + query?.Where(member => member.Username.Contains(login)); + break; + case StringPropertyMatchType.StartsWith: + query?.Where(member => member.Username.StartsWith(login)); + break; + case StringPropertyMatchType.EndsWith: + query?.Where(member => member.Username.EndsWith(login)); + break; + case StringPropertyMatchType.Wildcard: + query?.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } + + return _userRepository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Username); + } + } + + /// + /// Gets the total number of Users based on the count type + /// + /// + /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any + /// members + /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact + /// science + /// but that is how MS have made theirs so we'll follow that principal. + /// + /// to count by + /// with number of Users for passed in type + public int GetCount(MemberCountType countType) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery? query; + + switch (countType) + { + case MemberCountType.All: + query = Query(); + break; + case MemberCountType.LockedOut: + query = Query()?.Where(x => x.IsLockedOut); + break; + case MemberCountType.Approved: + query = Query()?.Where(x => x.IsApproved); + break; + default: + throw new ArgumentOutOfRangeException(nameof(countType)); + } + + return _userRepository.GetCountByQuery(query); + } + } + + public Guid CreateLoginSession(int userId, string requestingIpAddress) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + Guid session = _userRepository.CreateLoginSession(userId, requestingIpAddress); + scope.Complete(); + return session; + } + } + + public int ClearLoginSessions(int userId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var count = _userRepository.ClearLoginSessions(userId); + scope.Complete(); + return count; + } + } + + public void ClearLoginSession(Guid sessionId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _userRepository.ClearLoginSession(sessionId); + scope.Complete(); + } + } + + public bool ValidateLoginSession(int userId, Guid sessionId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var result = _userRepository.ValidateLoginSession(userId, sessionId); + scope.Complete(); + return result; + } + } + + public IDictionary GetUserStates() + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetUserStates(); + } + } + + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, UserState[]? userState = null, string[]? userGroups = null, string? filter = null) + { + IQuery? filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query()?.Where(x => + (x.Name != null && x.Name.Contains(filter!)) || x.Username.Contains(filter!)); + } + + return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, userState, userGroups, null, filterQuery); + } + + public IEnumerable GetAll( + long pageIndex, + int pageSize, + out long totalRecords, + string orderBy, + Direction orderDirection, + UserState[]? userState = null, + string[]? includeUserGroups = null, + string[]? excludeUserGroups = null, + IQuery? filter = null) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + Expression> sort; + switch (orderBy.ToUpperInvariant()) + { + case "USERNAME": + sort = member => member.Username; + break; + case "LANGUAGE": + sort = member => member.Language; + break; + case "NAME": + sort = member => member.Name; + break; + case "EMAIL": + sort = member => member.Email; + break; + case "ID": + sort = member => member.Id; + break; + case "CREATEDATE": + sort = member => member.CreateDate; + break; + case "UPDATEDATE": + sort = member => member.UpdateDate; + break; + case "ISAPPROVED": + sort = member => member.IsApproved; + break; + case "ISLOCKEDOUT": + sort = member => member.IsLockedOut; + break; + case "LASTLOGINDATE": + sort = member => member.LastLoginDate; + break; + default: + throw new IndexOutOfRangeException("The orderBy parameter " + orderBy + " is not valid"); + } + + return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, sort, orderDirection, includeUserGroups, excludeUserGroups, userState, filter); + } + } + + /// + /// Gets a list of paged objects + /// + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// + /// + /// + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Name); + } + } + + public IEnumerable GetNextUsers(int id, int count) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetNextUsers(id, count); + } + } + + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + /// + /// + /// + public IEnumerable GetAllInGroup(int? groupId) + { + if (groupId is null) + { + return Array.Empty(); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetAllInGroup(groupId.Value); + } + } + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + /// + /// + /// + public IEnumerable GetAllNotInGroup(int groupId) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + return _userRepository.GetAllNotInGroup(groupId); + } + } + + #endregion + + #region Implementation of IUserService + + /// + /// Gets an IProfile by User Id. + /// + /// Id of the User to retrieve + /// + /// + /// + public IProfile? GetProfileById(int id) + { + // This is called a TON. Go get the full user from cache which should already be IProfile + IUser? fullUser = GetUserById(id); + if (fullUser == null) + { + return null; + } + + var asProfile = fullUser as IProfile; + return asProfile ?? new UserProfile(fullUser.Id, fullUser.Name); + } + + /// + /// Gets a profile by username + /// + /// Username + /// + /// + /// + public IProfile? GetProfileByUserName(string username) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetProfile(username); + } + } + + /// + /// Gets a user by Id + /// + /// Id of the user to retrieve + /// + /// + /// + public IUser? GetUserById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + try + { + return _userRepository.Get(id); + } + catch (DbException) + { + // TODO: refactor users/upgrade + // currently kinda accepting anything on upgrade, but that won't deal with all cases + // so we need to do it differently, see the custom UmbracoPocoDataBuilder which should + // be better BUT requires that the app restarts after the upgrade! + if (IsUpgrading) + { + // NOTE: this will not be cached + return _userRepository.Get(id, false); + } + + throw; + } + } + } + + public IEnumerable GetUsersById(params int[]? ids) + { + if (ids?.Length <= 0) + { + return Enumerable.Empty(); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userRepository.GetMany(ids); + } + } + + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// If no 'entityIds' are specified all permissions will be removed for the specified group. + /// Id of the group + /// + /// Permissions as enumerable list of If nothing is specified all permissions + /// are removed. + /// + /// Specify the nodes to replace permissions for. + public void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + { + if (entityIds.Length == 0) + { + return; + } + + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); + scope.Complete(); + + var assigned = permissions?.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); + if (assigned is not null) + { + EntityPermission[] entityPermissions = + entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); + scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); + } + } + } + + /// + /// Assigns the same permission set for a single user group to any number of entities + /// + /// Id of the user group + /// + /// Specify the nodes to replace permissions for + public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) + { + if (entityIds.Length == 0) + { + return; + } + + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); + scope.Complete(); + + var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; + EntityPermission[] entityPermissions = + entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); + scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); + } + } + + /// + /// Gets all UserGroups or those specified as parameters + /// + /// Optional Ids of UserGroups to retrieve + /// An enumerable list of + public IEnumerable GetAllUserGroups(params int[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetMany(ids).OrderBy(x => x.Name); + } + } + + public IEnumerable GetUserGroupsByAlias(params string[] aliases) + { + if (aliases.Length == 0) + { + return Enumerable.Empty(); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => aliases.SqlIn(x.Alias)); + IEnumerable contents = _userGroupRepository.Get(query); + return contents?.WhereNotNull().ToArray() ?? Enumerable.Empty(); + } + } + + /// + /// Gets a UserGroup by its Alias + /// + /// Alias of the UserGroup to retrieve + /// + /// + /// + public IUserGroup? GetUserGroupByAlias(string alias) + { + if (string.IsNullOrWhiteSpace(alias)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "alias"); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.Alias == alias); + IEnumerable contents = _userGroupRepository.Get(query); + return contents?.FirstOrDefault(); + } + } + + /// + /// Gets a UserGroup by its Id + /// + /// Id of the UserGroup to retrieve + /// + /// + /// + public IUserGroup? GetUserGroupById(int id) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.Get(id); + } + } + + /// + /// Saves a UserGroup + /// + /// UserGroup to save + /// + /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in + /// than all users will be removed from this group and only these users will be added + /// + /// Default is + /// True + /// otherwise set to + /// False + /// to not raise events + /// + public void Save(IUserGroup userGroup, int[]? userIds = null) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + // we need to figure out which users have been added / removed, for audit purposes + var empty = new IUser[0]; + IUser[] addedUsers = empty; + IUser[] removedUsers = empty; + + if (userIds != null) + { + IUser[] groupUsers = + userGroup.HasIdentity ? _userRepository.GetAllInGroup(userGroup.Id).ToArray() : empty; + var xGroupUsers = groupUsers.ToDictionary(x => x.Id, x => x); + var groupIds = groupUsers.Select(x => x.Id).ToArray(); + IEnumerable addedUserIds = userIds.Except(groupIds); + + addedUsers = addedUserIds.Count() > 0 + ? _userRepository.GetMany(addedUserIds.ToArray()).Where(x => x.Id != 0).ToArray() + : new IUser[] { }; + removedUsers = groupIds.Except(userIds).Select(x => xGroupUsers[x]).Where(x => x.Id != 0).ToArray(); + } + + var userGroupWithUsers = new UserGroupWithUsers(userGroup, addedUsers, removedUsers); + + // this is the default/expected notification for the IUserGroup entity being saved + var savingNotification = new UserGroupSavingNotification(userGroup, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + scope.Complete(); + return; + } + + // this is an additional notification for special auditing + var savingUserGroupWithUsersNotification = + new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); + if (scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) + { + scope.Complete(); + return; + } + + _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); + + scope.Notifications.Publish( + new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom( + savingUserGroupWithUsersNotification)); + + scope.Complete(); + } + } + + /// + /// Deletes a UserGroup + /// + /// UserGroup to delete + public void DeleteUserGroup(IUserGroup userGroup) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + var deletingNotification = new UserGroupDeletingNotification(userGroup, evtMsgs); + if (scope.Notifications.PublishCancelable(deletingNotification)) + { + scope.Complete(); + return; + } + + _userGroupRepository.Delete(userGroup); + + scope.Notifications.Publish( + new UserGroupDeletedNotification(userGroup, evtMsgs).WithStateFrom(deletingNotification)); + + scope.Complete(); + } + } + + /// + /// Removes a specific section from all users + /// + /// This is useful when an entire section is removed from config + /// Alias of the section to remove + public void DeleteSectionFromAllUserGroups(string sectionAlias) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + IEnumerable assignedGroups = _userGroupRepository.GetGroupsAssignedToSection(sectionAlias); + foreach (IUserGroup group in assignedGroups) + { + // now remove the section for each user and commit + // now remove the section for each user and commit + group.RemoveAllowedSection(sectionAlias); + _userGroupRepository.Save(group); + } + + scope.Complete(); + } + } + + /// + /// Get explicitly assigned permissions for a user and optional node ids + /// + /// User to retrieve permissions for + /// Specifying nothing will return all permissions for all nodes + /// An enumerable list of + public EntityPermissionCollection GetPermissions(IUser? user, params int[] nodeIds) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetPermissions(user?.Groups.ToArray(), true, nodeIds); + } + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// Specifying nothing will return all permissions for all nodes + /// An enumerable list of + public EntityPermissionCollection GetPermissions(IUserGroup?[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetPermissions( + groups.WhereNotNull().Select(x => x.ToReadOnlyGroup()).ToArray(), + fallbackToDefaultPermissions, + nodeIds); + } + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// Groups to retrieve permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// Specifying nothing will return all permissions for all nodes + /// An enumerable list of + private IEnumerable GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _userGroupRepository.GetPermissions(groups, fallbackToDefaultPermissions, nodeIds); + } + } + + /// + /// Gets the implicit/inherited permissions for the user for the given path + /// + /// User to check permissions for + /// Path to check permissions for + public EntityPermissionSet GetPermissionsForPath(IUser? user, string? path) + { + var nodeIds = path?.GetIdsFromPathReversed(); + + if (nodeIds is null || nodeIds.Length == 0 || user is null) + { + return EntityPermissionSet.Empty(); + } + + // collect all permissions structures for all nodes for all groups belonging to the user + EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } + + /// + /// Gets the permissions for the provided group and path + /// + /// + /// Path to check permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// String indicating permissions for provided user and path + public EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false) + { + var nodeIds = path.GetIdsFromPathReversed(); + + if (nodeIds.Length == 0) + { + return EntityPermissionSet.Empty(); + } + + // collect all permissions structures for all nodes for all groups + EntityPermission[] groupPermissions = + GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, true).ToArray(); + + return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } + + /// + /// This performs the calculations for inherited nodes based on this + /// http://issues.umbraco.org/issue/U4-10075#comment=67-40085 + /// + /// + /// + /// + internal static EntityPermissionSet CalculatePermissionsForPathForUser( + EntityPermission[] groupPermissions, + int[] pathIds) + { + // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? + if (groupPermissions.Length == 0 || pathIds.Length == 0) + { + return EntityPermissionSet.Empty(); + } + + // The actual entity id being looked at (deepest part of the path) + var entityId = pathIds[0]; + + var resultPermissions = new EntityPermissionCollection(); + + // create a grouped by dictionary of another grouped by dictionary + var permissionsByGroup = groupPermissions + .GroupBy(x => x.UserGroupId) + .ToDictionary( + x => x.Key, + x => x.GroupBy(a => a.EntityId).ToDictionary(a => a.Key, a => a.ToArray())); + + // iterate through each group + foreach (KeyValuePair> byGroup in permissionsByGroup) + { + var added = false; + + // iterate deepest to shallowest + foreach (var pathId in pathIds) + { + if (byGroup.Value.TryGetValue(pathId, out EntityPermission[]? permissionsForNodeAndGroup) == false) + { + continue; + } + + // In theory there will only be one EntityPermission in this group + // but there's nothing stopping the logic of this method + // from having more so we deal with it here + foreach (EntityPermission entityPermission in permissionsForNodeAndGroup) + { + if (entityPermission.IsDefaultPermissions == false) + { + // explicit permission found so we'll append it and move on, the collection is a hashset anyways + // so only supports adding one element per groupid/contentid + resultPermissions.Add(entityPermission); + added = true; + break; + } + } + + // if the permission has been added for this group and this branch then we can exit this loop + if (added) + { + break; + } + } + + if (added == false && byGroup.Value.Count > 0) + { + // if there was no explicit permissions assigned in this branch for this group, then we will + // add the group's default permissions + resultPermissions.Add(byGroup.Value[entityId][0]); } } - /// - /// Get explicitly assigned permissions for a group and optional node Ids - /// - /// Groups to retrieve permissions for - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// Specifying nothing will return all permissions for all nodes - /// An enumerable list of - private IEnumerable GetPermissions(IReadOnlyUserGroup[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (groups == null) throw new ArgumentNullException(nameof(groups)); + var permissionSet = new EntityPermissionSet(entityId, resultPermissions); + return permissionSet; + } - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.GetPermissions(groups, fallbackToDefaultPermissions, nodeIds); - } + private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) + { + if (pathIds.Length == 0) + { + return new EntityPermissionCollection(Enumerable.Empty()); } - /// - /// Get explicitly assigned permissions for a group and optional node Ids - /// - /// - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// Specifying nothing will return all permissions for all nodes - /// An enumerable list of - public EntityPermissionCollection GetPermissions(IUserGroup?[] groups, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (groups == null) throw new ArgumentNullException(nameof(groups)); - using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) - { - return _userGroupRepository.GetPermissions(groups.WhereNotNull().Select(x => x.ToReadOnlyGroup()).ToArray(), fallbackToDefaultPermissions, nodeIds); - } - } - /// - /// Gets the implicit/inherited permissions for the user for the given path - /// - /// User to check permissions for - /// Path to check permissions for - public EntityPermissionSet GetPermissionsForPath(IUser? user, string? path) - { - var nodeIds = path?.GetIdsFromPathReversed(); - - if (nodeIds is null || nodeIds.Length == 0 || user is null) - return EntityPermissionSet.Empty(); - - //collect all permissions structures for all nodes for all groups belonging to the user - var groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); - - return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); - } - - /// - /// Gets the permissions for the provided group and path - /// - /// - /// Path to check permissions for - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// String indicating permissions for provided user and path - public EntityPermissionSet GetPermissionsForPath(IUserGroup[] groups, string path, bool fallbackToDefaultPermissions = false) - { - var nodeIds = path.GetIdsFromPathReversed(); - - if (nodeIds.Length == 0) - return EntityPermissionSet.Empty(); - - //collect all permissions structures for all nodes for all groups - var groupPermissions = GetPermissionsForPath(groups.Select(x => x.ToReadOnlyGroup()).ToArray(), nodeIds, fallbackToDefaultPermissions: true).ToArray(); - - return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); - } - - private EntityPermissionCollection GetPermissionsForPath(IReadOnlyUserGroup[] groups, int[] pathIds, bool fallbackToDefaultPermissions = false) - { - if (pathIds.Length == 0) - return new EntityPermissionCollection(Enumerable.Empty()); - - //get permissions for all nodes in the path by group - var permissions = GetPermissions(groups, fallbackToDefaultPermissions, pathIds) + // get permissions for all nodes in the path by group + IEnumerable> permissions = + GetPermissions(groups, fallbackToDefaultPermissions, pathIds) .GroupBy(x => x.UserGroupId); - return new EntityPermissionCollection( - permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions)).Where(x => x is not null)!); - } - - /// - /// This performs the calculations for inherited nodes based on this http://issues.umbraco.org/issue/U4-10075#comment=67-40085 - /// - /// - /// - /// - internal static EntityPermissionSet CalculatePermissionsForPathForUser( - EntityPermission[] groupPermissions, - int[] pathIds) - { - // not sure this will ever happen, it shouldn't since this should return defaults, but maybe those are empty? - if (groupPermissions.Length == 0 || pathIds.Length == 0) - return EntityPermissionSet.Empty(); - - //The actual entity id being looked at (deepest part of the path) - var entityId = pathIds[0]; - - var resultPermissions = new EntityPermissionCollection(); - - //create a grouped by dictionary of another grouped by dictionary - var permissionsByGroup = groupPermissions - .GroupBy(x => x.UserGroupId) - .ToDictionary( - x => x.Key, - x => x.GroupBy(a => a.EntityId).ToDictionary(a => a.Key, a => a.ToArray())); - - //iterate through each group - foreach (var byGroup in permissionsByGroup) - { - var added = false; - - //iterate deepest to shallowest - foreach (var pathId in pathIds) - { - EntityPermission[]? permissionsForNodeAndGroup; - if (byGroup.Value.TryGetValue(pathId, out permissionsForNodeAndGroup) == false) - continue; - - //In theory there will only be one EntityPermission in this group - // but there's nothing stopping the logic of this method - // from having more so we deal with it here - foreach (var entityPermission in permissionsForNodeAndGroup) - { - if (entityPermission.IsDefaultPermissions == false) - { - //explicit permission found so we'll append it and move on, the collection is a hashset anyways - //so only supports adding one element per groupid/contentid - resultPermissions.Add(entityPermission); - added = true; - break; - } - } - - //if the permission has been added for this group and this branch then we can exit this loop - if (added) - break; - } - - if (added == false && byGroup.Value.Count > 0) - { - //if there was no explicit permissions assigned in this branch for this group, then we will - //add the group's default permissions - resultPermissions.Add(byGroup.Value[entityId][0]); - } - - } - - var permissionSet = new EntityPermissionSet(entityId, resultPermissions); - return permissionSet; - } - - /// - /// Returns the resulting permission set for a group for the path based on all permissions provided for the branch - /// - /// - /// The collective set of permissions provided to calculate the resulting permissions set for the path - /// based on a single group - /// - /// Must be ordered deepest to shallowest (right to left) - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// - internal static EntityPermission? GetPermissionsForPathForGroup( - IEnumerable pathPermissions, - int[] pathIds, - bool fallbackToDefaultPermissions = false) - { - //get permissions for all nodes in the path - var permissionsByEntityId = pathPermissions.ToDictionary(x => x.EntityId, x => x); - - //then the permissions assigned to the path will be the 'deepest' node found that has permissions - foreach (var id in pathIds) - { - EntityPermission? permission; - if (permissionsByEntityId.TryGetValue(id, out permission)) - { - //don't return the default permissions if that is the one assigned here (we'll do that below if nothing was found) - if (permission.IsDefaultPermissions == false) - return permission; - } - } - - //if we've made it here it means that no implicit/inherited permissions were found so we return the defaults if that is specified - if (fallbackToDefaultPermissions == false) - return null; - - return permissionsByEntityId[pathIds[0]]; - } - - /// - /// Checks in a set of permissions associated with a user for those related to a given nodeId - /// - /// The set of permissions - /// The node Id - /// The permissions to return - /// True if permissions for the given path are found - public static bool TryGetAssignedPermissionsForNode(IList permissions, - int nodeId, - out string assignedPermissions) - { - if (permissions.Any(x => x.EntityId == nodeId)) - { - var found = permissions.First(x => x.EntityId == nodeId); - var assignedPermissionsArray = found.AssignedPermissions.ToList(); - - // Working with permissions assigned directly to a user AND to their groups, so maybe several per node - // and we need to get the most permissive set - foreach (var permission in permissions.Where(x => x.EntityId == nodeId).Skip(1)) - { - AddAdditionalPermissions(assignedPermissionsArray, permission.AssignedPermissions); - } - - assignedPermissions = string.Join("", assignedPermissionsArray); - return true; - } - - assignedPermissions = string.Empty; - return false; - } - - private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) - { - var permissionsToAdd = additionalPermissions - .Where(x => assignedPermissions.Contains(x) == false); - assignedPermissions.AddRange(permissionsToAdd); - } - - #endregion + return new EntityPermissionCollection( + permissions.Select(x => GetPermissionsForPathForGroup(x, pathIds, fallbackToDefaultPermissions)) + .Where(x => x is not null)!); } + + /// + /// Returns the resulting permission set for a group for the path based on all permissions provided for the branch + /// + /// + /// The collective set of permissions provided to calculate the resulting permissions set for the path + /// based on a single group + /// + /// Must be ordered deepest to shallowest (right to left) + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// + internal static EntityPermission? GetPermissionsForPathForGroup( + IEnumerable pathPermissions, + int[] pathIds, + bool fallbackToDefaultPermissions = false) + { + // get permissions for all nodes in the path + var permissionsByEntityId = pathPermissions.ToDictionary(x => x.EntityId, x => x); + + // then the permissions assigned to the path will be the 'deepest' node found that has permissions + foreach (var id in pathIds) + { + if (permissionsByEntityId.TryGetValue(id, out EntityPermission? permission)) + { + // don't return the default permissions if that is the one assigned here (we'll do that below if nothing was found) + if (permission.IsDefaultPermissions == false) + { + return permission; + } + } + } + + // if we've made it here it means that no implicit/inherited permissions were found so we return the defaults if that is specified + if (fallbackToDefaultPermissions == false) + { + return null; + } + + return permissionsByEntityId[pathIds[0]]; + } + + private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) + { + IEnumerable permissionsToAdd = additionalPermissions + .Where(x => assignedPermissions.Contains(x) == false); + assignedPermissions.AddRange(permissionsToAdd); + } + + #endregion } diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 86e823f8bc..f17a266616 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,94 +1,89 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UserServiceExtensions { - public static class UserServiceExtensions + public static EntityPermission? GetPermissions(this IUserService userService, IUser? user, string path) { - public static EntityPermission? GetPermissions(this IUserService userService, IUser? user, string path) + var ids = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) + ? Attempt.Succeed(value) + : Attempt.Fail()) + .Where(x => x.Success) + .Select(x => x.Result) + .ToArray(); + if (ids.Length == 0) { - var ids = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? Attempt.Succeed(value) : Attempt.Fail()) - .Where(x => x.Success) - .Select(x=>x.Result) - .ToArray(); - if (ids.Length == 0) throw new InvalidOperationException("The path: " + path + " could not be parsed into an array of integers or the path was empty"); - - return userService.GetPermissions(user, ids[ids.Length - 1]).FirstOrDefault(); + throw new InvalidOperationException("The path: " + path + + " could not be parsed into an array of integers or the path was empty"); } - /// - /// Get explicitly assigned permissions for a group and optional node Ids - /// - /// - /// - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - /// Specifying nothing will return all permissions for all nodes - /// An enumerable list of - public static EntityPermissionCollection GetPermissions(this IUserService service, IUserGroup? group, bool fallbackToDefaultPermissions, params int[] nodeIds) + return userService.GetPermissions(user, ids[^1]).FirstOrDefault(); + } + + /// + /// Get explicitly assigned permissions for a group and optional node Ids + /// + /// + /// + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + /// Specifying nothing will return all permissions for all nodes + /// An enumerable list of + public static EntityPermissionCollection GetPermissions(this IUserService service, IUserGroup? group, bool fallbackToDefaultPermissions, params int[] nodeIds) => + service.GetPermissions(new[] { group }, fallbackToDefaultPermissions, nodeIds); + + /// + /// Gets the permissions for the provided group and path + /// + /// + /// + /// Path to check permissions for + /// + /// Flag indicating if we want to include the default group permissions for each result if there are not explicit + /// permissions set + /// + public static EntityPermissionSet GetPermissionsForPath(this IUserService service, IUserGroup group, string path, bool fallbackToDefaultPermissions = false) => + service.GetPermissionsForPath(new[] { group }, path, fallbackToDefaultPermissions); + + /// + /// Remove all permissions for this user group for all nodes specified + /// + /// + /// + /// + public static void RemoveUserGroupPermissions(this IUserService userService, int groupId, params int[] entityIds) => + userService.ReplaceUserGroupPermissions(groupId, null, entityIds); + + /// + /// Remove all permissions for this user group for all nodes + /// + /// + /// + public static void RemoveUserGroupPermissions(this IUserService userService, int groupId) => + userService.ReplaceUserGroupPermissions(groupId, null); + + public static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) + { + IEnumerable fullUsers = userService.GetUsersById(ids); + + return fullUsers.Select(user => { - return service.GetPermissions(new[] {group}, fallbackToDefaultPermissions, nodeIds); - } + var asProfile = user as IProfile; + return asProfile ?? new UserProfile(user.Id, user.Name); + }); + } - /// - /// Gets the permissions for the provided group and path - /// - /// - /// - /// Path to check permissions for - /// - /// Flag indicating if we want to include the default group permissions for each result if there are not explicit permissions set - /// - public static EntityPermissionSet GetPermissionsForPath(this IUserService service, IUserGroup group, string path, bool fallbackToDefaultPermissions = false) - { - return service.GetPermissionsForPath(new[] { group }, path, fallbackToDefaultPermissions); - } - - /// - /// Remove all permissions for this user group for all nodes specified - /// - /// - /// - /// - public static void RemoveUserGroupPermissions(this IUserService userService, int groupId, params int[] entityIds) - { - userService.ReplaceUserGroupPermissions(groupId, null, entityIds); - } - - /// - /// Remove all permissions for this user group for all nodes - /// - /// - /// - public static void RemoveUserGroupPermissions(this IUserService userService, int groupId) - { - userService.ReplaceUserGroupPermissions(groupId, null); - } - - - public static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) - { - var fullUsers = userService.GetUsersById(ids); - - return fullUsers.Select(user => - { - var asProfile = user as IProfile; - return asProfile ?? new UserProfile(user.Id, user.Name); - }); - - } - - public static IUser? GetByKey(this IUserService userService, Guid key) - { - int id = BitConverter.ToInt32(key.ToByteArray(), 0); - return userService.GetUserById(id); - } + public static IUser? GetByKey(this IUserService userService, Guid key) + { + var id = BitConverter.ToInt32(key.ToByteArray(), 0); + return userService.GetUserById(id); } } diff --git a/src/Umbraco.Core/Settable.cs b/src/Umbraco.Core/Settable.cs index 07f53c2080..9f91ee15ff 100644 --- a/src/Umbraco.Core/Settable.cs +++ b/src/Umbraco.Core/Settable.cs @@ -1,95 +1,94 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Represents a value that can be assigned a value. +/// +/// The type of the value +public class Settable { + private T? _value; + /// - /// Represents a value that can be assigned a value. + /// Gets a value indicating whether a value has been assigned to this instance. /// - /// The type of the value - public class Settable + public bool HasValue { get; private set; } + + /// + /// Gets the value assigned to this instance. + /// + /// An exception is thrown if the HasValue property is false. + /// No value has been assigned to this instance. + public T? Value { - private T? _value; - - /// - /// Assigns a value to this instance. - /// - /// The value. - public void Set(T? value) + get { - if (value is not null) + if (HasValue == false) { - HasValue = true; + throw new InvalidOperationException("The HasValue property is false."); } - _value = value; - } - /// - /// Assigns a value to this instance by copying the value - /// of another instance, if the other instance has a value. - /// - /// The other instance. - public void Set(Settable other) - { - // set only if has value else don't change anything - if (other.HasValue) Set(other.Value); - } - - /// - /// Clears the value. - /// - public void Clear() - { - HasValue = false; - _value = default (T); - } - - /// - /// Gets a value indicating whether a value has been assigned to this instance. - /// - public bool HasValue { get; private set; } - - /// - /// Gets the value assigned to this instance. - /// - /// An exception is thrown if the HasValue property is false. - /// No value has been assigned to this instance. - public T? Value - { - get - { - if (HasValue == false) - throw new InvalidOperationException("The HasValue property is false."); - return _value; - } - } - - /// - /// Gets the value assigned to this instance, if a value has been assigned, - /// otherwise the default value of . - /// - /// The value assigned to this instance, if a value has been assigned, - /// else the default value of . - public T? ValueOrDefault() - { - return HasValue ? _value : default(T); - } - - /// - /// Gets the value assigned to this instance, if a value has been assigned, - /// otherwise a specified default value. - /// - /// The default value. - /// The value assigned to this instance, if a value has been assigned, - /// else . - public T? ValueOrDefault(T defaultValue) - { - return HasValue ? _value : defaultValue; - } - - /// - public override string? ToString() - { - return HasValue ? _value?.ToString() : "void"; + return _value; } } + + /// + /// Assigns a value to this instance. + /// + /// The value. + public void Set(T? value) + { + if (value is not null) + { + HasValue = true; + } + + _value = value; + } + + /// + /// Assigns a value to this instance by copying the value + /// of another instance, if the other instance has a value. + /// + /// The other instance. + public void Set(Settable other) + { + // set only if has value else don't change anything + if (other.HasValue) + { + Set(other.Value); + } + } + + /// + /// Clears the value. + /// + public void Clear() + { + HasValue = false; + _value = default; + } + + /// + /// Gets the value assigned to this instance, if a value has been assigned, + /// otherwise the default value of . + /// + /// + /// The value assigned to this instance, if a value has been assigned, + /// else the default value of . + /// + public T? ValueOrDefault() => HasValue ? _value : default; + + /// + /// Gets the value assigned to this instance, if a value has been assigned, + /// otherwise a specified default value. + /// + /// The default value. + /// + /// The value assigned to this instance, if a value has been assigned, + /// else . + /// + public T? ValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; + + /// + public override string? ToString() => HasValue ? _value?.ToString() : "void"; } diff --git a/src/Umbraco.Core/SimpleMainDom.cs b/src/Umbraco.Core/SimpleMainDom.cs index 3f4bd1ce7c..3b3bc1b0c0 100644 --- a/src/Umbraco.Core/SimpleMainDom.cs +++ b/src/Umbraco.Core/SimpleMainDom.cs @@ -1,80 +1,92 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Provides a simple implementation of . +/// +public class SimpleMainDom : IMainDom, IDisposable { - /// - /// Provides a simple implementation of . - /// - public class SimpleMainDom : IMainDom, IDisposable + private readonly List> _callbacks = new(); + private readonly object _locko = new(); + private bool _disposedValue; + private bool _isStopping; + + /// + public bool IsMainDom { get; private set; } = true; + + public void Dispose() { - private readonly object _locko = new object(); - private readonly List> _callbacks = new List>(); - private bool _isStopping; - private bool _disposedValue; + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(true); + GC.SuppressFinalize(this); + } - /// - public bool IsMainDom { get; private set; } = true; + // always acquire + public bool Acquire(IApplicationShutdownRegistry hostingEnvironment) => true; - // always acquire - public bool Acquire(IApplicationShutdownRegistry hostingEnvironment) => true; - - /// - public bool Register(Action? install, Action? release, int weight = 100) + /// + public bool Register(Action? install, Action? release, int weight = 100) + { + lock (_locko) { - lock (_locko) + if (_isStopping) { - if (_isStopping) return false; - install?.Invoke(); - if (release != null) - _callbacks.Add(new KeyValuePair(weight, release)); - return true; + return false; } + + install?.Invoke(); + if (release != null) + { + _callbacks.Add(new KeyValuePair(weight, release)); + } + + return true; + } + } + + public void Stop() + { + lock (_locko) + { + if (_isStopping) + { + return; + } + + if (IsMainDom == false) + { + return; // probably not needed + } + + _isStopping = true; } - public void Stop() + try { - lock (_locko) + foreach (Action callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) { - if (_isStopping) return; - if (IsMainDom == false) return; // probably not needed - _isStopping = true; - } - - try - { - foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) - { - callback(); // no timeout on callbacks - } - } - finally - { - // in any case... - IsMainDom = false; + callback(); // no timeout on callbacks } } - - protected virtual void Dispose(bool disposing) + finally { - if (!_disposedValue) - { - if (disposing) - { - Stop(); - } - _disposedValue = true; - } + // in any case... + IsMainDom = false; } + } - public void Dispose() + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + if (disposing) + { + Stop(); + } + + _disposedValue = true; } } } diff --git a/src/Umbraco.Core/StaticApplicationLogging.cs b/src/Umbraco.Core/StaticApplicationLogging.cs index f0d01d4073..eac0a3f51b 100644 --- a/src/Umbraco.Core/StaticApplicationLogging.cs +++ b/src/Umbraco.Core/StaticApplicationLogging.cs @@ -1,19 +1,18 @@ -using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class StaticApplicationLogging { - public static class StaticApplicationLogging - { - private static ILoggerFactory? s_loggerFactory; + private static ILoggerFactory? loggerFactory; - public static void Initialize(ILoggerFactory loggerFactory) => s_loggerFactory = loggerFactory; + public static ILogger Logger => CreateLogger(); - public static ILogger Logger => CreateLogger(); + public static void Initialize(ILoggerFactory loggerFactory) => StaticApplicationLogging.loggerFactory = loggerFactory; - public static ILogger CreateLogger() => s_loggerFactory?.CreateLogger() ?? NullLoggerFactory.Instance.CreateLogger(); + public static ILogger CreateLogger() => + loggerFactory?.CreateLogger() ?? NullLoggerFactory.Instance.CreateLogger(); - public static ILogger CreateLogger(Type type) => s_loggerFactory?.CreateLogger(type) ?? NullLogger.Instance; - } + public static ILogger CreateLogger(Type type) => loggerFactory?.CreateLogger(type) ?? NullLogger.Instance; } diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs index 3435c81780..2b1229be77 100644 --- a/src/Umbraco.Core/StringUdi.cs +++ b/src/Umbraco.Core/StringUdi.cs @@ -1,64 +1,51 @@ -using System; using System.ComponentModel; -using System.Linq; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a string-based entity identifier. +/// +[TypeConverter(typeof(UdiTypeConverter))] +public class StringUdi : Udi { /// - /// Represents a string-based entity identifier. + /// Initializes a new instance of the StringUdi class with an entity type and a string id. /// - [TypeConverter(typeof(UdiTypeConverter))] - public class StringUdi : Udi + /// The entity type part of the udi. + /// The string id part of the udi. + public StringUdi(string entityType, string id) + : base(entityType, "umb://" + entityType + "/" + EscapeUriString(id)) => + Id = id; + + /// + /// Initializes a new instance of the StringUdi class with a uri value. + /// + /// The uri value of the udi. + public StringUdi(Uri uriValue) + : base(uriValue) => + Id = Uri.UnescapeDataString(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash)); + + /// + /// The string part of the identifier. + /// + public string Id { get; } + + /// + public override bool IsRoot => Id == string.Empty; + + public StringUdi EnsureClosed() { - /// - /// The string part of the identifier. - /// - public string Id { get; private set; } - - /// - /// Initializes a new instance of the StringUdi class with an entity type and a string id. - /// - /// The entity type part of the udi. - /// The string id part of the udi. - public StringUdi(string entityType, string id) - : base(entityType, "umb://" + entityType + "/" + EscapeUriString(id)) - { - Id = id; - } - - /// - /// Initializes a new instance of the StringUdi class with a uri value. - /// - /// The uri value of the udi. - public StringUdi(Uri uriValue) - : base(uriValue) - { - Id = Uri.UnescapeDataString(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash)); - } - - private static string EscapeUriString(string s) - { - // Uri.EscapeUriString preserves / but also [ and ] which is bad - // Uri.EscapeDataString does not preserve / which is bad - - // reserved = : / ? # [ ] @ ! $ & ' ( ) * + , ; = - // unreserved = alpha digit - . _ ~ - - // we want to preserve the / and the unreserved - // so... - return string.Join("/", s.Split(Constants.CharArrays.ForwardSlash).Select(Uri.EscapeDataString)); - } - - /// - public override bool IsRoot - { - get { return Id == string.Empty; } - } - - public StringUdi EnsureClosed() - { - EnsureNotRoot(); - return this; - } + EnsureNotRoot(); + return this; } + + private static string EscapeUriString(string s) => + + // Uri.EscapeUriString preserves / but also [ and ] which is bad + // Uri.EscapeDataString does not preserve / which is bad + // reserved = : / ? # [ ] @ ! $ & ' ( ) * + , ; = + // unreserved = alpha digit - . _ ~ + // we want to preserve the / and the unreserved + // so... + string.Join("/", s.Split(Constants.CharArrays.ForwardSlash).Select(Uri.EscapeDataString)); } diff --git a/src/Umbraco.Core/Strings/CleanStringType.cs b/src/Umbraco.Core/Strings/CleanStringType.cs index 771e834d35..75ad000505 100644 --- a/src/Umbraco.Core/Strings/CleanStringType.cs +++ b/src/Umbraco.Core/Strings/CleanStringType.cs @@ -1,124 +1,121 @@ -using System; +namespace Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Strings +/// +/// Specifies the type of a clean string. +/// +/// +/// Specifies its casing, and its encoding. +/// +[Flags] +public enum CleanStringType { + // note: you have 32 bits at your disposal + // 0xffffffff + + // no value + /// - /// Specifies the type of a clean string. + /// No value. + /// + None = 0x00, + + // casing values + + /// + /// Flag mask for casing. + /// + CaseMask = PascalCase | CamelCase | Unchanged | LowerCase | UpperCase | UmbracoCase, + + /// + /// Pascal casing eg "PascalCase". + /// + PascalCase = 0x01, + + /// + /// Camel casing eg "camelCase". + /// + CamelCase = 0x02, + + /// + /// Unchanged casing eg "UncHanGed". + /// + Unchanged = 0x04, + + /// + /// Lower casing eg "lowercase". + /// + LowerCase = 0x08, + + /// + /// Upper casing eg "UPPERCASE". + /// + UpperCase = 0x10, + + /// + /// Umbraco "safe alias" case. /// /// - /// Specifies its casing, and its encoding. + /// Uppercases the first char of each term except for the first + /// char of the string, everything else including the first char of the + /// string is unchanged. /// - [Flags] - public enum CleanStringType - { - // note: you have 32 bits at your disposal - // 0xffffffff + UmbracoCase = 0x20, - // no value + // encoding values - /// - /// No value. - /// - None = 0x00, + /// + /// Flag mask for encoding. + /// + CodeMask = Utf8 | Ascii | TryAscii, + // Unicode encoding is obsolete, use Utf8 + // Unicode = 0x0100, - // casing values + /// + /// Utf8 encoding. + /// + Utf8 = 0x0200, - /// - /// Flag mask for casing. - /// - CaseMask = PascalCase | CamelCase | Unchanged | LowerCase | UpperCase | UmbracoCase, + /// + /// Ascii encoding. + /// + Ascii = 0x0400, - /// - /// Pascal casing eg "PascalCase". - /// - PascalCase = 0x01, + /// + /// Ascii encoding, if possible. + /// + TryAscii = 0x0800, - /// - /// Camel casing eg "camelCase". - /// - CamelCase = 0x02, + // role values - /// - /// Unchanged casing eg "UncHanGed". - /// - Unchanged = 0x04, + /// + /// Flag mask for role. + /// + RoleMask = UrlSegment | Alias | UnderscoreAlias | FileName | ConvertCase, - /// - /// Lower casing eg "lowercase". - /// - LowerCase = 0x08, + /// + /// Url role. + /// + UrlSegment = 0x010000, - /// - /// Upper casing eg "UPPERCASE". - /// - UpperCase = 0x10, + /// + /// Alias role. + /// + Alias = 0x020000, - /// - /// Umbraco "safe alias" case. - /// - /// Uppercases the first char of each term except for the first - /// char of the string, everything else including the first char of the - /// string is unchanged. - UmbracoCase = 0x20, + /// + /// FileName role. + /// + FileName = 0x040000, + /// + /// ConvertCase role. + /// + ConvertCase = 0x080000, - // encoding values - - /// - /// Flag mask for encoding. - /// - CodeMask = Utf8 | Ascii | TryAscii, - - // Unicode encoding is obsolete, use Utf8 - //Unicode = 0x0100, - - /// - /// Utf8 encoding. - /// - Utf8 = 0x0200, - - /// - /// Ascii encoding. - /// - Ascii = 0x0400, - - /// - /// Ascii encoding, if possible. - /// - TryAscii = 0x0800, - - // role values - - /// - /// Flag mask for role. - /// - RoleMask = UrlSegment | Alias | UnderscoreAlias | FileName | ConvertCase, - - /// - /// Url role. - /// - UrlSegment = 0x010000, - - /// - /// Alias role. - /// - Alias = 0x020000, - - /// - /// FileName role. - /// - FileName = 0x040000, - - /// - /// ConvertCase role. - /// - ConvertCase = 0x080000, - - /// - /// UnderscoreAlias role. - /// - /// This is Alias + leading underscore. - UnderscoreAlias = 0x100000 - } + /// + /// UnderscoreAlias role. + /// + /// This is Alias + leading underscore. + UnderscoreAlias = 0x100000, } diff --git a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs b/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs index a95a3edfc2..e2eb3df7a4 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs +++ b/src/Umbraco.Core/Strings/Css/StylesheetHelper.cs @@ -1,63 +1,70 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Strings.Css +namespace Umbraco.Cms.Core.Strings.Css; + +public class StylesheetHelper { - public class StylesheetHelper + private const string RuleRegexFormat = + @"/\*\*\s*umb_name:\s*(?{0}?)\s*\*/\s*(?[^,{{]*?)\s*{{\s*(?.*?)\s*}}"; + + public static IEnumerable ParseRules(string? input) { - private const string RuleRegexFormat = @"/\*\*\s*umb_name:\s*(?{0}?)\s*\*/\s*(?[^,{{]*?)\s*{{\s*(?.*?)\s*}}"; + var rules = new List(); + var ruleRegex = new Regex( + string.Format(RuleRegexFormat, @"[^\*\r\n]*"), + RegexOptions.IgnoreCase | RegexOptions.Singleline); - public static IEnumerable ParseRules(string? input) + if (input is not null) { - var rules = new List(); - var ruleRegex = new Regex(string.Format(RuleRegexFormat, @"[^\*\r\n]*"), RegexOptions.IgnoreCase | RegexOptions.Singleline); + var contents = input; + MatchCollection ruleMatches = ruleRegex.Matches(contents); - if (input is not null) + foreach (Match match in ruleMatches) { - var contents = input; - var ruleMatches = ruleRegex.Matches(contents); + var name = match.Groups["Name"].Value; - foreach (Match match in ruleMatches) + // If this name already exists, only use the first one + if (rules.Any(x => x.Name == name)) { - var name = match.Groups["Name"].Value; - - //If this name already exists, only use the first one - if (rules.Any(x => x.Name == name)) continue; - - rules.Add(new StylesheetRule - { - Name = match.Groups["Name"].Value, - Selector = match.Groups["Selector"].Value, - // Only match first selector when chained together - Styles = string.Join(Environment.NewLine, match.Groups["Styles"].Value.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None).Select(x => x.Trim()).ToArray()) - }); + continue; } + + rules.Add(new StylesheetRule + { + Name = match.Groups["Name"].Value, + Selector = match.Groups["Selector"].Value, + + // Only match first selector when chained together + Styles = string.Join( + Environment.NewLine, + match.Groups["Styles"].Value.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None) + .Select(x => x.Trim()).ToArray()), + }); } - - - return rules; } - public static string? ReplaceRule(string? input, string oldRuleName, StylesheetRule? rule) + return rules; + } + + public static string? ReplaceRule(string? input, string oldRuleName, StylesheetRule? rule) + { + var contents = input; + if (contents is not null) { - var contents = input; - if (contents is not null) - { - var ruleRegex = new Regex(string.Format(RuleRegexFormat, oldRuleName.EscapeRegexSpecialCharacters()), RegexOptions.IgnoreCase | RegexOptions.Singleline); - contents = ruleRegex.Replace(contents, rule != null ? rule.ToString() : ""); - } - - return contents; + var ruleRegex = new Regex( + string.Format(RuleRegexFormat, oldRuleName.EscapeRegexSpecialCharacters()), + RegexOptions.IgnoreCase | RegexOptions.Singleline); + contents = ruleRegex.Replace(contents, rule != null ? rule.ToString() : string.Empty); } - public static string AppendRule(string? input, StylesheetRule rule) - { - var contents = input; - contents += Environment.NewLine + Environment.NewLine + rule; - return contents; - } + return contents; + } + + public static string AppendRule(string? input, StylesheetRule rule) + { + var contents = input; + contents += Environment.NewLine + Environment.NewLine + rule; + return contents; } } diff --git a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs index 06a888c812..4b726f34ef 100644 --- a/src/Umbraco.Core/Strings/Css/StylesheetRule.cs +++ b/src/Umbraco.Core/Strings/Css/StylesheetRule.cs @@ -1,41 +1,43 @@ -using System; using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Strings.Css +namespace Umbraco.Cms.Core.Strings.Css; + +public class StylesheetRule { - public class StylesheetRule + public string Name { get; set; } = null!; + + public string Selector { get; set; } = null!; + + public string Styles { get; set; } = null!; + + public override string ToString() { - public string Name { get; set; } = null!; + var sb = new StringBuilder(); + sb.Append("/**"); + sb.AppendFormat("umb_name:{0}", Name); + sb.Append("*/"); + sb.Append(Environment.NewLine); + sb.Append(Selector); + sb.Append(" {"); + sb.Append(Environment.NewLine); - public string Selector { get; set; } = null!; - - public string Styles { get; set; } = null!; - - public override string ToString() + // append nicely formatted style rules + // - using tabs because the back office code editor uses tabs + if (Styles.IsNullOrWhiteSpace() == false) { - var sb = new StringBuilder(); - sb.Append("/**"); - sb.AppendFormat("umb_name:{0}", Name); - sb.Append("*/"); - sb.Append(Environment.NewLine); - sb.Append(Selector); - sb.Append(" {"); - sb.Append(Environment.NewLine); - // append nicely formatted style rules - // - using tabs because the back office code editor uses tabs - if (Styles.IsNullOrWhiteSpace() == false) + // since we already have a string builder in play here, we'll append to it the "hard" way + // instead of using string interpolation (for increased performance) + foreach (var style in + Styles?.Split(Constants.CharArrays.Semicolon, StringSplitOptions.RemoveEmptyEntries) ?? + Array.Empty()) { - // since we already have a string builder in play here, we'll append to it the "hard" way - // instead of using string interpolation (for increased performance) - foreach (var style in Styles?.Split(Constants.CharArrays.Semicolon, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty()) - { - sb.Append("\t").Append(style.StripNewLines().Trim()).Append(";").Append(Environment.NewLine); - } + sb.Append("\t").Append(style.StripNewLines().Trim()).Append(";").Append(Environment.NewLine); } - sb.Append("}"); - - return sb.ToString(); } + + sb.Append("}"); + + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs index d4d781c9bc..ea93a099f8 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs @@ -1,8 +1,5 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; -using System.IO; -using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Extensions; @@ -143,9 +140,11 @@ namespace Umbraco.Cms.Core.Strings public virtual string CleanStringForSafeFileName(string text, string culture) { if (string.IsNullOrWhiteSpace(text)) + { return string.Empty; + } - culture = culture ?? ""; + culture = culture ?? string.Empty; text = text.ReplaceMany(Path.GetInvalidFileNameChars(), '-'); var name = Path.GetFileNameWithoutExtension(text); @@ -153,12 +152,17 @@ namespace Umbraco.Cms.Core.Strings Debug.Assert(name != null, "name != null"); if (name.Length > 0) + { name = CleanString(name, CleanStringType.FileName, culture); + } + Debug.Assert(ext != null, "ext != null"); if (ext.Length > 0) + { ext = CleanString(ext.Substring(1), CleanStringType.FileName, culture); + } - return ext.Length > 0 ? (name + "." + ext) : name; + return ext.Length > 0 ? name + "." + ext : name; } #endregion @@ -190,10 +194,7 @@ namespace Umbraco.Cms.Core.Strings /// strings are cleaned up to camelCase and Ascii. /// The clean string. /// The string is cleaned in the context of the default culture. - public string CleanString(string text, CleanStringType stringType) - { - return CleanString(text, stringType, _config.DefaultCulture, null); - } + public string CleanString(string text, CleanStringType stringType) => CleanString(text, stringType, _config.DefaultCulture, null); /// /// Cleans a string, using a specified separator. @@ -204,10 +205,7 @@ namespace Umbraco.Cms.Core.Strings /// The separator. /// The clean string. /// The string is cleaned in the context of the default culture. - public string CleanString(string text, CleanStringType stringType, char separator) - { - return CleanString(text, stringType, _config.DefaultCulture, separator); - } + public string CleanString(string text, CleanStringType stringType, char separator) => CleanString(text, stringType, _config.DefaultCulture, separator); /// /// Cleans a string in the context of a specified culture. @@ -239,32 +237,43 @@ namespace Umbraco.Cms.Core.Strings protected virtual string CleanString(string text, CleanStringType stringType, string? culture, char? separator) { // be safe - if (text == null) throw new ArgumentNullException(nameof(text)); - culture = culture ?? ""; + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + culture = culture ?? string.Empty; // get config - var config = _config.For(stringType, culture); + DefaultShortStringHelperConfig.Config config = _config.For(stringType, culture); stringType = config.StringTypeExtend(stringType); // apply defaults if ((stringType & CleanStringType.CaseMask) == CleanStringType.None) + { stringType |= CleanStringType.CamelCase; + } + if ((stringType & CleanStringType.CodeMask) == CleanStringType.None) + { stringType |= CleanStringType.Ascii; + } // use configured unless specified separator = separator ?? config.Separator; // apply pre-filter if (config.PreFilter != null) + { text = config.PreFilter(text); + } // apply replacements //if (config.Replacements != null) // text = ReplaceMany(text, config.Replacements); // recode - var codeType = stringType & CleanStringType.CodeMask; + CleanStringType codeType = stringType & CleanStringType.CodeMask; switch (codeType) { case CleanStringType.Ascii: @@ -273,7 +282,11 @@ namespace Umbraco.Cms.Core.Strings case CleanStringType.TryAscii: const char ESC = (char) 27; var ctext = Utf8ToAsciiConverter.ToAsciiString(text, ESC); - if (ctext.Contains(ESC) == false) text = ctext; + if (ctext.Contains(ESC) == false) + { + text = ctext; + } + break; default: text = RemoveSurrogatePairs(text); @@ -285,7 +298,9 @@ namespace Umbraco.Cms.Core.Strings // apply post-filter if (config.PostFilter != null) + { text = config.PostFilter(text); + } return text; } @@ -323,7 +338,7 @@ namespace Umbraco.Cms.Core.Strings int opos = 0, ipos = 0; var state = StateBreak; - culture = culture ?? ""; + culture = culture ?? string.Empty; caseType &= CleanStringType.CaseMask; // if we apply global ToUpper or ToLower to text here @@ -364,9 +379,13 @@ namespace Umbraco.Cms.Core.Strings { ipos = i; if (opos > 0 && separator != char.MinValue) + { output[opos++] = separator; + } + state = isUpper ? StateUp : StateWord; } + break; // within a term / word @@ -379,8 +398,11 @@ namespace Umbraco.Cms.Core.Strings ipos = i; state = isTerm ? StateUp : StateBreak; if (state != StateBreak && separator != char.MinValue) + { output[opos++] = separator; + } } + break; // within a term / acronym @@ -391,14 +413,19 @@ namespace Umbraco.Cms.Core.Strings { // whether it's part of the acronym depends on whether we're greedy if (isTerm && config.GreedyAcronyms == false) + { i -= 1; // handle that char again, in another state - not part of the acronym + } + if (i - ipos > 1) // single-char can't be an acronym { CopyTerm(input, ipos, output, ref opos, i - ipos, caseType, culture, true); ipos = i; state = isTerm ? StateWord : StateBreak; if (state != StateBreak && separator != char.MinValue) + { output[opos++] = separator; + } } else if (isTerm) { @@ -411,6 +438,7 @@ namespace Umbraco.Cms.Core.Strings // keep moving forward as a word state = StateWord; } + break; // within a term / uppercase = could be a word or an acronym @@ -455,18 +483,19 @@ namespace Umbraco.Cms.Core.Strings } // note: supports surrogate pairs in input string - internal void CopyTerm(string input, int ipos, char[] output, ref int opos, int len, - CleanStringType caseType, string culture, bool isAcronym) + internal void CopyTerm(string input, int ipos, char[] output, ref int opos, int len, CleanStringType caseType, string culture, bool isAcronym) { var term = input.Substring(ipos, len); - var cultureInfo = string.IsNullOrEmpty(culture) ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture); + CultureInfo cultureInfo = string.IsNullOrEmpty(culture) ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture); if (isAcronym) { if ((caseType == CleanStringType.CamelCase && len <= 2 && opos > 0) || (caseType == CleanStringType.PascalCase && len <= 2) || - (caseType == CleanStringType.UmbracoCase)) + caseType == CleanStringType.UmbracoCase) + { caseType = CleanStringType.Unchanged; + } } // note: MSDN seems to imply that ToUpper or ToLower preserve the length @@ -586,7 +615,9 @@ namespace Umbraco.Cms.Core.Strings { // be safe if (text == null) + { throw new ArgumentNullException(nameof(text)); + } var input = text.ToCharArray(); var output = new char[input.Length * 2]; @@ -603,7 +634,10 @@ namespace Umbraco.Cms.Core.Strings if (upos == 0) { if (opos > 0) + { output[opos++] = separator; + } + upos = i + 1; } } @@ -612,15 +646,24 @@ namespace Umbraco.Cms.Core.Strings if (upos > 0) { if (upos < i && opos > 0) + { output[opos++] = separator; + } + upos = 0; } + output[opos++] = a; } + a = c; } + if (a != char.MinValue) + { output[opos++] = a; + } + return new string(output, 0, opos); } diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs index 4f2d202155..ec7ed9d002 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs @@ -1,227 +1,249 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.UmbracoSettings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Strings -{ - public class DefaultShortStringHelperConfig - { - private readonly Dictionary> _configs = new Dictionary>(); +namespace Umbraco.Cms.Core.Strings; - public DefaultShortStringHelperConfig Clone() +public class DefaultShortStringHelperConfig +{ + private readonly Dictionary> _configs = new(); + + public string DefaultCulture { get; set; } = string.Empty; // invariant + + public Dictionary? UrlReplaceCharacters { get; set; } + + public DefaultShortStringHelperConfig Clone() + { + var config = new DefaultShortStringHelperConfig { - var config = new DefaultShortStringHelperConfig + DefaultCulture = DefaultCulture, + UrlReplaceCharacters = UrlReplaceCharacters, + }; + + foreach (KeyValuePair> kvp1 in _configs) + { + Dictionary c = config._configs[kvp1.Key] = + new Dictionary(); + foreach (KeyValuePair kvp2 in _configs[kvp1.Key]) { - DefaultCulture = DefaultCulture, - UrlReplaceCharacters = UrlReplaceCharacters + c[kvp2.Key] = kvp2.Value.Clone(); + } + } + + return config; + } + + public DefaultShortStringHelperConfig WithConfig(Config config) => + WithConfig(DefaultCulture, CleanStringType.RoleMask, config); + + public DefaultShortStringHelperConfig WithConfig(CleanStringType stringRole, Config config) => + WithConfig(DefaultCulture, stringRole, config); + + public DefaultShortStringHelperConfig WithConfig(string? culture, CleanStringType stringRole, Config config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + culture = culture ?? string.Empty; + + if (_configs.ContainsKey(culture) == false) + { + _configs[culture] = new Dictionary(); + } + + _configs[culture][stringRole] = config; + return this; + } + + /// + /// Sets the default configuration. + /// + /// The short string helper. + public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings) + { + IEnumerable charCollection = requestHandlerSettings.GetCharReplacements(); + + UrlReplaceCharacters = charCollection + .Where(x => string.IsNullOrEmpty(x.Char) == false) + .ToDictionary(x => x.Char, x => x.Replacement); + + CleanStringType urlSegmentConvertTo = CleanStringType.Utf8; + if (requestHandlerSettings.ShouldConvertUrlsToAscii) + { + urlSegmentConvertTo = CleanStringType.Ascii; + } + + if (requestHandlerSettings.ShouldTryConvertUrlsToAscii) + { + urlSegmentConvertTo = CleanStringType.TryAscii; + } + + return WithConfig(CleanStringType.UrlSegment, new Config + { + PreFilter = ApplyUrlReplaceCharacters, + PostFilter = x => CutMaxLength(x, 240), + IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = urlSegmentConvertTo | CleanStringType.LowerCase, + BreakTermsOnUpper = false, + Separator = '-', + }).WithConfig(CleanStringType.FileName, new Config + { + PreFilter = ApplyUrlReplaceCharacters, + IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = CleanStringType.Utf8 | CleanStringType.LowerCase, + BreakTermsOnUpper = false, + Separator = '-', + }).WithConfig(CleanStringType.Alias, new Config + { + PreFilter = ApplyUrlReplaceCharacters, + IsTerm = (c, leading) => leading + ? char.IsLetter(c) // only letters + : char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, + BreakTermsOnUpper = false, + }).WithConfig(CleanStringType.UnderscoreAlias, new Config + { + PreFilter = ApplyUrlReplaceCharacters, + IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, + BreakTermsOnUpper = false, + }).WithConfig(CleanStringType.ConvertCase, new Config + { + PreFilter = null, + IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore + StringType = CleanStringType.Ascii, + BreakTermsOnUpper = true, + }); + } + + // internal: we don't want ppl to retrieve a config and modify it + // (the helper uses a private clone to prevent modifications) + internal Config For(CleanStringType stringType, string? culture) + { + culture = culture ?? string.Empty; + stringType = stringType & CleanStringType.RoleMask; + + Dictionary config; + if (_configs.ContainsKey(culture)) + { + config = _configs[culture]; + + // have we got a config for _that_ role? + if (config.ContainsKey(stringType)) + { + return config[stringType]; + } + + // have we got a generic config for _all_ roles? + if (config.ContainsKey(CleanStringType.RoleMask)) + { + return config[CleanStringType.RoleMask]; + } + } + else if (_configs.ContainsKey(DefaultCulture)) + { + config = _configs[DefaultCulture]; + + // have we got a config for _that_ role? + if (config.ContainsKey(stringType)) + { + return config[stringType]; + } + + // have we got a generic config for _all_ roles? + if (config.ContainsKey(CleanStringType.RoleMask)) + { + return config[CleanStringType.RoleMask]; + } + } + + return Config.NotConfigured; + } + + /// + /// Returns a new string in which characters have been replaced according to the Umbraco settings UrlReplaceCharacters. + /// + /// The string to filter. + /// The filtered string. + public string ApplyUrlReplaceCharacters(string s) => + UrlReplaceCharacters == null ? s : s.ReplaceMany(UrlReplaceCharacters); + + public static string CutMaxLength(string text, int length) => + text.Length <= length ? text : text.Substring(0, length); + + public sealed class Config + { + internal static readonly Config NotConfigured = new(); + + public Config() + { + StringType = CleanStringType.Utf8 | CleanStringType.Unchanged; + PreFilter = null; + PostFilter = null; + IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c); + BreakTermsOnUpper = false; + CutAcronymOnNonUpper = false; + GreedyAcronyms = false; + Separator = char.MinValue; + } + + public Func? PreFilter { get; set; } + + public Func? PostFilter { get; set; } + + public Func IsTerm { get; set; } + + public CleanStringType StringType { get; set; } + + // indicate whether an uppercase within a term eg "fooBar" is to break + // into a new term, or to be considered as part of the current term + public bool BreakTermsOnUpper { get; set; } + + // indicate whether a non-uppercase within an acronym eg "FOOBar" is to cut + // the acronym (at "B" or "a" depending on GreedyAcronyms) or to give + // up the acronym and treat the term as a word + public bool CutAcronymOnNonUpper { get; set; } + + // indicates whether acronyms parsing is greedy ie whether "FOObar" is + // "FOO" + "bar" (greedy) or "FO" + "Obar" (non-greedy) + public bool GreedyAcronyms { get; set; } + + // the separator char + // but then how can we tell we don't want any? + public char Separator { get; set; } + + public Config Clone() => + new Config + { + PreFilter = PreFilter, + PostFilter = PostFilter, + IsTerm = IsTerm, + StringType = StringType, + BreakTermsOnUpper = BreakTermsOnUpper, + CutAcronymOnNonUpper = CutAcronymOnNonUpper, + GreedyAcronyms = GreedyAcronyms, + Separator = Separator, }; - foreach (var kvp1 in _configs) - { - var c = config._configs[kvp1.Key] = new Dictionary(); - foreach (var kvp2 in _configs[kvp1.Key]) - c[kvp2.Key] = kvp2.Value.Clone(); - } - - return config; - } - - public string DefaultCulture { get; set; } = ""; // invariant - - public Dictionary? UrlReplaceCharacters { get; set; } - - public DefaultShortStringHelperConfig WithConfig(Config config) + // extends the config + public CleanStringType StringTypeExtend(CleanStringType stringType) { - return WithConfig(DefaultCulture, CleanStringType.RoleMask, config); - } - - public DefaultShortStringHelperConfig WithConfig(CleanStringType stringRole, Config config) - { - return WithConfig(DefaultCulture, stringRole, config); - } - - public DefaultShortStringHelperConfig WithConfig(string culture, CleanStringType stringRole, Config config) - { - if (config == null) throw new ArgumentNullException(nameof(config)); - - culture = culture ?? ""; - - if (_configs.ContainsKey(culture) == false) - _configs[culture] = new Dictionary(); - _configs[culture][stringRole] = config; - return this; - } - - /// - /// Sets the default configuration. - /// - /// The short string helper. - public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings) - { - IEnumerable charCollection = requestHandlerSettings.GetCharReplacements(); - - UrlReplaceCharacters = charCollection - .Where(x => string.IsNullOrEmpty(x.Char) == false) - .ToDictionary(x => x.Char, x => x.Replacement); - - var urlSegmentConvertTo = CleanStringType.Utf8; - if (requestHandlerSettings.ShouldConvertUrlsToAscii) - urlSegmentConvertTo = CleanStringType.Ascii; - if (requestHandlerSettings.ShouldTryConvertUrlsToAscii) - urlSegmentConvertTo = CleanStringType.TryAscii; - - return WithConfig(CleanStringType.UrlSegment, new Config + CleanStringType st = StringType; + foreach (CleanStringType mask in new[] { CleanStringType.CaseMask, CleanStringType.CodeMask }) { - PreFilter = ApplyUrlReplaceCharacters, - PostFilter = x => CutMaxLength(x, 240), - IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = urlSegmentConvertTo | CleanStringType.LowerCase, - BreakTermsOnUpper = false, - Separator = '-' - }).WithConfig(CleanStringType.FileName, new Config - { - PreFilter = ApplyUrlReplaceCharacters, - IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = CleanStringType.Utf8 | CleanStringType.LowerCase, - BreakTermsOnUpper = false, - Separator = '-' - }).WithConfig(CleanStringType.Alias, new Config - { - PreFilter = ApplyUrlReplaceCharacters, - IsTerm = (c, leading) => leading - ? char.IsLetter(c) // only letters - : (char.IsLetterOrDigit(c) || c == '_'), // letter, digit or underscore - StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, - BreakTermsOnUpper = false - }).WithConfig(CleanStringType.UnderscoreAlias, new Config - { - PreFilter = ApplyUrlReplaceCharacters, - IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, - BreakTermsOnUpper = false - }).WithConfig(CleanStringType.ConvertCase, new Config - { - PreFilter = null, - IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = CleanStringType.Ascii, - BreakTermsOnUpper = true - }); - } - - // internal: we don't want ppl to retrieve a config and modify it - // (the helper uses a private clone to prevent modifications) - internal Config For(CleanStringType stringType, string culture) - { - culture = culture ?? ""; - stringType = stringType & CleanStringType.RoleMask; - - Dictionary config; - if (_configs.ContainsKey(culture)) - { - config = _configs[culture]; - if (config.ContainsKey(stringType)) // have we got a config for _that_ role? - return config[stringType]; - if (config.ContainsKey(CleanStringType.RoleMask)) // have we got a generic config for _all_ roles? - return config[CleanStringType.RoleMask]; - } - else if (_configs.ContainsKey(DefaultCulture)) - { - config = _configs[DefaultCulture]; - if (config.ContainsKey(stringType)) // have we got a config for _that_ role? - return config[stringType]; - if (config.ContainsKey(CleanStringType.RoleMask)) // have we got a generic config for _all_ roles? - return config[CleanStringType.RoleMask]; - } - - return Config.NotConfigured; - } - - public sealed class Config - { - public Config() - { - StringType = CleanStringType.Utf8 | CleanStringType.Unchanged; - PreFilter = null; - PostFilter = null; - IsTerm = (c, leading) => leading ? char.IsLetter(c) : char.IsLetterOrDigit(c); - BreakTermsOnUpper = false; - CutAcronymOnNonUpper = false; - GreedyAcronyms = false; - Separator = char.MinValue; - } - - public Config Clone() - { - return new Config + CleanStringType a = stringType & mask; + if (a == 0) { - PreFilter = PreFilter, - PostFilter = PostFilter, - IsTerm = IsTerm, - StringType = StringType, - BreakTermsOnUpper = BreakTermsOnUpper, - CutAcronymOnNonUpper = CutAcronymOnNonUpper, - GreedyAcronyms = GreedyAcronyms, - Separator = Separator - }; - } - - public Func? PreFilter { get; set; } - public Func? PostFilter { get; set; } - public Func IsTerm { get; set; } - - public CleanStringType StringType { get; set; } - - // indicate whether an uppercase within a term eg "fooBar" is to break - // into a new term, or to be considered as part of the current term - public bool BreakTermsOnUpper { get; set; } - - // indicate whether a non-uppercase within an acronym eg "FOOBar" is to cut - // the acronym (at "B" or "a" depending on GreedyAcronyms) or to give - // up the acronym and treat the term as a word - public bool CutAcronymOnNonUpper { get; set; } - - // indicates whether acronyms parsing is greedy ie whether "FOObar" is - // "FOO" + "bar" (greedy) or "FO" + "Obar" (non-greedy) - public bool GreedyAcronyms { get; set; } - - // the separator char - // but then how can we tell we don't want any? - public char Separator { get; set; } - - // extends the config - public CleanStringType StringTypeExtend(CleanStringType stringType) - { - var st = StringType; - foreach (var mask in new[] { CleanStringType.CaseMask, CleanStringType.CodeMask }) - { - var a = stringType & mask; - if (a == 0) continue; - - st = st & ~mask; // clear what we have - st = st | a; // set the new value + continue; } - return st; + + st = st & ~mask; // clear what we have + st = st | a; // set the new value } - internal static readonly Config NotConfigured = new Config(); - } - - /// - /// Returns a new string in which characters have been replaced according to the Umbraco settings UrlReplaceCharacters. - /// - /// The string to filter. - /// The filtered string. - public string ApplyUrlReplaceCharacters(string s) - { - return UrlReplaceCharacters == null ? s : s.ReplaceMany(UrlReplaceCharacters); - } - - public static string CutMaxLength(string text, int length) - { - return text.Length <= length ? text : text.Substring(0, length); + return st; } } } diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index 22b8a40c0e..36c0d6e85e 100644 --- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs @@ -1,45 +1,43 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Default implementation of IUrlSegmentProvider. +/// +public class DefaultUrlSegmentProvider : IUrlSegmentProvider { + private readonly IShortStringHelper _shortStringHelper; + + public DefaultUrlSegmentProvider(IShortStringHelper shortStringHelper) => _shortStringHelper = shortStringHelper; + /// - /// Default implementation of IUrlSegmentProvider. + /// Gets the URL segment for a specified content and culture. /// - public class DefaultUrlSegmentProvider : IUrlSegmentProvider + /// The content. + /// The culture. + /// The URL segment. + public string? GetUrlSegment(IContentBase content, string? culture = null) => + GetUrlSegmentSource(content, culture)?.ToUrlSegment(_shortStringHelper, culture); + + private static string? GetUrlSegmentSource(IContentBase content, string? culture) { - private readonly IShortStringHelper _shortStringHelper; - - public DefaultUrlSegmentProvider(IShortStringHelper shortStringHelper) + string? source = null; + if (content.HasProperty(Constants.Conventions.Content.UrlName)) { - _shortStringHelper = shortStringHelper; + source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); } - /// - /// Gets the URL segment for a specified content and culture. - /// - /// The content. - /// The culture. - /// The URL segment. - public string? GetUrlSegment(IContentBase content, string? culture = null) + if (string.IsNullOrWhiteSpace(source)) { - return GetUrlSegmentSource(content, culture)?.ToUrlSegment(_shortStringHelper, culture); + // If the name of a node has been updated, but it has not been published, the url should use the published name, not the current node name + // If this node has never been published (GetPublishName is null), use the unpublished name + source = content is IContent document && document.Edited && document.GetPublishName(culture) != null + ? document.GetPublishName(culture) + : content.GetCultureName(culture); } - private static string? GetUrlSegmentSource(IContentBase content, string? culture) - { - string? source = null; - if (content.HasProperty(Constants.Conventions.Content.UrlName)) - source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); - if (string.IsNullOrWhiteSpace(source)) - { - // If the name of a node has been updated, but it has not been published, the url should use the published name, not the current node name - // If this node has never been published (GetPublishName is null), use the unpublished name - source = (content is IContent document) && document.Edited && document.GetPublishName(culture) != null - ? document.GetPublishName(culture) - : content.GetCultureName(culture); - } - return source; - } + return source; } } diff --git a/src/Umbraco.Core/Strings/Diff.cs b/src/Umbraco.Core/Strings/Diff.cs index 8d7ef9feaa..e8a3fdf84c 100644 --- a/src/Umbraco.Core/Strings/Diff.cs +++ b/src/Umbraco.Core/Strings/Diff.cs @@ -1,507 +1,540 @@ -using System; using System.Collections; using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// This Class implements the Difference Algorithm published in +/// "An O(ND) Difference Algorithm and its Variations" by Eugene Myers +/// Algorithmica Vol. 1 No. 2, 1986, p 251. +/// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents +/// each line is converted into a (hash) number. See DiffText(). +/// diff.cs: A port of the algorithm to C# +/// Copyright (c) by Matthias Hertel, http://www.mathertel.de +/// This work is licensed under a BSD style license. See http://www.mathertel.de/License.aspx +/// +internal class Diff { /// - /// This Class implements the Difference Algorithm published in - /// "An O(ND) Difference Algorithm and its Variations" by Eugene Myers - /// Algorithmica Vol. 1 No. 2, 1986, p 251. - /// - /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents - /// each line is converted into a (hash) number. See DiffText(). - /// - /// diff.cs: A port of the algorithm to C# - /// Copyright (c) by Matthias Hertel, http://www.mathertel.de - /// This work is licensed under a BSD style license. See http://www.mathertel.de/License.aspx + /// Find the difference in 2 texts, comparing by text lines. /// - internal class Diff + /// A-version of the text (usually the old one) + /// B-version of the text (usually the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffText(string textA, string textB) => + DiffText(textA, textB, false, false, false); // DiffText + + /// + /// Find the difference in 2 texts, comparing by text lines. + /// This method uses the DiffInt internally by 1st converting the string into char codes + /// then uses the diff int method + /// + /// A-version of the text (usually the old one) + /// B-version of the text (usually the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffText1(string textA, string textB) => + DiffInt(DiffCharCodes(textA, false), DiffCharCodes(textB, false)); + + /// + /// Find the difference in 2 text documents, comparing by text lines. + /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents + /// each line is converted into a (hash) number. This hash-value is computed by storing all + /// text lines into a common Hashtable so i can find duplicates in there, and generating a + /// new number each time a new text line is inserted. + /// + /// A-version of the text (usually the old one) + /// B-version of the text (usually the new one) + /// + /// When set to true, all leading and trailing whitespace characters are stripped out before the + /// comparison is done. + /// + /// + /// When set to true, all whitespace characters are converted to a single space character before + /// the comparison is done. + /// + /// + /// When set to true, all characters are converted to their lowercase equivalence before the + /// comparison is done. + /// + /// Returns a array of Items that describe the differences. + public static Item[] DiffText(string textA, string textB, bool trimSpace, bool ignoreSpace, bool ignoreCase) { - /// Data on one input file being compared. - /// - internal class DiffData + // prepare the input-text and convert to comparable numbers. + var h = new Hashtable(textA.Length + textB.Length); + + // The A-Version of the data (original data) to be compared. + var dataA = new DiffData(DiffCodes(textA, h, trimSpace, ignoreSpace, ignoreCase)); + + // The B-Version of the data (modified data) to be compared. + var dataB = new DiffData(DiffCodes(textB, h, trimSpace, ignoreSpace, ignoreCase)); + + h = null; // free up Hashtable memory (maybe) + + var max = dataA.Length + dataB.Length + 1; + + // vector for the (0,0) to (x,y) search + var downVector = new int[(2 * max) + 2]; + + // vector for the (u,v) to (N,M) search + var upVector = new int[(2 * max) + 2]; + + Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); + + Optimize(dataA); + Optimize(dataB); + return CreateDiffs(dataA, dataB); + } // DiffText + + /// + /// Find the difference in 2 arrays of integers. + /// + /// A-version of the numbers (usually the old one) + /// B-version of the numbers (usually the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffInt(int[] arrayA, int[] arrayB) + { + // The A-Version of the data (original data) to be compared. + var dataA = new DiffData(arrayA); + + // The B-Version of the data (modified data) to be compared. + var dataB = new DiffData(arrayB); + + var max = dataA.Length + dataB.Length + 1; + + // vector for the (0,0) to (x,y) search + var downVector = new int[(2 * max) + 2]; + + // vector for the (u,v) to (N,M) search + var upVector = new int[(2 * max) + 2]; + + Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); + return CreateDiffs(dataA, dataB); + } // Diff + + /// + /// Diffs the char codes. + /// + /// A text. + /// if set to true [ignore case]. + /// + private static int[] DiffCharCodes(string aText, bool ignoreCase) + { + if (ignoreCase) { - - /// Number of elements (lines). - internal int Length; - - /// Buffer of numbers that will be compared. - internal int[] Data; - - /// - /// Array of booleans that flag for modified data. - /// This is the result of the diff. - /// This means deletedA in the first Data or inserted in the second Data. - /// - internal bool[] Modified; - - /// - /// Initialize the Diff-Data buffer. - /// - /// reference to the buffer - internal DiffData(int[] initData) - { - Data = initData; - Length = initData.Length; - Modified = new bool[Length + 2]; - } // DiffData - - } // class DiffData - - /// details of one difference. - public struct Item - { - /// Start Line number in Data A. - public int StartA; - /// Start Line number in Data B. - public int StartB; - - /// Number of changes in Data A. - public int DeletedA; - /// Number of changes in Data B. - public int InsertedB; - } // Item - - /// - /// Shortest Middle Snake Return Data - /// - private struct Smsrd - { - internal int X, Y; - // internal int u, v; // 2002.09.20: no need for 2 points + aText = aText.ToUpperInvariant(); } - /// - /// Find the difference in 2 texts, comparing by text lines. - /// - /// A-version of the text (usually the old one) - /// B-version of the text (usually the new one) - /// Returns a array of Items that describe the differences. - public static Item[] DiffText(string textA, string textB) - { - return (DiffText(textA, textB, false, false, false)); - } // DiffText + var codes = new int[aText.Length]; - /// - /// Find the difference in 2 texts, comparing by text lines. - /// This method uses the DiffInt internally by 1st converting the string into char codes - /// then uses the diff int method - /// - /// A-version of the text (usually the old one) - /// B-version of the text (usually the new one) - /// Returns a array of Items that describe the differences. - public static Item[] DiffText1(string textA, string textB) + for (var n = 0; n < aText.Length; n++) { - return DiffInt(DiffCharCodes(textA, false), DiffCharCodes(textB, false)); + codes[n] = aText[n]; } + return codes; + } // DiffCharCodes - /// - /// Find the difference in 2 text documents, comparing by text lines. - /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents - /// each line is converted into a (hash) number. This hash-value is computed by storing all - /// text lines into a common Hashtable so i can find duplicates in there, and generating a - /// new number each time a new text line is inserted. - /// - /// A-version of the text (usually the old one) - /// B-version of the text (usually the new one) - /// When set to true, all leading and trailing whitespace characters are stripped out before the comparison is done. - /// When set to true, all whitespace characters are converted to a single space character before the comparison is done. - /// When set to true, all characters are converted to their lowercase equivalence before the comparison is done. - /// Returns a array of Items that describe the differences. - public static Item[] DiffText(string textA, string textB, bool trimSpace, bool ignoreSpace, bool ignoreCase) + /// + /// If a sequence of modified lines starts with a line that contains the same content + /// as the line that appends the changes, the difference sequence is modified so that the + /// appended line and not the starting line is marked as modified. + /// This leads to more readable diff sequences when comparing text files. + /// + /// A Diff data buffer containing the identified changes. + private static void Optimize(DiffData data) + { + var startPos = 0; + while (startPos < data.Length) { - // prepare the input-text and convert to comparable numbers. - var h = new Hashtable(textA.Length + textB.Length); - - // The A-Version of the data (original data) to be compared. - var dataA = new DiffData(DiffCodes(textA, h, trimSpace, ignoreSpace, ignoreCase)); - - // The B-Version of the data (modified data) to be compared. - var dataB = new DiffData(DiffCodes(textB, h, trimSpace, ignoreSpace, ignoreCase)); - - h = null; // free up Hashtable memory (maybe) - - var max = dataA.Length + dataB.Length + 1; - // vector for the (0,0) to (x,y) search - var downVector = new int[2 * max + 2]; - // vector for the (u,v) to (N,M) search - var upVector = new int[2 * max + 2]; - - Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); - - Optimize(dataA); - Optimize(dataB); - return CreateDiffs(dataA, dataB); - } // DiffText - - - /// - /// Diffs the char codes. - /// - /// A text. - /// if set to true [ignore case]. - /// - private static int[] DiffCharCodes(string aText, bool ignoreCase) - { - if (ignoreCase) - aText = aText.ToUpperInvariant(); - - var codes = new int[aText.Length]; - - for (int n = 0; n < aText.Length; n++) - codes[n] = (int)aText[n]; - - return (codes); - } // DiffCharCodes - - /// - /// If a sequence of modified lines starts with a line that contains the same content - /// as the line that appends the changes, the difference sequence is modified so that the - /// appended line and not the starting line is marked as modified. - /// This leads to more readable diff sequences when comparing text files. - /// - /// A Diff data buffer containing the identified changes. - private static void Optimize(DiffData data) - { - var startPos = 0; - while (startPos < data.Length) + while (startPos < data.Length && data.Modified[startPos] == false) { - while ((startPos < data.Length) && (data.Modified[startPos] == false)) - startPos++; - int endPos = startPos; - while ((endPos < data.Length) && (data.Modified[endPos] == true)) - endPos++; - - if ((endPos < data.Length) && (data.Data[startPos] == data.Data[endPos])) - { - data.Modified[startPos] = false; - data.Modified[endPos] = true; - } - else - { - startPos = endPos; - } // if - } // while - } // Optimize - - - /// - /// Find the difference in 2 arrays of integers. - /// - /// A-version of the numbers (usually the old one) - /// B-version of the numbers (usually the new one) - /// Returns a array of Items that describe the differences. - public static Item[] DiffInt(int[] arrayA, int[] arrayB) - { - // The A-Version of the data (original data) to be compared. - var dataA = new DiffData(arrayA); - - // The B-Version of the data (modified data) to be compared. - var dataB = new DiffData(arrayB); - - var max = dataA.Length + dataB.Length + 1; - // vector for the (0,0) to (x,y) search - var downVector = new int[2 * max + 2]; - // vector for the (u,v) to (N,M) search - var upVector = new int[2 * max + 2]; - - Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); - return CreateDiffs(dataA, dataB); - } // Diff - - - /// - /// This function converts all text lines of the text into unique numbers for every unique text line - /// so further work can work only with simple numbers. - /// - /// the input text - /// This extern initialized Hashtable is used for storing all ever used text lines. - /// ignore leading and trailing space characters - /// - /// - /// a array of integers. - private static int[] DiffCodes(string aText, IDictionary h, bool trimSpace, bool ignoreSpace, bool ignoreCase) - { - // get all codes of the text - var lastUsedCode = h.Count; - - // strip off all cr, only use lf as text line separator. - aText = aText.Replace("\r", ""); - var lines = aText.Split(Constants.CharArrays.LineFeed); - - var codes = new int[lines.Length]; - - for (int i = 0; i < lines.Length; ++i) - { - string s = lines[i]; - if (trimSpace) - s = s.Trim(); - - if (ignoreSpace) - { - s = Regex.Replace(s, "\\s+", " "); // TODO: optimization: faster blank removal. - } - - if (ignoreCase) - s = s.ToLower(); - - object? aCode = h[s]; - if (aCode == null) - { - lastUsedCode++; - h[s] = lastUsedCode; - codes[i] = lastUsedCode; - } - else - { - codes[i] = (int)aCode; - } // if - } // for - return (codes); - } // DiffCodes - - - /// - /// This is the algorithm to find the Shortest Middle Snake (SMS). - /// - /// sequence A - /// lower bound of the actual range in DataA - /// upper bound of the actual range in DataA (exclusive) - /// sequence B - /// lower bound of the actual range in DataB - /// upper bound of the actual range in DataB (exclusive) - /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. - /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. - /// a MiddleSnakeData record containing x,y and u,v - private static Smsrd Sms(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) - { - int max = dataA.Length + dataB.Length + 1; - - int downK = lowerA - lowerB; // the k-line to start the forward search - int upK = upperA - upperB; // the k-line to start the reverse search - - int delta = (upperA - lowerA) - (upperB - lowerB); - bool oddDelta = (delta & 1) != 0; - - // The vectors in the publication accepts negative indexes. the vectors implemented here are 0-based - // and are access using a specific offset: UpOffset UpVector and DownOffset for DownVektor - int downOffset = max - downK; - int upOffset = max - upK; - - int maxD = ((upperA - lowerA + upperB - lowerB) / 2) + 1; - - // Debug.Write(2, "SMS", String.Format("Search the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); - - // init vectors - downVector[downOffset + downK + 1] = lowerA; - upVector[upOffset + upK - 1] = upperA; - - for (int d = 0; d <= maxD; d++) - { - - // Extend the forward path. - Smsrd ret; - for (int k = downK - d; k <= downK + d; k += 2) - { - // Debug.Write(0, "SMS", "extend forward path " + k.ToString()); - - // find the only or better starting point - int x, y; - if (k == downK - d) - { - x = downVector[downOffset + k + 1]; // down - } - else - { - x = downVector[downOffset + k - 1] + 1; // a step to the right - if ((k < downK + d) && (downVector[downOffset + k + 1] >= x)) - x = downVector[downOffset + k + 1]; // down - } - y = x - k; - - // find the end of the furthest reaching forward D-path in diagonal k. - while ((x < upperA) && (y < upperB) && (dataA.Data[x] == dataB.Data[y])) - { - x++; y++; - } - downVector[downOffset + k] = x; - - // overlap ? - if (oddDelta && (upK - d < k) && (k < upK + d)) - { - if (upVector[upOffset + k] <= downVector[downOffset + k]) - { - ret.X = downVector[downOffset + k]; - ret.Y = downVector[downOffset + k] - k; - // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points - // ret.v = UpVector[UpOffset + k] - k; - return (ret); - } // if - } // if - - } // for k - - // Extend the reverse path. - for (int k = upK - d; k <= upK + d; k += 2) - { - // Debug.Write(0, "SMS", "extend reverse path " + k.ToString()); - - // find the only or better starting point - int x, y; - if (k == upK + d) - { - x = upVector[upOffset + k - 1]; // up - } - else - { - x = upVector[upOffset + k + 1] - 1; // left - if ((k > upK - d) && (upVector[upOffset + k - 1] < x)) - x = upVector[upOffset + k - 1]; // up - } // if - y = x - k; - - while ((x > lowerA) && (y > lowerB) && (dataA.Data[x - 1] == dataB.Data[y - 1])) - { - x--; y--; // diagonal - } - upVector[upOffset + k] = x; - - // overlap ? - if (!oddDelta && (downK - d <= k) && (k <= downK + d)) - { - if (upVector[upOffset + k] <= downVector[downOffset + k]) - { - ret.X = downVector[downOffset + k]; - ret.Y = downVector[downOffset + k] - k; - // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points - // ret.v = UpVector[UpOffset + k] - k; - return (ret); - } // if - } // if - - } // for k - - } // for D - - throw new ApplicationException("the algorithm should never come here."); - } // SMS - - - /// - /// This is the divide-and-conquer implementation of the longest common-subsequence (LCS) - /// algorithm. - /// The published algorithm passes recursively parts of the A and B sequences. - /// To avoid copying these arrays the lower and upper bounds are passed while the sequences stay constant. - /// - /// sequence A - /// lower bound of the actual range in DataA - /// upper bound of the actual range in DataA (exclusive) - /// sequence B - /// lower bound of the actual range in DataB - /// upper bound of the actual range in DataB (exclusive) - /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. - /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. - private static void Lcs(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) - { - // Debug.Write(2, "LCS", String.Format("Analyze the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); - - // Fast walk through equal lines at the start - while (lowerA < upperA && lowerB < upperB && dataA.Data[lowerA] == dataB.Data[lowerB]) - { - lowerA++; lowerB++; + startPos++; } - // Fast walk through equal lines at the end - while (lowerA < upperA && lowerB < upperB && dataA.Data[upperA - 1] == dataB.Data[upperB - 1]) + var endPos = startPos; + while (endPos < data.Length && data.Modified[endPos]) { - --upperA; --upperB; + endPos++; } - if (lowerA == upperA) + if (endPos < data.Length && data.Data[startPos] == data.Data[endPos]) { - // mark as inserted lines. - while (lowerB < upperB) - dataB.Modified[lowerB++] = true; - - } - else if (lowerB == upperB) - { - // mark as deleted lines. - while (lowerA < upperA) - dataA.Modified[lowerA++] = true; - + data.Modified[startPos] = false; + data.Modified[endPos] = true; } else { - // Find the middle snake and length of an optimal path for A and B - Smsrd smsrd = Sms(dataA, lowerA, upperA, dataB, lowerB, upperB, downVector, upVector); - // Debug.Write(2, "MiddleSnakeData", String.Format("{0},{1}", smsrd.x, smsrd.y)); + startPos = endPos; + } // if + } // while + } // Optimize - // The path is from LowerX to (x,y) and (x,y) to UpperX - Lcs(dataA, lowerA, smsrd.X, dataB, lowerB, smsrd.Y, downVector, upVector); - Lcs(dataA, smsrd.X, upperA, dataB, smsrd.Y, upperB, downVector, upVector); // 2002.09.20: no need for 2 points - } - } // LCS() + /// + /// This function converts all text lines of the text into unique numbers for every unique text line + /// so further work can work only with simple numbers. + /// + /// the input text + /// This extern initialized Hashtable is used for storing all ever used text lines. + /// ignore leading and trailing space characters + /// + /// + /// a array of integers. + private static int[] DiffCodes(string aText, IDictionary h, bool trimSpace, bool ignoreSpace, bool ignoreCase) + { + // get all codes of the text + var lastUsedCode = h.Count; + // strip off all cr, only use lf as text line separator. + aText = aText.Replace("\r", string.Empty); + var lines = aText.Split(Constants.CharArrays.LineFeed); - /// Scan the tables of which lines are inserted and deleted, - /// producing an edit script in forward order. - /// - /// dynamic array - private static Item[] CreateDiffs(DiffData dataA, DiffData dataB) + var codes = new int[lines.Length]; + + for (var i = 0; i < lines.Length; ++i) { - ArrayList a = new ArrayList(); - Item aItem; - Item[] result; - - int lineA = 0; - int lineB = 0; - while (lineA < dataA.Length || lineB < dataB.Length) + var s = lines[i]; + if (trimSpace) { - if ((lineA < dataA.Length) && (!dataA.Modified[lineA]) - && (lineB < dataB.Length) && (!dataB.Modified[lineB])) - { - // equal lines - lineA++; - lineB++; + s = s.Trim(); + } + if (ignoreSpace) + { + s = Regex.Replace(s, "\\s+", " "); // TODO: optimization: faster blank removal. + } + + if (ignoreCase) + { + s = s.ToLower(); + } + + var aCode = h[s]; + if (aCode == null) + { + lastUsedCode++; + h[s] = lastUsedCode; + codes[i] = lastUsedCode; + } + else + { + codes[i] = (int)aCode; + } // if + } // for + + return codes; + } // DiffCodes + + /// + /// This is the algorithm to find the Shortest Middle Snake (SMS). + /// + /// sequence A + /// lower bound of the actual range in DataA + /// upper bound of the actual range in DataA (exclusive) + /// sequence B + /// lower bound of the actual range in DataB + /// upper bound of the actual range in DataB (exclusive) + /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. + /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. + /// a MiddleSnakeData record containing x,y and u,v + private static Smsrd Sms(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) + { + var max = dataA.Length + dataB.Length + 1; + + var downK = lowerA - lowerB; // the k-line to start the forward search + var upK = upperA - upperB; // the k-line to start the reverse search + + var delta = upperA - lowerA - (upperB - lowerB); + var oddDelta = (delta & 1) != 0; + + // The vectors in the publication accepts negative indexes. the vectors implemented here are 0-based + // and are access using a specific offset: UpOffset UpVector and DownOffset for DownVektor + var downOffset = max - downK; + var upOffset = max - upK; + + var maxD = ((upperA - lowerA + upperB - lowerB) / 2) + 1; + + // Debug.Write(2, "SMS", String.Format("Search the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); + + // init vectors + downVector[downOffset + downK + 1] = lowerA; + upVector[upOffset + upK - 1] = upperA; + + for (var d = 0; d <= maxD; d++) + { + // Extend the forward path. + Smsrd ret; + for (var k = downK - d; k <= downK + d; k += 2) + { + // Debug.Write(0, "SMS", "extend forward path " + k.ToString()); + + // find the only or better starting point + int x, y; + if (k == downK - d) + { + x = downVector[downOffset + k + 1]; // down } else { - // maybe deleted and/or inserted lines - int startA = lineA; - int startB = lineB; - - while (lineA < dataA.Length && (lineB >= dataB.Length || dataA.Modified[lineA])) - // while (LineA < DataA.Length && DataA.modified[LineA]) - lineA++; - - while (lineB < dataB.Length && (lineA >= dataA.Length || dataB.Modified[lineB])) - // while (LineB < DataB.Length && DataB.modified[LineB]) - lineB++; - - if ((startA < lineA) || (startB < lineB)) + x = downVector[downOffset + k - 1] + 1; // a step to the right + if (k < downK + d && downVector[downOffset + k + 1] >= x) { - // store a new difference-item - aItem = new Item(); - aItem.StartA = startA; - aItem.StartB = startB; - aItem.DeletedA = lineA - startA; - aItem.InsertedB = lineB - startB; - a.Add(aItem); + x = downVector[downOffset + k + 1]; // down + } + } + + y = x - k; + + // find the end of the furthest reaching forward D-path in diagonal k. + while (x < upperA && y < upperB && dataA.Data[x] == dataB.Data[y]) + { + x++; + y++; + } + + downVector[downOffset + k] = x; + + // overlap ? + if (oddDelta && upK - d < k && k < upK + d) + { + if (upVector[upOffset + k] <= downVector[downOffset + k]) + { + ret.X = downVector[downOffset + k]; + ret.Y = downVector[downOffset + k] - k; + + // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points + // ret.v = UpVector[UpOffset + k] - k; + return ret; } // if } // if - } // while + } // for k - result = new Item[a.Count]; - a.CopyTo(result); + // Extend the reverse path. + for (var k = upK - d; k <= upK + d; k += 2) + { + // Debug.Write(0, "SMS", "extend reverse path " + k.ToString()); - return (result); + // find the only or better starting point + int x, y; + if (k == upK + d) + { + x = upVector[upOffset + k - 1]; // up + } + else + { + x = upVector[upOffset + k + 1] - 1; // left + if (k > upK - d && upVector[upOffset + k - 1] < x) + { + x = upVector[upOffset + k - 1]; // up + } + } // if + + y = x - k; + + while (x > lowerA && y > lowerB && dataA.Data[x - 1] == dataB.Data[y - 1]) + { + x--; + y--; // diagonal + } + + upVector[upOffset + k] = x; + + // overlap ? + if (!oddDelta && downK - d <= k && k <= downK + d) + { + if (upVector[upOffset + k] <= downVector[downOffset + k]) + { + ret.X = downVector[downOffset + k]; + ret.Y = downVector[downOffset + k] - k; + + // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points + // ret.v = UpVector[UpOffset + k] - k; + return ret; + } // if + } // if + } // for k + } // for D + + throw new ApplicationException("the algorithm should never come here."); + } // SMS + + /// + /// This is the divide-and-conquer implementation of the longest common-subsequence (LCS) + /// algorithm. + /// The published algorithm passes recursively parts of the A and B sequences. + /// To avoid copying these arrays the lower and upper bounds are passed while the sequences stay constant. + /// + /// sequence A + /// lower bound of the actual range in DataA + /// upper bound of the actual range in DataA (exclusive) + /// sequence B + /// lower bound of the actual range in DataB + /// upper bound of the actual range in DataB (exclusive) + /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. + /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. + private static void Lcs(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) + { + // Debug.Write(2, "LCS", String.Format("Analyze the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); + + // Fast walk through equal lines at the start + while (lowerA < upperA && lowerB < upperB && dataA.Data[lowerA] == dataB.Data[lowerB]) + { + lowerA++; + lowerB++; } - } // class Diff + // Fast walk through equal lines at the end + while (lowerA < upperA && lowerB < upperB && dataA.Data[upperA - 1] == dataB.Data[upperB - 1]) + { + --upperA; + --upperB; + } + if (lowerA == upperA) + { + // mark as inserted lines. + while (lowerB < upperB) + { + dataB.Modified[lowerB++] = true; + } + } + else if (lowerB == upperB) + { + // mark as deleted lines. + while (lowerA < upperA) + { + dataA.Modified[lowerA++] = true; + } + } + else + { + // Find the middle snake and length of an optimal path for A and B + Smsrd smsrd = Sms(dataA, lowerA, upperA, dataB, lowerB, upperB, downVector, upVector); -} + // Debug.Write(2, "MiddleSnakeData", String.Format("{0},{1}", smsrd.x, smsrd.y)); + + // The path is from LowerX to (x,y) and (x,y) to UpperX + Lcs(dataA, lowerA, smsrd.X, dataB, lowerB, smsrd.Y, downVector, upVector); + Lcs(dataA, smsrd.X, upperA, dataB, smsrd.Y, upperB, downVector, upVector); // 2002.09.20: no need for 2 points + } + } // LCS() + + /// + /// Scan the tables of which lines are inserted and deleted, + /// producing an edit script in forward order. + /// + /// dynamic array + private static Item[] CreateDiffs(DiffData dataA, DiffData dataB) + { + var a = new ArrayList(); + Item aItem; + Item[] result; + + var lineA = 0; + var lineB = 0; + while (lineA < dataA.Length || lineB < dataB.Length) + { + if (lineA < dataA.Length && !dataA.Modified[lineA] + && lineB < dataB.Length && !dataB.Modified[lineB]) + { + // equal lines + lineA++; + lineB++; + } + else + { + // maybe deleted and/or inserted lines + var startA = lineA; + var startB = lineB; + + while (lineA < dataA.Length && (lineB >= dataB.Length || dataA.Modified[lineA])) + + // while (LineA < DataA.Length && DataA.modified[LineA]) + { + lineA++; + } + + while (lineB < dataB.Length && (lineA >= dataA.Length || dataB.Modified[lineB])) + + // while (LineB < DataB.Length && DataB.modified[LineB]) + { + lineB++; + } + + if (startA < lineA || startB < lineB) + { + // store a new difference-item + aItem = new Item + { + StartA = startA, + StartB = startB, + DeletedA = lineA - startA, + InsertedB = lineB - startB, + }; + a.Add(aItem); + } // if + } // if + } // while + + result = new Item[a.Count]; + a.CopyTo(result); + + return result; + } + + /// details of one difference. + public struct Item + { + /// Start Line number in Data A. + public int StartA; + + /// Start Line number in Data B. + public int StartB; + + /// Number of changes in Data A. + public int DeletedA; + + /// Number of changes in Data B. + public int InsertedB; + } // Item + + /// + /// Data on one input file being compared. + /// + internal class DiffData + { + /// Buffer of numbers that will be compared. + internal int[] Data; + + /// Number of elements (lines). + internal int Length; + + /// + /// Array of booleans that flag for modified data. + /// This is the result of the diff. + /// This means deletedA in the first Data or inserted in the second Data. + /// + internal bool[] Modified; + + /// + /// Initialize the Diff-Data buffer. + /// + /// reference to the buffer + internal DiffData(int[] initData) + { + Data = initData; + Length = initData.Length; + Modified = new bool[Length + 2]; + } // DiffData + } // class DiffData + + /// + /// Shortest Middle Snake Return Data + /// + private struct Smsrd + { + internal int X; + internal int Y; + + // internal int u, v; // 2002.09.20: no need for 2 points + } +} // class Diff diff --git a/src/Umbraco.Core/Strings/HtmlEncodedString.cs b/src/Umbraco.Core/Strings/HtmlEncodedString.cs index 16941cef48..4477d5436c 100644 --- a/src/Umbraco.Core/Strings/HtmlEncodedString.cs +++ b/src/Umbraco.Core/Strings/HtmlEncodedString.cs @@ -1,33 +1,21 @@ -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Represents an HTML-encoded string that should not be encoded again. +/// +public class HtmlEncodedString : IHtmlEncodedString { - /// - /// Represents an HTML-encoded string that should not be encoded again. - /// - public class HtmlEncodedString : IHtmlEncodedString - { + private readonly string _htmlString; - private string _htmlString; + /// Initializes a new instance of the class. + /// An HTML-encoded string that should not be encoded again. + public HtmlEncodedString(string value) => _htmlString = value; - /// Initializes a new instance of the class. - /// An HTML-encoded string that should not be encoded again. - public HtmlEncodedString(string value) - { - this._htmlString = value; - } + /// Returns an HTML-encoded string. + /// An HTML-encoded string. + public string ToHtmlString() => _htmlString; - /// Returns an HTML-encoded string. - /// An HTML-encoded string. - public string ToHtmlString() - { - return this._htmlString; - } - - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - { - return this._htmlString; - } - - } + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => _htmlString; } diff --git a/src/Umbraco.Core/Strings/IHtmlEncodedString.cs b/src/Umbraco.Core/Strings/IHtmlEncodedString.cs index b7c0c27d2d..bf94f834ad 100644 --- a/src/Umbraco.Core/Strings/IHtmlEncodedString.cs +++ b/src/Umbraco.Core/Strings/IHtmlEncodedString.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Represents an HTML-encoded string that should not be encoded again. +/// +public interface IHtmlEncodedString { /// - /// Represents an HTML-encoded string that should not be encoded again. + /// Returns an HTML-encoded string. /// - public interface IHtmlEncodedString - { - /// - /// Returns an HTML-encoded string. - /// - /// An HTML-encoded string. - string? ToHtmlString(); - } + /// An HTML-encoded string. + string? ToHtmlString(); } diff --git a/src/Umbraco.Core/Strings/IShortStringHelper.cs b/src/Umbraco.Core/Strings/IShortStringHelper.cs index a436758d9a..a5c20f1a09 100644 --- a/src/Umbraco.Core/Strings/IShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/IShortStringHelper.cs @@ -1,114 +1,129 @@ -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Provides string functions for short strings such as aliases or URL segments. +/// +/// Not necessarily optimized to work on large bodies of text. +public interface IShortStringHelper { /// - /// Provides string functions for short strings such as aliases or URL segments. + /// Cleans a string to produce a string that can safely be used in an alias. /// - /// Not necessarily optimized to work on large bodies of text. - public interface IShortStringHelper - { - /// - /// Cleans a string to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The safe alias. - /// - /// The string will be cleaned in the context of the IShortStringHelper default culture. - /// A safe alias is [a-z][a-zA-Z0-9_]* although legacy will also accept '-', and '_' at the beginning. - /// - string CleanStringForSafeAlias(string text); + /// The text to filter. + /// The safe alias. + /// + /// The string will be cleaned in the context of the IShortStringHelper default culture. + /// A safe alias is [a-z][a-zA-Z0-9_]* although legacy will also accept '-', and '_' at the beginning. + /// + string CleanStringForSafeAlias(string text); - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The culture. - /// The safe alias. - string CleanStringForSafeAlias(string text, string culture); + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias. + /// + /// The text to filter. + /// The culture. + /// The safe alias. + string CleanStringForSafeAlias(string text, string culture); - /// - /// Cleans a string to produce a string that can safely be used in an URL segment. - /// - /// The text to filter. - /// The safe URL segment. - /// The string will be cleaned in the context of the IShortStringHelper default culture. - string CleanStringForUrlSegment(string text); + /// + /// Cleans a string to produce a string that can safely be used in an URL segment. + /// + /// The text to filter. + /// The safe URL segment. + /// The string will be cleaned in the context of the IShortStringHelper default culture. + string CleanStringForUrlSegment(string text); - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an URL segment. - /// - /// The text to filter. - /// The culture. - /// The safe URL segment. - string CleanStringForUrlSegment(string text, string? culture); + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an URL + /// segment. + /// + /// The text to filter. + /// The culture. + /// The safe URL segment. + string CleanStringForUrlSegment(string text, string? culture); - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a URL). - /// - /// The text to filter. - /// The safe filename. - /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but that issue is not documented. - string CleanStringForSafeFileName(string text); + /// + /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a + /// filename, + /// both internally (on disk) and externally (as a URL). + /// + /// The text to filter. + /// The safe filename. + /// + /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but + /// that issue is not documented. + /// + string CleanStringForSafeFileName(string text); - /// - /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a URL). - /// - /// The text to filter. - /// The culture. - /// The safe filename. - /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but that issue is not documented. - string CleanStringForSafeFileName(string text, string culture); + /// + /// Cleans a string, in the context of a specified culture, to produce a string that can safely be used as a filename, + /// both internally (on disk) and externally (as a URL). + /// + /// The text to filter. + /// The culture. + /// The safe filename. + /// + /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but + /// that issue is not documented. + /// + string CleanStringForSafeFileName(string text, string culture); - /// - /// Splits a pascal-cased string by inserting a separator in between each term. - /// - /// The text to split. - /// The separator. - /// The split string. - /// Supports Utf8 and Ascii strings, not Unicode strings. - string SplitPascalCasing(string text, char separator); + /// + /// Splits a pascal-cased string by inserting a separator in between each term. + /// + /// The text to split. + /// The separator. + /// The split string. + /// Supports Utf8 and Ascii strings, not Unicode strings. + string SplitPascalCasing(string text, char separator); - /// - /// Cleans a string. - /// - /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The clean string. - /// The string is cleaned in the context of the IShortStringHelper default culture. - string CleanString(string text, CleanStringType stringType); + /// + /// Cleans a string. + /// + /// The text to clean. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The clean string. + /// The string is cleaned in the context of the IShortStringHelper default culture. + string CleanString(string text, CleanStringType stringType); - /// - /// Cleans a string, using a specified separator. - /// - /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The separator. - /// The clean string. - /// The string is cleaned in the context of the IShortStringHelper default culture. - string CleanString(string text, CleanStringType stringType, char separator); + /// + /// Cleans a string, using a specified separator. + /// + /// The text to clean. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The separator. + /// The clean string. + /// The string is cleaned in the context of the IShortStringHelper default culture. + string CleanString(string text, CleanStringType stringType, char separator); - /// - /// Cleans a string in the context of a specified culture. - /// - /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The culture. - /// The clean string. - string CleanString(string text, CleanStringType stringType, string culture); + /// + /// Cleans a string in the context of a specified culture. + /// + /// The text to clean. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The culture. + /// The clean string. + string CleanString(string text, CleanStringType stringType, string culture); - /// - /// Cleans a string in the context of a specified culture, using a specified separator. - /// - /// The text to clean. - /// A flag indicating the target casing and encoding of the string. By default, - /// strings are cleaned up to camelCase and Ascii. - /// The separator. - /// The culture. - /// The clean string. - string CleanString(string text, CleanStringType stringType, char separator, string culture); - } + /// + /// Cleans a string in the context of a specified culture, using a specified separator. + /// + /// The text to clean. + /// + /// A flag indicating the target casing and encoding of the string. By default, + /// strings are cleaned up to camelCase and Ascii. + /// + /// The separator. + /// The culture. + /// The clean string. + string CleanString(string text, CleanStringType stringType, char separator, string culture); } diff --git a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs index 74d147173f..c7050050e1 100644 --- a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs @@ -1,26 +1,27 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +/// +/// Provides URL segments for content. +/// +/// Url segments should comply with IETF RFCs regarding content, encoding, etc. +public interface IUrlSegmentProvider { /// - /// Provides URL segments for content. + /// Gets the URL segment for a specified content and culture. /// - /// Url segments should comply with IETF RFCs regarding content, encoding, etc. - public interface IUrlSegmentProvider - { - /// - /// Gets the URL segment for a specified content and culture. - /// - /// The content. - /// The culture. - /// The URL segment. - /// This is for when Umbraco is capable of managing more than one URL - /// per content, in 1-to-1 multilingual configurations. Then there would be one - /// URL per culture. - string? GetUrlSegment(IContentBase content, string? culture = null); + /// The content. + /// The culture. + /// The URL segment. + /// + /// This is for when Umbraco is capable of managing more than one URL + /// per content, in 1-to-1 multilingual configurations. Then there would be one + /// URL per culture. + /// + string? GetUrlSegment(IContentBase content, string? culture = null); - // TODO: For the 301 tracking, we need to add another extended interface to this so that - // the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing. - // Currently the way it works is very hacky, see notes in: RedirectTrackingEventHandler.ContentService_Publishing - } + // TODO: For the 301 tracking, we need to add another extended interface to this so that + // the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing. + // Currently the way it works is very hacky, see notes in: RedirectTrackingEventHandler.ContentService_Publishing } diff --git a/src/Umbraco.Core/Strings/PathUtility.cs b/src/Umbraco.Core/Strings/PathUtility.cs index bc88fa8bca..cab7127a6e 100644 --- a/src/Umbraco.Core/Strings/PathUtility.cs +++ b/src/Umbraco.Core/Strings/PathUtility.cs @@ -1,22 +1,29 @@ -namespace Umbraco.Cms.Core.Strings -{ - public static class PathUtility - { +namespace Umbraco.Cms.Core.Strings; - /// - /// Ensures that a path has `~/` as prefix - /// - /// - /// - public static string EnsurePathIsApplicationRootPrefixed(string path) +public static class PathUtility +{ + /// + /// Ensures that a path has `~/` as prefix + /// + /// + /// + public static string EnsurePathIsApplicationRootPrefixed(string path) + { + if (path.StartsWith("~/")) { - if (path.StartsWith("~/")) - return path; - if (path.StartsWith("/") == false && path.StartsWith("\\") == false) - path = string.Format("/{0}", path); - if (path.StartsWith("~") == false) - path = string.Format("~{0}", path); return path; } + + if (path.StartsWith("/") == false && path.StartsWith("\\") == false) + { + path = string.Format("/{0}", path); + } + + if (path.StartsWith("~") == false) + { + path = string.Format("~{0}", path); + } + + return path; } } diff --git a/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs b/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs index 551efc475a..39b826dae9 100644 --- a/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs +++ b/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +public class UrlSegmentProviderCollection : BuilderCollectionBase { - public class UrlSegmentProviderCollection : BuilderCollectionBase + public UrlSegmentProviderCollection(Func> items) + : base(items) { - public UrlSegmentProviderCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs b/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs index 60504734f6..f9aa13b335 100644 --- a/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs +++ b/src/Umbraco.Core/Strings/UrlSegmentProviderCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Strings +namespace Umbraco.Cms.Core.Strings; + +public class UrlSegmentProviderCollectionBuilder : OrderedCollectionBuilderBase { - public class UrlSegmentProviderCollectionBuilder : OrderedCollectionBuilderBase - { - protected override UrlSegmentProviderCollectionBuilder This => this; - } + protected override UrlSegmentProviderCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs b/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs index 3f492a7b87..4221273150 100644 --- a/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs +++ b/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs @@ -1,3627 +1,3624 @@ -using System; +namespace Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Strings +/// +/// Provides methods to convert Utf8 text to Ascii. +/// +/// +/// Tries to match characters such as accented eg "é" to Ascii equivalent eg "e". +/// Converts all "whitespace" characters to a single whitespace. +/// Removes all non-Utf8 (unicode) characters, so in fact it can sort-of "convert" Unicode to Ascii. +/// Replaces symbols with '?'. +/// +public static class Utf8ToAsciiConverter { /// - /// Provides methods to convert Utf8 text to Ascii. + /// Converts an Utf8 string into an Ascii string. /// - /// - /// Tries to match characters such as accented eg "é" to Ascii equivalent eg "e". - /// Converts all "whitespace" characters to a single whitespace. - /// Removes all non-Utf8 (unicode) characters, so in fact it can sort-of "convert" Unicode to Ascii. - /// Replaces symbols with '?'. - /// - public static class Utf8ToAsciiConverter + /// The text to convert. + /// The character to use to replace characters that cannot properly be converted. + /// The converted text. + public static string ToAsciiString(string text, char fail = '?') { - /// - /// Converts an Utf8 string into an Ascii string. - /// - /// The text to convert. - /// The character to use to replace characters that cannot properly be converted. - /// The converted text. - public static string ToAsciiString(string text, char fail = '?') + var input = text.ToCharArray(); + + // this is faster although it uses more memory + // but... we should be filtering short strings only... + var output = new char[input.Length * 3]; // *3 because of things such as OE + var len = ToAscii(input, output, fail); + return new string(output, 0, len); + + // var output = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra + // ToAscii(input, output); + // return output.ToString(); + } + + /// + /// Converts an Utf8 string into an array of Ascii characters. + /// + /// The text to convert. + /// The character to use to replace characters that cannot properly be converted. + /// The converted text. + public static char[] ToAsciiCharArray(string text, char fail = '?') + { + var input = text.ToCharArray(); + + // this is faster although it uses more memory + // but... we should be filtering short strings only... + var output = new char[input.Length * 3]; // *3 because of things such as OE + var len = ToAscii(input, output, fail); + var array = new char[len]; + Array.Copy(output, array, len); + return array; + + // var temp = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra + // ToAscii(input, temp); + // var output = new char[temp.Length]; + // temp.CopyTo(0, output, 0, temp.Length); + // return output; + } + + /// + /// Converts an array of Utf8 characters into an array of Ascii characters. + /// + /// The input array. + /// The output array. + /// The character to use to replace characters that cannot properly be converted. + /// The number of characters in the output array. + /// The caller must ensure that the output array is big enough. + /// The output array is not big enough. + private static int ToAscii(char[] input, char[] output, char fail = '?') + { + var opos = 0; + + for (var ipos = 0; ipos < input.Length; ipos++) { - var input = text.ToCharArray(); - - // this is faster although it uses more memory - // but... we should be filtering short strings only... - - var output = new char[input.Length * 3]; // *3 because of things such as OE - var len = ToAscii(input, output, fail); - return new string(output, 0, len); - - //var output = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra - //ToAscii(input, output); - //return output.ToString(); - } - - /// - /// Converts an Utf8 string into an array of Ascii characters. - /// - /// The text to convert. - /// The character to use to replace characters that cannot properly be converted. - /// The converted text. - public static char[] ToAsciiCharArray(string text, char fail = '?') - { - var input = text.ToCharArray(); - - // this is faster although it uses more memory - // but... we should be filtering short strings only... - - var output = new char[input.Length * 3]; // *3 because of things such as OE - var len = ToAscii(input, output, fail); - var array = new char[len]; - Array.Copy(output, array, len); - return array; - - //var temp = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra - //ToAscii(input, temp); - //var output = new char[temp.Length]; - //temp.CopyTo(0, output, 0, temp.Length); - //return output; - } - - /// - /// Converts an array of Utf8 characters into an array of Ascii characters. - /// - /// The input array. - /// The output array. - /// The character to use to replace characters that cannot properly be converted. - /// The number of characters in the output array. - /// The caller must ensure that the output array is big enough. - /// The output array is not big enough. - private static int ToAscii(char[] input, char[] output, char fail = '?') - { - var opos = 0; - - for (var ipos = 0; ipos < input.Length; ipos++) - if (char.IsSurrogate(input[ipos])) // ignore high surrogate - { - ipos++; // and skip low surrogate - output[opos++] = fail; - } - else - ToAscii(input, ipos, output, ref opos, fail); - - return opos; - } - - //private static void ToAscii(char[] input, StringBuilder output) - //{ - // var chars = new char[5]; - - // for (var ipos = 0; ipos < input.Length; ipos++) - // { - // var opos = 0; - // if (char.IsSurrogate(input[ipos])) - // ipos++; - // else - // { - // ToAscii(input, ipos, chars, ref opos); - // output.Append(chars, 0, opos); - // } - // } - //} - - /// - /// Converts the character at position in input array of Utf8 characters - /// and writes the converted value to output array of Ascii characters at position , - /// and increments that position accordingly. - /// - /// The input array. - /// The input position. - /// The output array. - /// The output position. - /// The character to use to replace characters that cannot properly be converted. - /// - /// Adapted from various sources on the 'net including Lucene.Net.Analysis.ASCIIFoldingFilter. - /// Input should contain Utf8 characters exclusively and NOT Unicode. - /// Removes controls, normalizes whitespaces, replaces symbols by '?'. - /// - private static void ToAscii(char[] input, int ipos, char[] output, ref int opos, char fail = '?') - { - var c = input[ipos]; - - if (char.IsControl(c)) + // ignore high surrogate + if (char.IsSurrogate(input[ipos])) { - // Control characters are non-printing and formatting characters, such as ACK, BEL, CR, FF, LF, and VT. - // The Unicode standard assigns the following code points to control characters: from \U0000 to \U001F, - // \U007F, and from \U0080 to \U009F. According to the Unicode standard, these values are to be - // interpreted as control characters unless their use is otherwise defined by an application. Valid - // control characters are members of the UnicodeCategory.Control category. - - // we don't want them - } - //else if (char.IsSeparator(c)) - //{ - // // The Unicode standard recognizes three subcategories of separators: - // // - Space separators (the UnicodeCategory.SpaceSeparator category), which includes characters such as \u0020. - // // - Line separators (the UnicodeCategory.LineSeparator category), which includes \u2028. - // // - Paragraph separators (the UnicodeCategory.ParagraphSeparator category), which includes \u2029. - // // - // // Note: The Unicode standard classifies the characters \u000A (LF), \u000C (FF), and \u000A (CR) as control - // // characters (members of the UnicodeCategory.Control category), not as separator characters. - - // // better do it via WhiteSpace - //} - else if (char.IsWhiteSpace(c)) - { - // White space characters are the following Unicode characters: - // - Members of the SpaceSeparator category, which includes the characters SPACE (U+0020), - // OGHAM SPACE MARK (U+1680), MONGOLIAN VOWEL SEPARATOR (U+180E), EN QUAD (U+2000), EM QUAD (U+2001), - // EN SPACE (U+2002), EM SPACE (U+2003), THREE-PER-EM SPACE (U+2004), FOUR-PER-EM SPACE (U+2005), - // SIX-PER-EM SPACE (U+2006), FIGURE SPACE (U+2007), PUNCTUATION SPACE (U+2008), THIN SPACE (U+2009), - // HAIR SPACE (U+200A), NARROW NO-BREAK SPACE (U+202F), MEDIUM MATHEMATICAL SPACE (U+205F), - // and IDEOGRAPHIC SPACE (U+3000). - // - Members of the LineSeparator category, which consists solely of the LINE SEPARATOR character (U+2028). - // - Members of the ParagraphSeparator category, which consists solely of the PARAGRAPH SEPARATOR character (U+2029). - // - The characters CHARACTER TABULATION (U+0009), LINE FEED (U+000A), LINE TABULATION (U+000B), - // FORM FEED (U+000C), CARRIAGE RETURN (U+000D), NEXT LINE (U+0085), and NO-BREAK SPACE (U+00A0). - - // make it a whitespace - output[opos++] = ' '; - } - else if (c < '\u0080') - { - // safe - output[opos++] = c; + ipos++; // and skip low surrogate + output[opos++] = fail; } else { - switch (c) - { - - case '\u00C0': - // À [LATIN CAPITAL LETTER A WITH GRAVE] - case '\u00C1': - // � [LATIN CAPITAL LETTER A WITH ACUTE] - case '\u00C2': - //  [LATIN CAPITAL LETTER A WITH CIRCUMFLEX] - case '\u00C3': - // à [LATIN CAPITAL LETTER A WITH TILDE] - case '\u00C4': - // Ä [LATIN CAPITAL LETTER A WITH DIAERESIS] - case '\u00C5': - // Ã… [LATIN CAPITAL LETTER A WITH RING ABOVE] - case '\u0100': - // Ä€ [LATIN CAPITAL LETTER A WITH MACRON] - case '\u0102': - // Ä‚ [LATIN CAPITAL LETTER A WITH BREVE] - case '\u0104': - // Ä„ [LATIN CAPITAL LETTER A WITH OGONEK] - case '\u018F': - // � http://en.wikipedia.org/wiki/Schwa [LATIN CAPITAL LETTER SCHWA] - case '\u01CD': - // � [LATIN CAPITAL LETTER A WITH CARON] - case '\u01DE': - // Çž [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON] - case '\u01E0': - // Ç  [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON] - case '\u01FA': - // Ǻ [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE] - case '\u0200': - // È€ [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE] - case '\u0202': - // È‚ [LATIN CAPITAL LETTER A WITH INVERTED BREVE] - case '\u0226': - // Ȧ [LATIN CAPITAL LETTER A WITH DOT ABOVE] - case '\u023A': - // Ⱥ [LATIN CAPITAL LETTER A WITH STROKE] - case '\u1D00': - // á´€ [LATIN LETTER SMALL CAPITAL A] - case '\u1E00': - // Ḁ [LATIN CAPITAL LETTER A WITH RING BELOW] - case '\u1EA0': - // Ạ [LATIN CAPITAL LETTER A WITH DOT BELOW] - case '\u1EA2': - // Ả [LATIN CAPITAL LETTER A WITH HOOK ABOVE] - case '\u1EA4': - // Ấ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE] - case '\u1EA6': - // Ầ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE] - case '\u1EA8': - // Ẩ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1EAA': - // Ẫ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE] - case '\u1EAC': - // Ậ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW] - case '\u1EAE': - // Ắ [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE] - case '\u1EB0': - // Ằ [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE] - case '\u1EB2': - // Ẳ [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE] - case '\u1EB4': - // Ẵ [LATIN CAPITAL LETTER A WITH BREVE AND TILDE] - case '\u1EB6': - // Ặ [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW] - case '\u24B6': - // â’¶ [CIRCLED LATIN CAPITAL LETTER A] - case '\uFF21': // A [FULLWIDTH LATIN CAPITAL LETTER A] - output[opos++] = 'A'; - break; - - case '\u00E0': - // à [LATIN SMALL LETTER A WITH GRAVE] - case '\u00E1': - // á [LATIN SMALL LETTER A WITH ACUTE] - case '\u00E2': - // â [LATIN SMALL LETTER A WITH CIRCUMFLEX] - case '\u00E3': - // ã [LATIN SMALL LETTER A WITH TILDE] - case '\u00E4': - // ä [LATIN SMALL LETTER A WITH DIAERESIS] - case '\u00E5': - // Ã¥ [LATIN SMALL LETTER A WITH RING ABOVE] - case '\u0101': - // � [LATIN SMALL LETTER A WITH MACRON] - case '\u0103': - // ă [LATIN SMALL LETTER A WITH BREVE] - case '\u0105': - // Ä… [LATIN SMALL LETTER A WITH OGONEK] - case '\u01CE': - // ÇŽ [LATIN SMALL LETTER A WITH CARON] - case '\u01DF': - // ÇŸ [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON] - case '\u01E1': - // Ç¡ [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON] - case '\u01FB': - // Ç» [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE] - case '\u0201': - // � [LATIN SMALL LETTER A WITH DOUBLE GRAVE] - case '\u0203': - // ȃ [LATIN SMALL LETTER A WITH INVERTED BREVE] - case '\u0227': - // ȧ [LATIN SMALL LETTER A WITH DOT ABOVE] - case '\u0250': - // � [LATIN SMALL LETTER TURNED A] - case '\u0259': - // É™ [LATIN SMALL LETTER SCHWA] - case '\u025A': - // Éš [LATIN SMALL LETTER SCHWA WITH HOOK] - case '\u1D8F': - // � [LATIN SMALL LETTER A WITH RETROFLEX HOOK] - case '\u1D95': - // á¶• [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK] - case '\u1E01': - // ạ [LATIN SMALL LETTER A WITH RING BELOW] - case '\u1E9A': - // ả [LATIN SMALL LETTER A WITH RIGHT HALF RING] - case '\u1EA1': - // ạ [LATIN SMALL LETTER A WITH DOT BELOW] - case '\u1EA3': - // ả [LATIN SMALL LETTER A WITH HOOK ABOVE] - case '\u1EA5': - // ấ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE] - case '\u1EA7': - // ầ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE] - case '\u1EA9': - // ẩ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1EAB': - // ẫ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE] - case '\u1EAD': - // ậ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW] - case '\u1EAF': - // ắ [LATIN SMALL LETTER A WITH BREVE AND ACUTE] - case '\u1EB1': - // ằ [LATIN SMALL LETTER A WITH BREVE AND GRAVE] - case '\u1EB3': - // ẳ [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE] - case '\u1EB5': - // ẵ [LATIN SMALL LETTER A WITH BREVE AND TILDE] - case '\u1EB7': - // ặ [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW] - case '\u2090': - // � [LATIN SUBSCRIPT SMALL LETTER A] - case '\u2094': - // �? [LATIN SUBSCRIPT SMALL LETTER SCHWA] - case '\u24D0': - // � [CIRCLED LATIN SMALL LETTER A] - case '\u2C65': - // â±¥ [LATIN SMALL LETTER A WITH STROKE] - case '\u2C6F': - // Ɐ [LATIN CAPITAL LETTER TURNED A] - case '\uFF41': // � [FULLWIDTH LATIN SMALL LETTER A] - output[opos++] = 'a'; - break; - - case '\uA732': // Ꜳ [LATIN CAPITAL LETTER AA] - output[opos++] = 'A'; - output[opos++] = 'A'; - break; - - case '\u00C6': - // Æ [LATIN CAPITAL LETTER AE] - case '\u01E2': - // Ç¢ [LATIN CAPITAL LETTER AE WITH MACRON] - case '\u01FC': - // Ǽ [LATIN CAPITAL LETTER AE WITH ACUTE] - case '\u1D01': // á´� [LATIN LETTER SMALL CAPITAL AE] - output[opos++] = 'A'; - output[opos++] = 'E'; - break; - - case '\uA734': // Ꜵ [LATIN CAPITAL LETTER AO] - output[opos++] = 'A'; - output[opos++] = 'O'; - break; - - case '\uA736': // Ꜷ [LATIN CAPITAL LETTER AU] - output[opos++] = 'A'; - output[opos++] = 'U'; - break; - - case '\uA738': - // Ꜹ [LATIN CAPITAL LETTER AV] - case '\uA73A': // Ꜻ [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR] - output[opos++] = 'A'; - output[opos++] = 'V'; - break; - - case '\uA73C': // Ꜽ [LATIN CAPITAL LETTER AY] - output[opos++] = 'A'; - output[opos++] = 'Y'; - break; - - case '\u249C': // â’œ [PARENTHESIZED LATIN SMALL LETTER A] - output[opos++] = '('; - output[opos++] = 'a'; - output[opos++] = ')'; - break; - - case '\uA733': // ꜳ [LATIN SMALL LETTER AA] - output[opos++] = 'a'; - output[opos++] = 'a'; - break; - - case '\u00E6': - // æ [LATIN SMALL LETTER AE] - case '\u01E3': - // Ç£ [LATIN SMALL LETTER AE WITH MACRON] - case '\u01FD': - // ǽ [LATIN SMALL LETTER AE WITH ACUTE] - case '\u1D02': // á´‚ [LATIN SMALL LETTER TURNED AE] - output[opos++] = 'a'; - output[opos++] = 'e'; - break; - - case '\uA735': // ꜵ [LATIN SMALL LETTER AO] - output[opos++] = 'a'; - output[opos++] = 'o'; - break; - - case '\uA737': // ꜷ [LATIN SMALL LETTER AU] - output[opos++] = 'a'; - output[opos++] = 'u'; - break; - - case '\uA739': - // ꜹ [LATIN SMALL LETTER AV] - case '\uA73B': // ꜻ [LATIN SMALL LETTER AV WITH HORIZONTAL BAR] - output[opos++] = 'a'; - output[opos++] = 'v'; - break; - - case '\uA73D': // ꜽ [LATIN SMALL LETTER AY] - output[opos++] = 'a'; - output[opos++] = 'y'; - break; - - case '\u0181': - // � [LATIN CAPITAL LETTER B WITH HOOK] - case '\u0182': - // Æ‚ [LATIN CAPITAL LETTER B WITH TOPBAR] - case '\u0243': - // Ƀ [LATIN CAPITAL LETTER B WITH STROKE] - case '\u0299': - // Ê™ [LATIN LETTER SMALL CAPITAL B] - case '\u1D03': - // á´ƒ [LATIN LETTER SMALL CAPITAL BARRED B] - case '\u1E02': - // Ḃ [LATIN CAPITAL LETTER B WITH DOT ABOVE] - case '\u1E04': - // Ḅ [LATIN CAPITAL LETTER B WITH DOT BELOW] - case '\u1E06': - // Ḇ [LATIN CAPITAL LETTER B WITH LINE BELOW] - case '\u24B7': - // â’· [CIRCLED LATIN CAPITAL LETTER B] - case '\uFF22': // ï¼¢ [FULLWIDTH LATIN CAPITAL LETTER B] - output[opos++] = 'B'; - break; - - case '\u0180': - // Æ€ [LATIN SMALL LETTER B WITH STROKE] - case '\u0183': - // ƃ [LATIN SMALL LETTER B WITH TOPBAR] - case '\u0253': - // É“ [LATIN SMALL LETTER B WITH HOOK] - case '\u1D6C': - // ᵬ [LATIN SMALL LETTER B WITH MIDDLE TILDE] - case '\u1D80': - // á¶€ [LATIN SMALL LETTER B WITH PALATAL HOOK] - case '\u1E03': - // ḃ [LATIN SMALL LETTER B WITH DOT ABOVE] - case '\u1E05': - // ḅ [LATIN SMALL LETTER B WITH DOT BELOW] - case '\u1E07': - // ḇ [LATIN SMALL LETTER B WITH LINE BELOW] - case '\u24D1': - // â“‘ [CIRCLED LATIN SMALL LETTER B] - case '\uFF42': // b [FULLWIDTH LATIN SMALL LETTER B] - output[opos++] = 'b'; - break; - - case '\u249D': // â’� [PARENTHESIZED LATIN SMALL LETTER B] - output[opos++] = '('; - output[opos++] = 'b'; - output[opos++] = ')'; - break; - - case '\u00C7': - // Ç [LATIN CAPITAL LETTER C WITH CEDILLA] - case '\u0106': - // Ć [LATIN CAPITAL LETTER C WITH ACUTE] - case '\u0108': - // Ĉ [LATIN CAPITAL LETTER C WITH CIRCUMFLEX] - case '\u010A': - // ÄŠ [LATIN CAPITAL LETTER C WITH DOT ABOVE] - case '\u010C': - // ÄŒ [LATIN CAPITAL LETTER C WITH CARON] - case '\u0187': - // Ƈ [LATIN CAPITAL LETTER C WITH HOOK] - case '\u023B': - // È» [LATIN CAPITAL LETTER C WITH STROKE] - case '\u0297': - // Ê— [LATIN LETTER STRETCHED C] - case '\u1D04': - // á´„ [LATIN LETTER SMALL CAPITAL C] - case '\u1E08': - // Ḉ [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE] - case '\u24B8': - // â’¸ [CIRCLED LATIN CAPITAL LETTER C] - case '\uFF23': // ï¼£ [FULLWIDTH LATIN CAPITAL LETTER C] - output[opos++] = 'C'; - break; - - case '\u00E7': - // ç [LATIN SMALL LETTER C WITH CEDILLA] - case '\u0107': - // ć [LATIN SMALL LETTER C WITH ACUTE] - case '\u0109': - // ĉ [LATIN SMALL LETTER C WITH CIRCUMFLEX] - case '\u010B': - // Ä‹ [LATIN SMALL LETTER C WITH DOT ABOVE] - case '\u010D': - // � [LATIN SMALL LETTER C WITH CARON] - case '\u0188': - // ƈ [LATIN SMALL LETTER C WITH HOOK] - case '\u023C': - // ȼ [LATIN SMALL LETTER C WITH STROKE] - case '\u0255': - // É• [LATIN SMALL LETTER C WITH CURL] - case '\u1E09': - // ḉ [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE] - case '\u2184': - // ↄ [LATIN SMALL LETTER REVERSED C] - case '\u24D2': - // â“’ [CIRCLED LATIN SMALL LETTER C] - case '\uA73E': - // Ꜿ [LATIN CAPITAL LETTER REVERSED C WITH DOT] - case '\uA73F': - // ꜿ [LATIN SMALL LETTER REVERSED C WITH DOT] - case '\uFF43': // c [FULLWIDTH LATIN SMALL LETTER C] - output[opos++] = 'c'; - break; - - case '\u249E': // â’ž [PARENTHESIZED LATIN SMALL LETTER C] - output[opos++] = '('; - output[opos++] = 'c'; - output[opos++] = ')'; - break; - - case '\u00D0': - // � [LATIN CAPITAL LETTER ETH] - case '\u010E': - // ÄŽ [LATIN CAPITAL LETTER D WITH CARON] - case '\u0110': - // � [LATIN CAPITAL LETTER D WITH STROKE] - case '\u0189': - // Ɖ [LATIN CAPITAL LETTER AFRICAN D] - case '\u018A': - // ÆŠ [LATIN CAPITAL LETTER D WITH HOOK] - case '\u018B': - // Æ‹ [LATIN CAPITAL LETTER D WITH TOPBAR] - case '\u1D05': - // á´… [LATIN LETTER SMALL CAPITAL D] - case '\u1D06': - // á´† [LATIN LETTER SMALL CAPITAL ETH] - case '\u1E0A': - // Ḋ [LATIN CAPITAL LETTER D WITH DOT ABOVE] - case '\u1E0C': - // Ḍ [LATIN CAPITAL LETTER D WITH DOT BELOW] - case '\u1E0E': - // Ḏ [LATIN CAPITAL LETTER D WITH LINE BELOW] - case '\u1E10': - // � [LATIN CAPITAL LETTER D WITH CEDILLA] - case '\u1E12': - // Ḓ [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW] - case '\u24B9': - // â’¹ [CIRCLED LATIN CAPITAL LETTER D] - case '\uA779': - // � [LATIN CAPITAL LETTER INSULAR D] - case '\uFF24': // D [FULLWIDTH LATIN CAPITAL LETTER D] - output[opos++] = 'D'; - break; - - case '\u00F0': - // ð [LATIN SMALL LETTER ETH] - case '\u010F': - // � [LATIN SMALL LETTER D WITH CARON] - case '\u0111': - // Ä‘ [LATIN SMALL LETTER D WITH STROKE] - case '\u018C': - // ÆŒ [LATIN SMALL LETTER D WITH TOPBAR] - case '\u0221': - // È¡ [LATIN SMALL LETTER D WITH CURL] - case '\u0256': - // É– [LATIN SMALL LETTER D WITH TAIL] - case '\u0257': - // É— [LATIN SMALL LETTER D WITH HOOK] - case '\u1D6D': - // áµ­ [LATIN SMALL LETTER D WITH MIDDLE TILDE] - case '\u1D81': - // � [LATIN SMALL LETTER D WITH PALATAL HOOK] - case '\u1D91': - // á¶‘ [LATIN SMALL LETTER D WITH HOOK AND TAIL] - case '\u1E0B': - // ḋ [LATIN SMALL LETTER D WITH DOT ABOVE] - case '\u1E0D': - // � [LATIN SMALL LETTER D WITH DOT BELOW] - case '\u1E0F': - // � [LATIN SMALL LETTER D WITH LINE BELOW] - case '\u1E11': - // ḑ [LATIN SMALL LETTER D WITH CEDILLA] - case '\u1E13': - // ḓ [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW] - case '\u24D3': - // â““ [CIRCLED LATIN SMALL LETTER D] - case '\uA77A': - // � [LATIN SMALL LETTER INSULAR D] - case '\uFF44': // d [FULLWIDTH LATIN SMALL LETTER D] - output[opos++] = 'd'; - break; - - case '\u01C4': - // Ç„ [LATIN CAPITAL LETTER DZ WITH CARON] - case '\u01F1': // DZ [LATIN CAPITAL LETTER DZ] - output[opos++] = 'D'; - output[opos++] = 'Z'; - break; - - case '\u01C5': - // Ç… [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON] - case '\u01F2': // Dz [LATIN CAPITAL LETTER D WITH SMALL LETTER Z] - output[opos++] = 'D'; - output[opos++] = 'z'; - break; - - case '\u249F': // â’Ÿ [PARENTHESIZED LATIN SMALL LETTER D] - output[opos++] = '('; - output[opos++] = 'd'; - output[opos++] = ')'; - break; - - case '\u0238': // ȸ [LATIN SMALL LETTER DB DIGRAPH] - output[opos++] = 'd'; - output[opos++] = 'b'; - break; - - case '\u01C6': - // dž [LATIN SMALL LETTER DZ WITH CARON] - case '\u01F3': - // dz [LATIN SMALL LETTER DZ] - case '\u02A3': - // Ê£ [LATIN SMALL LETTER DZ DIGRAPH] - case '\u02A5': // Ê¥ [LATIN SMALL LETTER DZ DIGRAPH WITH CURL] - output[opos++] = 'd'; - output[opos++] = 'z'; - break; - - case '\u00C8': - // È [LATIN CAPITAL LETTER E WITH GRAVE] - case '\u00C9': - // É [LATIN CAPITAL LETTER E WITH ACUTE] - case '\u00CA': - // Ê [LATIN CAPITAL LETTER E WITH CIRCUMFLEX] - case '\u00CB': - // Ë [LATIN CAPITAL LETTER E WITH DIAERESIS] - case '\u0112': - // Ä’ [LATIN CAPITAL LETTER E WITH MACRON] - case '\u0114': - // �? [LATIN CAPITAL LETTER E WITH BREVE] - case '\u0116': - // Ä– [LATIN CAPITAL LETTER E WITH DOT ABOVE] - case '\u0118': - // Ę [LATIN CAPITAL LETTER E WITH OGONEK] - case '\u011A': - // Äš [LATIN CAPITAL LETTER E WITH CARON] - case '\u018E': - // ÆŽ [LATIN CAPITAL LETTER REVERSED E] - case '\u0190': - // � [LATIN CAPITAL LETTER OPEN E] - case '\u0204': - // È„ [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE] - case '\u0206': - // Ȇ [LATIN CAPITAL LETTER E WITH INVERTED BREVE] - case '\u0228': - // Ȩ [LATIN CAPITAL LETTER E WITH CEDILLA] - case '\u0246': - // Ɇ [LATIN CAPITAL LETTER E WITH STROKE] - case '\u1D07': - // á´‡ [LATIN LETTER SMALL CAPITAL E] - case '\u1E14': - // �? [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE] - case '\u1E16': - // Ḗ [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE] - case '\u1E18': - // Ḙ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW] - case '\u1E1A': - // Ḛ [LATIN CAPITAL LETTER E WITH TILDE BELOW] - case '\u1E1C': - // Ḝ [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE] - case '\u1EB8': - // Ẹ [LATIN CAPITAL LETTER E WITH DOT BELOW] - case '\u1EBA': - // Ẻ [LATIN CAPITAL LETTER E WITH HOOK ABOVE] - case '\u1EBC': - // Ẽ [LATIN CAPITAL LETTER E WITH TILDE] - case '\u1EBE': - // Ế [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE] - case '\u1EC0': - // Ề [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE] - case '\u1EC2': - // Ể [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1EC4': - // Ễ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE] - case '\u1EC6': - // Ệ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW] - case '\u24BA': - // â’º [CIRCLED LATIN CAPITAL LETTER E] - case '\u2C7B': - // â±» [LATIN LETTER SMALL CAPITAL TURNED E] - case '\uFF25': // ï¼¥ [FULLWIDTH LATIN CAPITAL LETTER E] - output[opos++] = 'E'; - break; - - case '\u00E8': - // è [LATIN SMALL LETTER E WITH GRAVE] - case '\u00E9': - // é [LATIN SMALL LETTER E WITH ACUTE] - case '\u00EA': - // ê [LATIN SMALL LETTER E WITH CIRCUMFLEX] - case '\u00EB': - // ë [LATIN SMALL LETTER E WITH DIAERESIS] - case '\u0113': - // Ä“ [LATIN SMALL LETTER E WITH MACRON] - case '\u0115': - // Ä• [LATIN SMALL LETTER E WITH BREVE] - case '\u0117': - // Ä— [LATIN SMALL LETTER E WITH DOT ABOVE] - case '\u0119': - // Ä™ [LATIN SMALL LETTER E WITH OGONEK] - case '\u011B': - // Ä› [LATIN SMALL LETTER E WITH CARON] - case '\u01DD': - // � [LATIN SMALL LETTER TURNED E] - case '\u0205': - // È… [LATIN SMALL LETTER E WITH DOUBLE GRAVE] - case '\u0207': - // ȇ [LATIN SMALL LETTER E WITH INVERTED BREVE] - case '\u0229': - // È© [LATIN SMALL LETTER E WITH CEDILLA] - case '\u0247': - // ɇ [LATIN SMALL LETTER E WITH STROKE] - case '\u0258': - // ɘ [LATIN SMALL LETTER REVERSED E] - case '\u025B': - // É› [LATIN SMALL LETTER OPEN E] - case '\u025C': - // Éœ [LATIN SMALL LETTER REVERSED OPEN E] - case '\u025D': - // � [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK] - case '\u025E': - // Éž [LATIN SMALL LETTER CLOSED REVERSED OPEN E] - case '\u029A': - // Êš [LATIN SMALL LETTER CLOSED OPEN E] - case '\u1D08': - // á´ˆ [LATIN SMALL LETTER TURNED OPEN E] - case '\u1D92': - // á¶’ [LATIN SMALL LETTER E WITH RETROFLEX HOOK] - case '\u1D93': - // á¶“ [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK] - case '\u1D94': - // �? [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK] - case '\u1E15': - // ḕ [LATIN SMALL LETTER E WITH MACRON AND GRAVE] - case '\u1E17': - // ḗ [LATIN SMALL LETTER E WITH MACRON AND ACUTE] - case '\u1E19': - // ḙ [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW] - case '\u1E1B': - // ḛ [LATIN SMALL LETTER E WITH TILDE BELOW] - case '\u1E1D': - // � [LATIN SMALL LETTER E WITH CEDILLA AND BREVE] - case '\u1EB9': - // ẹ [LATIN SMALL LETTER E WITH DOT BELOW] - case '\u1EBB': - // ẻ [LATIN SMALL LETTER E WITH HOOK ABOVE] - case '\u1EBD': - // ẽ [LATIN SMALL LETTER E WITH TILDE] - case '\u1EBF': - // ế [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE] - case '\u1EC1': - // � [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE] - case '\u1EC3': - // ể [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1EC5': - // á»… [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE] - case '\u1EC7': - // ệ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW] - case '\u2091': - // â‚‘ [LATIN SUBSCRIPT SMALL LETTER E] - case '\u24D4': - // �? [CIRCLED LATIN SMALL LETTER E] - case '\u2C78': - // ⱸ [LATIN SMALL LETTER E WITH NOTCH] - case '\uFF45': // ï½… [FULLWIDTH LATIN SMALL LETTER E] - output[opos++] = 'e'; - break; - - case '\u24A0': // â’  [PARENTHESIZED LATIN SMALL LETTER E] - output[opos++] = '('; - output[opos++] = 'e'; - output[opos++] = ')'; - break; - - case '\u0191': - // Æ‘ [LATIN CAPITAL LETTER F WITH HOOK] - case '\u1E1E': - // Ḟ [LATIN CAPITAL LETTER F WITH DOT ABOVE] - case '\u24BB': - // â’» [CIRCLED LATIN CAPITAL LETTER F] - case '\uA730': - // ꜰ [LATIN LETTER SMALL CAPITAL F] - case '\uA77B': - // � [LATIN CAPITAL LETTER INSULAR F] - case '\uA7FB': - // ꟻ [LATIN EPIGRAPHIC LETTER REVERSED F] - case '\uFF26': // F [FULLWIDTH LATIN CAPITAL LETTER F] - output[opos++] = 'F'; - break; - - case '\u0192': - // Æ’ [LATIN SMALL LETTER F WITH HOOK] - case '\u1D6E': - // áµ® [LATIN SMALL LETTER F WITH MIDDLE TILDE] - case '\u1D82': - // á¶‚ [LATIN SMALL LETTER F WITH PALATAL HOOK] - case '\u1E1F': - // ḟ [LATIN SMALL LETTER F WITH DOT ABOVE] - case '\u1E9B': - // ẛ [LATIN SMALL LETTER LONG S WITH DOT ABOVE] - case '\u24D5': - // â“• [CIRCLED LATIN SMALL LETTER F] - case '\uA77C': - // � [LATIN SMALL LETTER INSULAR F] - case '\uFF46': // f [FULLWIDTH LATIN SMALL LETTER F] - output[opos++] = 'f'; - break; - - case '\u24A1': // â’¡ [PARENTHESIZED LATIN SMALL LETTER F] - output[opos++] = '('; - output[opos++] = 'f'; - output[opos++] = ')'; - break; - - case '\uFB00': // ff [LATIN SMALL LIGATURE FF] - output[opos++] = 'f'; - output[opos++] = 'f'; - break; - - case '\uFB03': // ffi [LATIN SMALL LIGATURE FFI] - output[opos++] = 'f'; - output[opos++] = 'f'; - output[opos++] = 'i'; - break; - - case '\uFB04': // ffl [LATIN SMALL LIGATURE FFL] - output[opos++] = 'f'; - output[opos++] = 'f'; - output[opos++] = 'l'; - break; - - case '\uFB01': // � [LATIN SMALL LIGATURE FI] - output[opos++] = 'f'; - output[opos++] = 'i'; - break; - - case '\uFB02': // fl [LATIN SMALL LIGATURE FL] - output[opos++] = 'f'; - output[opos++] = 'l'; - break; - - case '\u011C': - // Äœ [LATIN CAPITAL LETTER G WITH CIRCUMFLEX] - case '\u011E': - // Äž [LATIN CAPITAL LETTER G WITH BREVE] - case '\u0120': - // Ä  [LATIN CAPITAL LETTER G WITH DOT ABOVE] - case '\u0122': - // Ä¢ [LATIN CAPITAL LETTER G WITH CEDILLA] - case '\u0193': - // Æ“ [LATIN CAPITAL LETTER G WITH HOOK] - case '\u01E4': - // Ǥ [LATIN CAPITAL LETTER G WITH STROKE] - case '\u01E5': - // Ç¥ [LATIN SMALL LETTER G WITH STROKE] - case '\u01E6': - // Ǧ [LATIN CAPITAL LETTER G WITH CARON] - case '\u01E7': - // ǧ [LATIN SMALL LETTER G WITH CARON] - case '\u01F4': - // Ç´ [LATIN CAPITAL LETTER G WITH ACUTE] - case '\u0262': - // É¢ [LATIN LETTER SMALL CAPITAL G] - case '\u029B': - // Ê› [LATIN LETTER SMALL CAPITAL G WITH HOOK] - case '\u1E20': - // Ḡ [LATIN CAPITAL LETTER G WITH MACRON] - case '\u24BC': - // â’¼ [CIRCLED LATIN CAPITAL LETTER G] - case '\uA77D': - // � [LATIN CAPITAL LETTER INSULAR G] - case '\uA77E': - // � [LATIN CAPITAL LETTER TURNED INSULAR G] - case '\uFF27': // ï¼§ [FULLWIDTH LATIN CAPITAL LETTER G] - output[opos++] = 'G'; - break; - - case '\u011D': - // � [LATIN SMALL LETTER G WITH CIRCUMFLEX] - case '\u011F': - // ÄŸ [LATIN SMALL LETTER G WITH BREVE] - case '\u0121': - // Ä¡ [LATIN SMALL LETTER G WITH DOT ABOVE] - case '\u0123': - // Ä£ [LATIN SMALL LETTER G WITH CEDILLA] - case '\u01F5': - // ǵ [LATIN SMALL LETTER G WITH ACUTE] - case '\u0260': - // É  [LATIN SMALL LETTER G WITH HOOK] - case '\u0261': - // É¡ [LATIN SMALL LETTER SCRIPT G] - case '\u1D77': - // áµ· [LATIN SMALL LETTER TURNED G] - case '\u1D79': - // áµ¹ [LATIN SMALL LETTER INSULAR G] - case '\u1D83': - // ᶃ [LATIN SMALL LETTER G WITH PALATAL HOOK] - case '\u1E21': - // ḡ [LATIN SMALL LETTER G WITH MACRON] - case '\u24D6': - // â“– [CIRCLED LATIN SMALL LETTER G] - case '\uA77F': - // � [LATIN SMALL LETTER TURNED INSULAR G] - case '\uFF47': // g [FULLWIDTH LATIN SMALL LETTER G] - output[opos++] = 'g'; - break; - - case '\u24A2': // â’¢ [PARENTHESIZED LATIN SMALL LETTER G] - output[opos++] = '('; - output[opos++] = 'g'; - output[opos++] = ')'; - break; - - case '\u0124': - // Ĥ [LATIN CAPITAL LETTER H WITH CIRCUMFLEX] - case '\u0126': - // Ħ [LATIN CAPITAL LETTER H WITH STROKE] - case '\u021E': - // Èž [LATIN CAPITAL LETTER H WITH CARON] - case '\u029C': - // Êœ [LATIN LETTER SMALL CAPITAL H] - case '\u1E22': - // Ḣ [LATIN CAPITAL LETTER H WITH DOT ABOVE] - case '\u1E24': - // Ḥ [LATIN CAPITAL LETTER H WITH DOT BELOW] - case '\u1E26': - // Ḧ [LATIN CAPITAL LETTER H WITH DIAERESIS] - case '\u1E28': - // Ḩ [LATIN CAPITAL LETTER H WITH CEDILLA] - case '\u1E2A': - // Ḫ [LATIN CAPITAL LETTER H WITH BREVE BELOW] - case '\u24BD': - // â’½ [CIRCLED LATIN CAPITAL LETTER H] - case '\u2C67': - // â±§ [LATIN CAPITAL LETTER H WITH DESCENDER] - case '\u2C75': - // â±µ [LATIN CAPITAL LETTER HALF H] - case '\uFF28': // H [FULLWIDTH LATIN CAPITAL LETTER H] - output[opos++] = 'H'; - break; - - case '\u0125': - // Ä¥ [LATIN SMALL LETTER H WITH CIRCUMFLEX] - case '\u0127': - // ħ [LATIN SMALL LETTER H WITH STROKE] - case '\u021F': - // ÈŸ [LATIN SMALL LETTER H WITH CARON] - case '\u0265': - // É¥ [LATIN SMALL LETTER TURNED H] - case '\u0266': - // ɦ [LATIN SMALL LETTER H WITH HOOK] - case '\u02AE': - // Ê® [LATIN SMALL LETTER TURNED H WITH FISHHOOK] - case '\u02AF': - // ʯ [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL] - case '\u1E23': - // ḣ [LATIN SMALL LETTER H WITH DOT ABOVE] - case '\u1E25': - // ḥ [LATIN SMALL LETTER H WITH DOT BELOW] - case '\u1E27': - // ḧ [LATIN SMALL LETTER H WITH DIAERESIS] - case '\u1E29': - // ḩ [LATIN SMALL LETTER H WITH CEDILLA] - case '\u1E2B': - // ḫ [LATIN SMALL LETTER H WITH BREVE BELOW] - case '\u1E96': - // ẖ [LATIN SMALL LETTER H WITH LINE BELOW] - case '\u24D7': - // â“— [CIRCLED LATIN SMALL LETTER H] - case '\u2C68': - // ⱨ [LATIN SMALL LETTER H WITH DESCENDER] - case '\u2C76': - // â±¶ [LATIN SMALL LETTER HALF H] - case '\uFF48': // h [FULLWIDTH LATIN SMALL LETTER H] - output[opos++] = 'h'; - break; - - case '\u01F6': // Ƕ http://en.wikipedia.org/wiki/Hwair [LATIN CAPITAL LETTER HWAIR] - output[opos++] = 'H'; - output[opos++] = 'V'; - break; - - case '\u24A3': // â’£ [PARENTHESIZED LATIN SMALL LETTER H] - output[opos++] = '('; - output[opos++] = 'h'; - output[opos++] = ')'; - break; - - case '\u0195': // Æ• [LATIN SMALL LETTER HV] - output[opos++] = 'h'; - output[opos++] = 'v'; - break; - - case '\u00CC': - // ÃŒ [LATIN CAPITAL LETTER I WITH GRAVE] - case '\u00CD': - // � [LATIN CAPITAL LETTER I WITH ACUTE] - case '\u00CE': - // ÃŽ [LATIN CAPITAL LETTER I WITH CIRCUMFLEX] - case '\u00CF': - // � [LATIN CAPITAL LETTER I WITH DIAERESIS] - case '\u0128': - // Ĩ [LATIN CAPITAL LETTER I WITH TILDE] - case '\u012A': - // Ī [LATIN CAPITAL LETTER I WITH MACRON] - case '\u012C': - // Ĭ [LATIN CAPITAL LETTER I WITH BREVE] - case '\u012E': - // Ä® [LATIN CAPITAL LETTER I WITH OGONEK] - case '\u0130': - // İ [LATIN CAPITAL LETTER I WITH DOT ABOVE] - case '\u0196': - // Æ– [LATIN CAPITAL LETTER IOTA] - case '\u0197': - // Æ— [LATIN CAPITAL LETTER I WITH STROKE] - case '\u01CF': - // � [LATIN CAPITAL LETTER I WITH CARON] - case '\u0208': - // Ȉ [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE] - case '\u020A': - // ÈŠ [LATIN CAPITAL LETTER I WITH INVERTED BREVE] - case '\u026A': - // ɪ [LATIN LETTER SMALL CAPITAL I] - case '\u1D7B': - // áµ» [LATIN SMALL CAPITAL LETTER I WITH STROKE] - case '\u1E2C': - // Ḭ [LATIN CAPITAL LETTER I WITH TILDE BELOW] - case '\u1E2E': - // Ḯ [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE] - case '\u1EC8': - // Ỉ [LATIN CAPITAL LETTER I WITH HOOK ABOVE] - case '\u1ECA': - // Ị [LATIN CAPITAL LETTER I WITH DOT BELOW] - case '\u24BE': - // â’¾ [CIRCLED LATIN CAPITAL LETTER I] - case '\uA7FE': - // ꟾ [LATIN EPIGRAPHIC LETTER I LONGA] - case '\uFF29': // I [FULLWIDTH LATIN CAPITAL LETTER I] - output[opos++] = 'I'; - break; - - case '\u00EC': - // ì [LATIN SMALL LETTER I WITH GRAVE] - case '\u00ED': - // í [LATIN SMALL LETTER I WITH ACUTE] - case '\u00EE': - // î [LATIN SMALL LETTER I WITH CIRCUMFLEX] - case '\u00EF': - // ï [LATIN SMALL LETTER I WITH DIAERESIS] - case '\u0129': - // Ä© [LATIN SMALL LETTER I WITH TILDE] - case '\u012B': - // Ä« [LATIN SMALL LETTER I WITH MACRON] - case '\u012D': - // Ä­ [LATIN SMALL LETTER I WITH BREVE] - case '\u012F': - // į [LATIN SMALL LETTER I WITH OGONEK] - case '\u0131': - // ı [LATIN SMALL LETTER DOTLESS I] - case '\u01D0': - // � [LATIN SMALL LETTER I WITH CARON] - case '\u0209': - // ȉ [LATIN SMALL LETTER I WITH DOUBLE GRAVE] - case '\u020B': - // È‹ [LATIN SMALL LETTER I WITH INVERTED BREVE] - case '\u0268': - // ɨ [LATIN SMALL LETTER I WITH STROKE] - case '\u1D09': - // á´‰ [LATIN SMALL LETTER TURNED I] - case '\u1D62': - // áµ¢ [LATIN SUBSCRIPT SMALL LETTER I] - case '\u1D7C': - // áµ¼ [LATIN SMALL LETTER IOTA WITH STROKE] - case '\u1D96': - // á¶– [LATIN SMALL LETTER I WITH RETROFLEX HOOK] - case '\u1E2D': - // ḭ [LATIN SMALL LETTER I WITH TILDE BELOW] - case '\u1E2F': - // ḯ [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE] - case '\u1EC9': - // ỉ [LATIN SMALL LETTER I WITH HOOK ABOVE] - case '\u1ECB': - // ị [LATIN SMALL LETTER I WITH DOT BELOW] - case '\u2071': - // � [SUPERSCRIPT LATIN SMALL LETTER I] - case '\u24D8': - // ⓘ [CIRCLED LATIN SMALL LETTER I] - case '\uFF49': // i [FULLWIDTH LATIN SMALL LETTER I] - output[opos++] = 'i'; - break; - - case '\u0132': // IJ [LATIN CAPITAL LIGATURE IJ] - output[opos++] = 'I'; - output[opos++] = 'J'; - break; - - case '\u24A4': // â’¤ [PARENTHESIZED LATIN SMALL LETTER I] - output[opos++] = '('; - output[opos++] = 'i'; - output[opos++] = ')'; - break; - - case '\u0133': // ij [LATIN SMALL LIGATURE IJ] - output[opos++] = 'i'; - output[opos++] = 'j'; - break; - - case '\u0134': - // Ä´ [LATIN CAPITAL LETTER J WITH CIRCUMFLEX] - case '\u0248': - // Ɉ [LATIN CAPITAL LETTER J WITH STROKE] - case '\u1D0A': - // á´Š [LATIN LETTER SMALL CAPITAL J] - case '\u24BF': - // â’¿ [CIRCLED LATIN CAPITAL LETTER J] - case '\uFF2A': // J [FULLWIDTH LATIN CAPITAL LETTER J] - output[opos++] = 'J'; - break; - - case '\u0135': - // ĵ [LATIN SMALL LETTER J WITH CIRCUMFLEX] - case '\u01F0': - // ǰ [LATIN SMALL LETTER J WITH CARON] - case '\u0237': - // È· [LATIN SMALL LETTER DOTLESS J] - case '\u0249': - // ɉ [LATIN SMALL LETTER J WITH STROKE] - case '\u025F': - // ÉŸ [LATIN SMALL LETTER DOTLESS J WITH STROKE] - case '\u0284': - // Ê„ [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK] - case '\u029D': - // � [LATIN SMALL LETTER J WITH CROSSED-TAIL] - case '\u24D9': - // â“™ [CIRCLED LATIN SMALL LETTER J] - case '\u2C7C': - // â±¼ [LATIN SUBSCRIPT SMALL LETTER J] - case '\uFF4A': // j [FULLWIDTH LATIN SMALL LETTER J] - output[opos++] = 'j'; - break; - - case '\u24A5': // â’¥ [PARENTHESIZED LATIN SMALL LETTER J] - output[opos++] = '('; - output[opos++] = 'j'; - output[opos++] = ')'; - break; - - case '\u0136': - // Ķ [LATIN CAPITAL LETTER K WITH CEDILLA] - case '\u0198': - // Ƙ [LATIN CAPITAL LETTER K WITH HOOK] - case '\u01E8': - // Ǩ [LATIN CAPITAL LETTER K WITH CARON] - case '\u1D0B': - // á´‹ [LATIN LETTER SMALL CAPITAL K] - case '\u1E30': - // Ḱ [LATIN CAPITAL LETTER K WITH ACUTE] - case '\u1E32': - // Ḳ [LATIN CAPITAL LETTER K WITH DOT BELOW] - case '\u1E34': - // Ḵ [LATIN CAPITAL LETTER K WITH LINE BELOW] - case '\u24C0': - // â“€ [CIRCLED LATIN CAPITAL LETTER K] - case '\u2C69': - // Ⱪ [LATIN CAPITAL LETTER K WITH DESCENDER] - case '\uA740': - // � [LATIN CAPITAL LETTER K WITH STROKE] - case '\uA742': - // � [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE] - case '\uA744': - // � [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE] - case '\uFF2B': // K [FULLWIDTH LATIN CAPITAL LETTER K] - output[opos++] = 'K'; - break; - - case '\u0137': - // Ä· [LATIN SMALL LETTER K WITH CEDILLA] - case '\u0199': - // Æ™ [LATIN SMALL LETTER K WITH HOOK] - case '\u01E9': - // Ç© [LATIN SMALL LETTER K WITH CARON] - case '\u029E': - // Êž [LATIN SMALL LETTER TURNED K] - case '\u1D84': - // á¶„ [LATIN SMALL LETTER K WITH PALATAL HOOK] - case '\u1E31': - // ḱ [LATIN SMALL LETTER K WITH ACUTE] - case '\u1E33': - // ḳ [LATIN SMALL LETTER K WITH DOT BELOW] - case '\u1E35': - // ḵ [LATIN SMALL LETTER K WITH LINE BELOW] - case '\u24DA': - // ⓚ [CIRCLED LATIN SMALL LETTER K] - case '\u2C6A': - // ⱪ [LATIN SMALL LETTER K WITH DESCENDER] - case '\uA741': - // � [LATIN SMALL LETTER K WITH STROKE] - case '\uA743': - // � [LATIN SMALL LETTER K WITH DIAGONAL STROKE] - case '\uA745': - // � [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE] - case '\uFF4B': // k [FULLWIDTH LATIN SMALL LETTER K] - output[opos++] = 'k'; - break; - - case '\u24A6': // â’¦ [PARENTHESIZED LATIN SMALL LETTER K] - output[opos++] = '('; - output[opos++] = 'k'; - output[opos++] = ')'; - break; - - case '\u0139': - // Ĺ [LATIN CAPITAL LETTER L WITH ACUTE] - case '\u013B': - // Ä» [LATIN CAPITAL LETTER L WITH CEDILLA] - case '\u013D': - // Ľ [LATIN CAPITAL LETTER L WITH CARON] - case '\u013F': - // Ä¿ [LATIN CAPITAL LETTER L WITH MIDDLE DOT] - case '\u0141': - // � [LATIN CAPITAL LETTER L WITH STROKE] - case '\u023D': - // Ƚ [LATIN CAPITAL LETTER L WITH BAR] - case '\u029F': - // ÊŸ [LATIN LETTER SMALL CAPITAL L] - case '\u1D0C': - // á´Œ [LATIN LETTER SMALL CAPITAL L WITH STROKE] - case '\u1E36': - // Ḷ [LATIN CAPITAL LETTER L WITH DOT BELOW] - case '\u1E38': - // Ḹ [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON] - case '\u1E3A': - // Ḻ [LATIN CAPITAL LETTER L WITH LINE BELOW] - case '\u1E3C': - // Ḽ [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW] - case '\u24C1': - // � [CIRCLED LATIN CAPITAL LETTER L] - case '\u2C60': - // â±  [LATIN CAPITAL LETTER L WITH DOUBLE BAR] - case '\u2C62': - // â±¢ [LATIN CAPITAL LETTER L WITH MIDDLE TILDE] - case '\uA746': - // � [LATIN CAPITAL LETTER BROKEN L] - case '\uA748': - // � [LATIN CAPITAL LETTER L WITH HIGH STROKE] - case '\uA780': - // Ꞁ [LATIN CAPITAL LETTER TURNED L] - case '\uFF2C': // L [FULLWIDTH LATIN CAPITAL LETTER L] - output[opos++] = 'L'; - break; - - case '\u013A': - // ĺ [LATIN SMALL LETTER L WITH ACUTE] - case '\u013C': - // ļ [LATIN SMALL LETTER L WITH CEDILLA] - case '\u013E': - // ľ [LATIN SMALL LETTER L WITH CARON] - case '\u0140': - // Å€ [LATIN SMALL LETTER L WITH MIDDLE DOT] - case '\u0142': - // Å‚ [LATIN SMALL LETTER L WITH STROKE] - case '\u019A': - // Æš [LATIN SMALL LETTER L WITH BAR] - case '\u0234': - // È´ [LATIN SMALL LETTER L WITH CURL] - case '\u026B': - // É« [LATIN SMALL LETTER L WITH MIDDLE TILDE] - case '\u026C': - // ɬ [LATIN SMALL LETTER L WITH BELT] - case '\u026D': - // É­ [LATIN SMALL LETTER L WITH RETROFLEX HOOK] - case '\u1D85': - // á¶… [LATIN SMALL LETTER L WITH PALATAL HOOK] - case '\u1E37': - // ḷ [LATIN SMALL LETTER L WITH DOT BELOW] - case '\u1E39': - // ḹ [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON] - case '\u1E3B': - // ḻ [LATIN SMALL LETTER L WITH LINE BELOW] - case '\u1E3D': - // ḽ [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW] - case '\u24DB': - // â“› [CIRCLED LATIN SMALL LETTER L] - case '\u2C61': - // ⱡ [LATIN SMALL LETTER L WITH DOUBLE BAR] - case '\uA747': - // � [LATIN SMALL LETTER BROKEN L] - case '\uA749': - // � [LATIN SMALL LETTER L WITH HIGH STROKE] - case '\uA781': - // � [LATIN SMALL LETTER TURNED L] - case '\uFF4C': // l [FULLWIDTH LATIN SMALL LETTER L] - output[opos++] = 'l'; - break; - - case '\u01C7': // LJ [LATIN CAPITAL LETTER LJ] - output[opos++] = 'L'; - output[opos++] = 'J'; - break; - - case '\u1EFA': // Ỻ [LATIN CAPITAL LETTER MIDDLE-WELSH LL] - output[opos++] = 'L'; - output[opos++] = 'L'; - break; - - case '\u01C8': // Lj [LATIN CAPITAL LETTER L WITH SMALL LETTER J] - output[opos++] = 'L'; - output[opos++] = 'j'; - break; - - case '\u24A7': // â’§ [PARENTHESIZED LATIN SMALL LETTER L] - output[opos++] = '('; - output[opos++] = 'l'; - output[opos++] = ')'; - break; - - case '\u01C9': // lj [LATIN SMALL LETTER LJ] - output[opos++] = 'l'; - output[opos++] = 'j'; - break; - - case '\u1EFB': // á»» [LATIN SMALL LETTER MIDDLE-WELSH LL] - output[opos++] = 'l'; - output[opos++] = 'l'; - break; - - case '\u02AA': // ʪ [LATIN SMALL LETTER LS DIGRAPH] - output[opos++] = 'l'; - output[opos++] = 's'; - break; - - case '\u02AB': // Ê« [LATIN SMALL LETTER LZ DIGRAPH] - output[opos++] = 'l'; - output[opos++] = 'z'; - break; - - case '\u019C': - // Æœ [LATIN CAPITAL LETTER TURNED M] - case '\u1D0D': - // á´� [LATIN LETTER SMALL CAPITAL M] - case '\u1E3E': - // Ḿ [LATIN CAPITAL LETTER M WITH ACUTE] - case '\u1E40': - // á¹€ [LATIN CAPITAL LETTER M WITH DOT ABOVE] - case '\u1E42': - // Ṃ [LATIN CAPITAL LETTER M WITH DOT BELOW] - case '\u24C2': - // â“‚ [CIRCLED LATIN CAPITAL LETTER M] - case '\u2C6E': - // â±® [LATIN CAPITAL LETTER M WITH HOOK] - case '\uA7FD': - // ꟽ [LATIN EPIGRAPHIC LETTER INVERTED M] - case '\uA7FF': - // ꟿ [LATIN EPIGRAPHIC LETTER ARCHAIC M] - case '\uFF2D': // ï¼­ [FULLWIDTH LATIN CAPITAL LETTER M] - output[opos++] = 'M'; - break; - - case '\u026F': - // ɯ [LATIN SMALL LETTER TURNED M] - case '\u0270': - // ɰ [LATIN SMALL LETTER TURNED M WITH LONG LEG] - case '\u0271': - // ɱ [LATIN SMALL LETTER M WITH HOOK] - case '\u1D6F': - // ᵯ [LATIN SMALL LETTER M WITH MIDDLE TILDE] - case '\u1D86': - // ᶆ [LATIN SMALL LETTER M WITH PALATAL HOOK] - case '\u1E3F': - // ḿ [LATIN SMALL LETTER M WITH ACUTE] - case '\u1E41': - // � [LATIN SMALL LETTER M WITH DOT ABOVE] - case '\u1E43': - // ṃ [LATIN SMALL LETTER M WITH DOT BELOW] - case '\u24DC': - // ⓜ [CIRCLED LATIN SMALL LETTER M] - case '\uFF4D': // � [FULLWIDTH LATIN SMALL LETTER M] - output[opos++] = 'm'; - break; - - case '\u24A8': // â’¨ [PARENTHESIZED LATIN SMALL LETTER M] - output[opos++] = '('; - output[opos++] = 'm'; - output[opos++] = ')'; - break; - - case '\u00D1': - // Ñ [LATIN CAPITAL LETTER N WITH TILDE] - case '\u0143': - // Ã…Æ’ [LATIN CAPITAL LETTER N WITH ACUTE] - case '\u0145': - // Å… [LATIN CAPITAL LETTER N WITH CEDILLA] - case '\u0147': - // Ň [LATIN CAPITAL LETTER N WITH CARON] - case '\u014A': - // Ã…Å  http://en.wikipedia.org/wiki/Eng_(letter) [LATIN CAPITAL LETTER ENG] - case '\u019D': - // � [LATIN CAPITAL LETTER N WITH LEFT HOOK] - case '\u01F8': - // Ǹ [LATIN CAPITAL LETTER N WITH GRAVE] - case '\u0220': - // È  [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG] - case '\u0274': - // É´ [LATIN LETTER SMALL CAPITAL N] - case '\u1D0E': - // á´Ž [LATIN LETTER SMALL CAPITAL REVERSED N] - case '\u1E44': - // Ṅ [LATIN CAPITAL LETTER N WITH DOT ABOVE] - case '\u1E46': - // Ṇ [LATIN CAPITAL LETTER N WITH DOT BELOW] - case '\u1E48': - // Ṉ [LATIN CAPITAL LETTER N WITH LINE BELOW] - case '\u1E4A': - // Ṋ [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW] - case '\u24C3': - // Ⓝ [CIRCLED LATIN CAPITAL LETTER N] - case '\uFF2E': // ï¼® [FULLWIDTH LATIN CAPITAL LETTER N] - output[opos++] = 'N'; - break; - - case '\u00F1': - // ñ [LATIN SMALL LETTER N WITH TILDE] - case '\u0144': - // Å„ [LATIN SMALL LETTER N WITH ACUTE] - case '\u0146': - // ņ [LATIN SMALL LETTER N WITH CEDILLA] - case '\u0148': - // ň [LATIN SMALL LETTER N WITH CARON] - case '\u0149': - // ʼn [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE] - case '\u014B': - // Å‹ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN SMALL LETTER ENG] - case '\u019E': - // Æž [LATIN SMALL LETTER N WITH LONG RIGHT LEG] - case '\u01F9': - // ǹ [LATIN SMALL LETTER N WITH GRAVE] - case '\u0235': - // ȵ [LATIN SMALL LETTER N WITH CURL] - case '\u0272': - // ɲ [LATIN SMALL LETTER N WITH LEFT HOOK] - case '\u0273': - // ɳ [LATIN SMALL LETTER N WITH RETROFLEX HOOK] - case '\u1D70': - // áµ° [LATIN SMALL LETTER N WITH MIDDLE TILDE] - case '\u1D87': - // ᶇ [LATIN SMALL LETTER N WITH PALATAL HOOK] - case '\u1E45': - // á¹… [LATIN SMALL LETTER N WITH DOT ABOVE] - case '\u1E47': - // ṇ [LATIN SMALL LETTER N WITH DOT BELOW] - case '\u1E49': - // ṉ [LATIN SMALL LETTER N WITH LINE BELOW] - case '\u1E4B': - // ṋ [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW] - case '\u207F': - // � [SUPERSCRIPT LATIN SMALL LETTER N] - case '\u24DD': - // � [CIRCLED LATIN SMALL LETTER N] - case '\uFF4E': // n [FULLWIDTH LATIN SMALL LETTER N] - output[opos++] = 'n'; - break; - - case '\u01CA': // ÇŠ [LATIN CAPITAL LETTER NJ] - output[opos++] = 'N'; - output[opos++] = 'J'; - break; - - case '\u01CB': // Ç‹ [LATIN CAPITAL LETTER N WITH SMALL LETTER J] - output[opos++] = 'N'; - output[opos++] = 'j'; - break; - - case '\u24A9': // â’© [PARENTHESIZED LATIN SMALL LETTER N] - output[opos++] = '('; - output[opos++] = 'n'; - output[opos++] = ')'; - break; - - case '\u01CC': // ÇŒ [LATIN SMALL LETTER NJ] - output[opos++] = 'n'; - output[opos++] = 'j'; - break; - - case '\u00D2': - // Ã’ [LATIN CAPITAL LETTER O WITH GRAVE] - case '\u00D3': - // Ó [LATIN CAPITAL LETTER O WITH ACUTE] - case '\u00D4': - // �? [LATIN CAPITAL LETTER O WITH CIRCUMFLEX] - case '\u00D5': - // Õ [LATIN CAPITAL LETTER O WITH TILDE] - case '\u00D6': - // Ö [LATIN CAPITAL LETTER O WITH DIAERESIS] - case '\u00D8': - // Ø [LATIN CAPITAL LETTER O WITH STROKE] - case '\u014C': - // Ã…Å’ [LATIN CAPITAL LETTER O WITH MACRON] - case '\u014E': - // ÅŽ [LATIN CAPITAL LETTER O WITH BREVE] - case '\u0150': - // � [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE] - case '\u0186': - // Ɔ [LATIN CAPITAL LETTER OPEN O] - case '\u019F': - // ÆŸ [LATIN CAPITAL LETTER O WITH MIDDLE TILDE] - case '\u01A0': - // Æ  [LATIN CAPITAL LETTER O WITH HORN] - case '\u01D1': - // Ç‘ [LATIN CAPITAL LETTER O WITH CARON] - case '\u01EA': - // Ǫ [LATIN CAPITAL LETTER O WITH OGONEK] - case '\u01EC': - // Ǭ [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON] - case '\u01FE': - // Ǿ [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE] - case '\u020C': - // ÈŒ [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE] - case '\u020E': - // ÈŽ [LATIN CAPITAL LETTER O WITH INVERTED BREVE] - case '\u022A': - // Ȫ [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON] - case '\u022C': - // Ȭ [LATIN CAPITAL LETTER O WITH TILDE AND MACRON] - case '\u022E': - // È® [LATIN CAPITAL LETTER O WITH DOT ABOVE] - case '\u0230': - // Ȱ [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON] - case '\u1D0F': - // á´� [LATIN LETTER SMALL CAPITAL O] - case '\u1D10': - // á´� [LATIN LETTER SMALL CAPITAL OPEN O] - case '\u1E4C': - // Ṍ [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE] - case '\u1E4E': - // Ṏ [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS] - case '\u1E50': - // � [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE] - case '\u1E52': - // á¹’ [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE] - case '\u1ECC': - // Ọ [LATIN CAPITAL LETTER O WITH DOT BELOW] - case '\u1ECE': - // Ỏ [LATIN CAPITAL LETTER O WITH HOOK ABOVE] - case '\u1ED0': - // � [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE] - case '\u1ED2': - // á»’ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE] - case '\u1ED4': - // �? [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1ED6': - // á»– [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE] - case '\u1ED8': - // Ộ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW] - case '\u1EDA': - // Ớ [LATIN CAPITAL LETTER O WITH HORN AND ACUTE] - case '\u1EDC': - // Ờ [LATIN CAPITAL LETTER O WITH HORN AND GRAVE] - case '\u1EDE': - // Ở [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE] - case '\u1EE0': - // á»  [LATIN CAPITAL LETTER O WITH HORN AND TILDE] - case '\u1EE2': - // Ợ [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW] - case '\u24C4': - // â“„ [CIRCLED LATIN CAPITAL LETTER O] - case '\uA74A': - // � [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY] - case '\uA74C': - // � [LATIN CAPITAL LETTER O WITH LOOP] - case '\uFF2F': // O [FULLWIDTH LATIN CAPITAL LETTER O] - output[opos++] = 'O'; - break; - - case '\u00F2': - // ò [LATIN SMALL LETTER O WITH GRAVE] - case '\u00F3': - // ó [LATIN SMALL LETTER O WITH ACUTE] - case '\u00F4': - // ô [LATIN SMALL LETTER O WITH CIRCUMFLEX] - case '\u00F5': - // õ [LATIN SMALL LETTER O WITH TILDE] - case '\u00F6': - // ö [LATIN SMALL LETTER O WITH DIAERESIS] - case '\u00F8': - // ø [LATIN SMALL LETTER O WITH STROKE] - case '\u014D': - // � [LATIN SMALL LETTER O WITH MACRON] - case '\u014F': - // � [LATIN SMALL LETTER O WITH BREVE] - case '\u0151': - // Å‘ [LATIN SMALL LETTER O WITH DOUBLE ACUTE] - case '\u01A1': - // Æ¡ [LATIN SMALL LETTER O WITH HORN] - case '\u01D2': - // Ç’ [LATIN SMALL LETTER O WITH CARON] - case '\u01EB': - // Ç« [LATIN SMALL LETTER O WITH OGONEK] - case '\u01ED': - // Ç­ [LATIN SMALL LETTER O WITH OGONEK AND MACRON] - case '\u01FF': - // Ç¿ [LATIN SMALL LETTER O WITH STROKE AND ACUTE] - case '\u020D': - // � [LATIN SMALL LETTER O WITH DOUBLE GRAVE] - case '\u020F': - // � [LATIN SMALL LETTER O WITH INVERTED BREVE] - case '\u022B': - // È« [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON] - case '\u022D': - // È­ [LATIN SMALL LETTER O WITH TILDE AND MACRON] - case '\u022F': - // ȯ [LATIN SMALL LETTER O WITH DOT ABOVE] - case '\u0231': - // ȱ [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON] - case '\u0254': - // �? [LATIN SMALL LETTER OPEN O] - case '\u0275': - // ɵ [LATIN SMALL LETTER BARRED O] - case '\u1D16': - // á´– [LATIN SMALL LETTER TOP HALF O] - case '\u1D17': - // á´— [LATIN SMALL LETTER BOTTOM HALF O] - case '\u1D97': - // á¶— [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK] - case '\u1E4D': - // � [LATIN SMALL LETTER O WITH TILDE AND ACUTE] - case '\u1E4F': - // � [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS] - case '\u1E51': - // ṑ [LATIN SMALL LETTER O WITH MACRON AND GRAVE] - case '\u1E53': - // ṓ [LATIN SMALL LETTER O WITH MACRON AND ACUTE] - case '\u1ECD': - // � [LATIN SMALL LETTER O WITH DOT BELOW] - case '\u1ECF': - // � [LATIN SMALL LETTER O WITH HOOK ABOVE] - case '\u1ED1': - // ố [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE] - case '\u1ED3': - // ồ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE] - case '\u1ED5': - // ổ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] - case '\u1ED7': - // á»— [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE] - case '\u1ED9': - // á»™ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW] - case '\u1EDB': - // á»› [LATIN SMALL LETTER O WITH HORN AND ACUTE] - case '\u1EDD': - // � [LATIN SMALL LETTER O WITH HORN AND GRAVE] - case '\u1EDF': - // ở [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE] - case '\u1EE1': - // ỡ [LATIN SMALL LETTER O WITH HORN AND TILDE] - case '\u1EE3': - // ợ [LATIN SMALL LETTER O WITH HORN AND DOT BELOW] - case '\u2092': - // â‚’ [LATIN SUBSCRIPT SMALL LETTER O] - case '\u24DE': - // ⓞ [CIRCLED LATIN SMALL LETTER O] - case '\u2C7A': - // ⱺ [LATIN SMALL LETTER O WITH LOW RING INSIDE] - case '\uA74B': - // � [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY] - case '\uA74D': - // � [LATIN SMALL LETTER O WITH LOOP] - case '\uFF4F': // � [FULLWIDTH LATIN SMALL LETTER O] - output[opos++] = 'o'; - break; - - case '\u0152': - // Å’ [LATIN CAPITAL LIGATURE OE] - case '\u0276': // ɶ [LATIN LETTER SMALL CAPITAL OE] - output[opos++] = 'O'; - output[opos++] = 'E'; - break; - - case '\uA74E': // � [LATIN CAPITAL LETTER OO] - output[opos++] = 'O'; - output[opos++] = 'O'; - break; - - case '\u0222': - // È¢ http://en.wikipedia.org/wiki/OU [LATIN CAPITAL LETTER OU] - case '\u1D15': // á´• [LATIN LETTER SMALL CAPITAL OU] - output[opos++] = 'O'; - output[opos++] = 'U'; - break; - - case '\u24AA': // â’ª [PARENTHESIZED LATIN SMALL LETTER O] - output[opos++] = '('; - output[opos++] = 'o'; - output[opos++] = ')'; - break; - - case '\u0153': - // Å“ [LATIN SMALL LIGATURE OE] - case '\u1D14': // á´�? [LATIN SMALL LETTER TURNED OE] - output[opos++] = 'o'; - output[opos++] = 'e'; - break; - - case '\uA74F': // � [LATIN SMALL LETTER OO] - output[opos++] = 'o'; - output[opos++] = 'o'; - break; - - case '\u0223': // È£ http://en.wikipedia.org/wiki/OU [LATIN SMALL LETTER OU] - output[opos++] = 'o'; - output[opos++] = 'u'; - break; - - case '\u01A4': - // Ƥ [LATIN CAPITAL LETTER P WITH HOOK] - case '\u1D18': - // á´˜ [LATIN LETTER SMALL CAPITAL P] - case '\u1E54': - // �? [LATIN CAPITAL LETTER P WITH ACUTE] - case '\u1E56': - // á¹– [LATIN CAPITAL LETTER P WITH DOT ABOVE] - case '\u24C5': - // â“… [CIRCLED LATIN CAPITAL LETTER P] - case '\u2C63': - // â±£ [LATIN CAPITAL LETTER P WITH STROKE] - case '\uA750': - // � [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER] - case '\uA752': - // � [LATIN CAPITAL LETTER P WITH FLOURISH] - case '\uA754': - // �? [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL] - case '\uFF30': // ï¼° [FULLWIDTH LATIN CAPITAL LETTER P] - output[opos++] = 'P'; - break; - - case '\u01A5': - // Æ¥ [LATIN SMALL LETTER P WITH HOOK] - case '\u1D71': - // áµ± [LATIN SMALL LETTER P WITH MIDDLE TILDE] - case '\u1D7D': - // áµ½ [LATIN SMALL LETTER P WITH STROKE] - case '\u1D88': - // ᶈ [LATIN SMALL LETTER P WITH PALATAL HOOK] - case '\u1E55': - // ṕ [LATIN SMALL LETTER P WITH ACUTE] - case '\u1E57': - // á¹— [LATIN SMALL LETTER P WITH DOT ABOVE] - case '\u24DF': - // ⓟ [CIRCLED LATIN SMALL LETTER P] - case '\uA751': - // � [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER] - case '\uA753': - // � [LATIN SMALL LETTER P WITH FLOURISH] - case '\uA755': - // � [LATIN SMALL LETTER P WITH SQUIRREL TAIL] - case '\uA7FC': - // ꟼ [LATIN EPIGRAPHIC LETTER REVERSED P] - case '\uFF50': // � [FULLWIDTH LATIN SMALL LETTER P] - output[opos++] = 'p'; - break; - - case '\u24AB': // â’« [PARENTHESIZED LATIN SMALL LETTER P] - output[opos++] = '('; - output[opos++] = 'p'; - output[opos++] = ')'; - break; - - case '\u024A': - // ÉŠ [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL] - case '\u24C6': - // Ⓠ [CIRCLED LATIN CAPITAL LETTER Q] - case '\uA756': - // � [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER] - case '\uA758': - // � [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE] - case '\uFF31': // ï¼± [FULLWIDTH LATIN CAPITAL LETTER Q] - output[opos++] = 'Q'; - break; - - case '\u0138': - // ĸ http://en.wikipedia.org/wiki/Kra_(letter) [LATIN SMALL LETTER KRA] - case '\u024B': - // É‹ [LATIN SMALL LETTER Q WITH HOOK TAIL] - case '\u02A0': - // Ê  [LATIN SMALL LETTER Q WITH HOOK] - case '\u24E0': - // â“  [CIRCLED LATIN SMALL LETTER Q] - case '\uA757': - // � [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER] - case '\uA759': - // � [LATIN SMALL LETTER Q WITH DIAGONAL STROKE] - case '\uFF51': // q [FULLWIDTH LATIN SMALL LETTER Q] - output[opos++] = 'q'; - break; - - case '\u24AC': // â’¬ [PARENTHESIZED LATIN SMALL LETTER Q] - output[opos++] = '('; - output[opos++] = 'q'; - output[opos++] = ')'; - break; - - case '\u0239': // ȹ [LATIN SMALL LETTER QP DIGRAPH] - output[opos++] = 'q'; - output[opos++] = 'p'; - break; - - case '\u0154': - // �? [LATIN CAPITAL LETTER R WITH ACUTE] - case '\u0156': - // Å– [LATIN CAPITAL LETTER R WITH CEDILLA] - case '\u0158': - // Ã…Ëœ [LATIN CAPITAL LETTER R WITH CARON] - case '\u0210': - // È’ [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE] - case '\u0212': - // È’ [LATIN CAPITAL LETTER R WITH INVERTED BREVE] - case '\u024C': - // ÉŒ [LATIN CAPITAL LETTER R WITH STROKE] - case '\u0280': - // Ê€ [LATIN LETTER SMALL CAPITAL R] - case '\u0281': - // � [LATIN LETTER SMALL CAPITAL INVERTED R] - case '\u1D19': - // á´™ [LATIN LETTER SMALL CAPITAL REVERSED R] - case '\u1D1A': - // á´š [LATIN LETTER SMALL CAPITAL TURNED R] - case '\u1E58': - // Ṙ [LATIN CAPITAL LETTER R WITH DOT ABOVE] - case '\u1E5A': - // Ṛ [LATIN CAPITAL LETTER R WITH DOT BELOW] - case '\u1E5C': - // Ṝ [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON] - case '\u1E5E': - // Ṟ [LATIN CAPITAL LETTER R WITH LINE BELOW] - case '\u24C7': - // Ⓡ [CIRCLED LATIN CAPITAL LETTER R] - case '\u2C64': - // Ɽ [LATIN CAPITAL LETTER R WITH TAIL] - case '\uA75A': - // � [LATIN CAPITAL LETTER R ROTUNDA] - case '\uA782': - // êž‚ [LATIN CAPITAL LETTER INSULAR R] - case '\uFF32': // ï¼² [FULLWIDTH LATIN CAPITAL LETTER R] - output[opos++] = 'R'; - break; - - case '\u0155': - // Å• [LATIN SMALL LETTER R WITH ACUTE] - case '\u0157': - // Å— [LATIN SMALL LETTER R WITH CEDILLA] - case '\u0159': - // Ã…â„¢ [LATIN SMALL LETTER R WITH CARON] - case '\u0211': - // È‘ [LATIN SMALL LETTER R WITH DOUBLE GRAVE] - case '\u0213': - // È“ [LATIN SMALL LETTER R WITH INVERTED BREVE] - case '\u024D': - // � [LATIN SMALL LETTER R WITH STROKE] - case '\u027C': - // ɼ [LATIN SMALL LETTER R WITH LONG LEG] - case '\u027D': - // ɽ [LATIN SMALL LETTER R WITH TAIL] - case '\u027E': - // ɾ [LATIN SMALL LETTER R WITH FISHHOOK] - case '\u027F': - // É¿ [LATIN SMALL LETTER REVERSED R WITH FISHHOOK] - case '\u1D63': - // áµ£ [LATIN SUBSCRIPT SMALL LETTER R] - case '\u1D72': - // áµ² [LATIN SMALL LETTER R WITH MIDDLE TILDE] - case '\u1D73': - // áµ³ [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE] - case '\u1D89': - // ᶉ [LATIN SMALL LETTER R WITH PALATAL HOOK] - case '\u1E59': - // á¹™ [LATIN SMALL LETTER R WITH DOT ABOVE] - case '\u1E5B': - // á¹› [LATIN SMALL LETTER R WITH DOT BELOW] - case '\u1E5D': - // � [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON] - case '\u1E5F': - // ṟ [LATIN SMALL LETTER R WITH LINE BELOW] - case '\u24E1': - // â“¡ [CIRCLED LATIN SMALL LETTER R] - case '\uA75B': - // � [LATIN SMALL LETTER R ROTUNDA] - case '\uA783': - // ꞃ [LATIN SMALL LETTER INSULAR R] - case '\uFF52': // ï½’ [FULLWIDTH LATIN SMALL LETTER R] - output[opos++] = 'r'; - break; - - case '\u24AD': // â’­ [PARENTHESIZED LATIN SMALL LETTER R] - output[opos++] = '('; - output[opos++] = 'r'; - output[opos++] = ')'; - break; - - case '\u015A': - // Ã…Å¡ [LATIN CAPITAL LETTER S WITH ACUTE] - case '\u015C': - // Ã…Å“ [LATIN CAPITAL LETTER S WITH CIRCUMFLEX] - case '\u015E': - // Åž [LATIN CAPITAL LETTER S WITH CEDILLA] - case '\u0160': - // Å  [LATIN CAPITAL LETTER S WITH CARON] - case '\u0218': - // Ș [LATIN CAPITAL LETTER S WITH COMMA BELOW] - case '\u1E60': - // á¹  [LATIN CAPITAL LETTER S WITH DOT ABOVE] - case '\u1E62': - // á¹¢ [LATIN CAPITAL LETTER S WITH DOT BELOW] - case '\u1E64': - // Ṥ [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE] - case '\u1E66': - // Ṧ [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE] - case '\u1E68': - // Ṩ [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE] - case '\u24C8': - // Ⓢ [CIRCLED LATIN CAPITAL LETTER S] - case '\uA731': - // ꜱ [LATIN LETTER SMALL CAPITAL S] - case '\uA785': - // êž… [LATIN SMALL LETTER INSULAR S] - case '\uFF33': // ï¼³ [FULLWIDTH LATIN CAPITAL LETTER S] - output[opos++] = 'S'; - break; - - case '\u015B': - // Å› [LATIN SMALL LETTER S WITH ACUTE] - case '\u015D': - // � [LATIN SMALL LETTER S WITH CIRCUMFLEX] - case '\u015F': - // ÅŸ [LATIN SMALL LETTER S WITH CEDILLA] - case '\u0161': - // Å¡ [LATIN SMALL LETTER S WITH CARON] - case '\u017F': - // Å¿ http://en.wikipedia.org/wiki/Long_S [LATIN SMALL LETTER LONG S] - case '\u0219': - // È™ [LATIN SMALL LETTER S WITH COMMA BELOW] - case '\u023F': - // È¿ [LATIN SMALL LETTER S WITH SWASH TAIL] - case '\u0282': - // Ê‚ [LATIN SMALL LETTER S WITH HOOK] - case '\u1D74': - // áµ´ [LATIN SMALL LETTER S WITH MIDDLE TILDE] - case '\u1D8A': - // á¶Š [LATIN SMALL LETTER S WITH PALATAL HOOK] - case '\u1E61': - // ṡ [LATIN SMALL LETTER S WITH DOT ABOVE] - case '\u1E63': - // á¹£ [LATIN SMALL LETTER S WITH DOT BELOW] - case '\u1E65': - // á¹¥ [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE] - case '\u1E67': - // á¹§ [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE] - case '\u1E69': - // ṩ [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE] - case '\u1E9C': - // ẜ [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE] - case '\u1E9D': - // � [LATIN SMALL LETTER LONG S WITH HIGH STROKE] - case '\u24E2': - // â“¢ [CIRCLED LATIN SMALL LETTER S] - case '\uA784': - // êž„ [LATIN CAPITAL LETTER INSULAR S] - case '\uFF53': // s [FULLWIDTH LATIN SMALL LETTER S] - output[opos++] = 's'; - break; - - case '\u1E9E': // ẞ [LATIN CAPITAL LETTER SHARP S] - output[opos++] = 'S'; - output[opos++] = 'S'; - break; - - case '\u24AE': // â’® [PARENTHESIZED LATIN SMALL LETTER S] - output[opos++] = '('; - output[opos++] = 's'; - output[opos++] = ')'; - break; - - case '\u00DF': // ß [LATIN SMALL LETTER SHARP S] - output[opos++] = 's'; - output[opos++] = 's'; - break; - - case '\uFB06': // st [LATIN SMALL LIGATURE ST] - output[opos++] = 's'; - output[opos++] = 't'; - break; - - case '\u0162': - // Å¢ [LATIN CAPITAL LETTER T WITH CEDILLA] - case '\u0164': - // Ť [LATIN CAPITAL LETTER T WITH CARON] - case '\u0166': - // Ŧ [LATIN CAPITAL LETTER T WITH STROKE] - case '\u01AC': - // Ƭ [LATIN CAPITAL LETTER T WITH HOOK] - case '\u01AE': - // Æ® [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK] - case '\u021A': - // Èš [LATIN CAPITAL LETTER T WITH COMMA BELOW] - case '\u023E': - // Ⱦ [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE] - case '\u1D1B': - // á´› [LATIN LETTER SMALL CAPITAL T] - case '\u1E6A': - // Ṫ [LATIN CAPITAL LETTER T WITH DOT ABOVE] - case '\u1E6C': - // Ṭ [LATIN CAPITAL LETTER T WITH DOT BELOW] - case '\u1E6E': - // á¹® [LATIN CAPITAL LETTER T WITH LINE BELOW] - case '\u1E70': - // á¹° [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW] - case '\u24C9': - // Ⓣ [CIRCLED LATIN CAPITAL LETTER T] - case '\uA786': - // Ꞇ [LATIN CAPITAL LETTER INSULAR T] - case '\uFF34': // ï¼´ [FULLWIDTH LATIN CAPITAL LETTER T] - output[opos++] = 'T'; - break; - - case '\u0163': - // Å£ [LATIN SMALL LETTER T WITH CEDILLA] - case '\u0165': - // Ã…Â¥ [LATIN SMALL LETTER T WITH CARON] - case '\u0167': - // ŧ [LATIN SMALL LETTER T WITH STROKE] - case '\u01AB': - // Æ« [LATIN SMALL LETTER T WITH PALATAL HOOK] - case '\u01AD': - // Æ­ [LATIN SMALL LETTER T WITH HOOK] - case '\u021B': - // È› [LATIN SMALL LETTER T WITH COMMA BELOW] - case '\u0236': - // ȶ [LATIN SMALL LETTER T WITH CURL] - case '\u0287': - // ʇ [LATIN SMALL LETTER TURNED T] - case '\u0288': - // ʈ [LATIN SMALL LETTER T WITH RETROFLEX HOOK] - case '\u1D75': - // áµµ [LATIN SMALL LETTER T WITH MIDDLE TILDE] - case '\u1E6B': - // ṫ [LATIN SMALL LETTER T WITH DOT ABOVE] - case '\u1E6D': - // á¹­ [LATIN SMALL LETTER T WITH DOT BELOW] - case '\u1E6F': - // ṯ [LATIN SMALL LETTER T WITH LINE BELOW] - case '\u1E71': - // á¹± [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW] - case '\u1E97': - // ẗ [LATIN SMALL LETTER T WITH DIAERESIS] - case '\u24E3': - // â“£ [CIRCLED LATIN SMALL LETTER T] - case '\u2C66': - // ⱦ [LATIN SMALL LETTER T WITH DIAGONAL STROKE] - case '\uFF54': // �? [FULLWIDTH LATIN SMALL LETTER T] - output[opos++] = 't'; - break; - - case '\u00DE': - // Þ [LATIN CAPITAL LETTER THORN] - case '\uA766': // � [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER] - output[opos++] = 'T'; - output[opos++] = 'H'; - break; - - case '\uA728': // Ꜩ [LATIN CAPITAL LETTER TZ] - output[opos++] = 'T'; - output[opos++] = 'Z'; - break; - - case '\u24AF': // â’¯ [PARENTHESIZED LATIN SMALL LETTER T] - output[opos++] = '('; - output[opos++] = 't'; - output[opos++] = ')'; - break; - - case '\u02A8': // ʨ [LATIN SMALL LETTER TC DIGRAPH WITH CURL] - output[opos++] = 't'; - output[opos++] = 'c'; - break; - - case '\u00FE': - // þ [LATIN SMALL LETTER THORN] - case '\u1D7A': - // ᵺ [LATIN SMALL LETTER TH WITH STRIKETHROUGH] - case '\uA767': // � [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER] - output[opos++] = 't'; - output[opos++] = 'h'; - break; - - case '\u02A6': // ʦ [LATIN SMALL LETTER TS DIGRAPH] - output[opos++] = 't'; - output[opos++] = 's'; - break; - - case '\uA729': // ꜩ [LATIN SMALL LETTER TZ] - output[opos++] = 't'; - output[opos++] = 'z'; - break; - - case '\u00D9': - // Ù [LATIN CAPITAL LETTER U WITH GRAVE] - case '\u00DA': - // Ú [LATIN CAPITAL LETTER U WITH ACUTE] - case '\u00DB': - // Û [LATIN CAPITAL LETTER U WITH CIRCUMFLEX] - case '\u00DC': - // Ü [LATIN CAPITAL LETTER U WITH DIAERESIS] - case '\u0168': - // Ũ [LATIN CAPITAL LETTER U WITH TILDE] - case '\u016A': - // Ū [LATIN CAPITAL LETTER U WITH MACRON] - case '\u016C': - // Ŭ [LATIN CAPITAL LETTER U WITH BREVE] - case '\u016E': - // Å® [LATIN CAPITAL LETTER U WITH RING ABOVE] - case '\u0170': - // Ű [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE] - case '\u0172': - // Ų [LATIN CAPITAL LETTER U WITH OGONEK] - case '\u01AF': - // Ư [LATIN CAPITAL LETTER U WITH HORN] - case '\u01D3': - // Ç“ [LATIN CAPITAL LETTER U WITH CARON] - case '\u01D5': - // Ç• [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON] - case '\u01D7': - // Ç— [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE] - case '\u01D9': - // Ç™ [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON] - case '\u01DB': - // Ç› [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE] - case '\u0214': - // �? [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE] - case '\u0216': - // È– [LATIN CAPITAL LETTER U WITH INVERTED BREVE] - case '\u0244': - // É„ [LATIN CAPITAL LETTER U BAR] - case '\u1D1C': - // á´œ [LATIN LETTER SMALL CAPITAL U] - case '\u1D7E': - // áµ¾ [LATIN SMALL CAPITAL LETTER U WITH STROKE] - case '\u1E72': - // á¹² [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW] - case '\u1E74': - // á¹´ [LATIN CAPITAL LETTER U WITH TILDE BELOW] - case '\u1E76': - // á¹¶ [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW] - case '\u1E78': - // Ṹ [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE] - case '\u1E7A': - // Ṻ [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS] - case '\u1EE4': - // Ụ [LATIN CAPITAL LETTER U WITH DOT BELOW] - case '\u1EE6': - // Ủ [LATIN CAPITAL LETTER U WITH HOOK ABOVE] - case '\u1EE8': - // Ứ [LATIN CAPITAL LETTER U WITH HORN AND ACUTE] - case '\u1EEA': - // Ừ [LATIN CAPITAL LETTER U WITH HORN AND GRAVE] - case '\u1EEC': - // Ử [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE] - case '\u1EEE': - // á»® [LATIN CAPITAL LETTER U WITH HORN AND TILDE] - case '\u1EF0': - // á»° [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW] - case '\u24CA': - // Ⓤ [CIRCLED LATIN CAPITAL LETTER U] - case '\uFF35': // ï¼µ [FULLWIDTH LATIN CAPITAL LETTER U] - output[opos++] = 'U'; - break; - - case '\u00F9': - // ù [LATIN SMALL LETTER U WITH GRAVE] - case '\u00FA': - // ú [LATIN SMALL LETTER U WITH ACUTE] - case '\u00FB': - // û [LATIN SMALL LETTER U WITH CIRCUMFLEX] - case '\u00FC': - // ü [LATIN SMALL LETTER U WITH DIAERESIS] - case '\u0169': - // Å© [LATIN SMALL LETTER U WITH TILDE] - case '\u016B': - // Å« [LATIN SMALL LETTER U WITH MACRON] - case '\u016D': - // Å­ [LATIN SMALL LETTER U WITH BREVE] - case '\u016F': - // ů [LATIN SMALL LETTER U WITH RING ABOVE] - case '\u0171': - // ű [LATIN SMALL LETTER U WITH DOUBLE ACUTE] - case '\u0173': - // ų [LATIN SMALL LETTER U WITH OGONEK] - case '\u01B0': - // ư [LATIN SMALL LETTER U WITH HORN] - case '\u01D4': - // �? [LATIN SMALL LETTER U WITH CARON] - case '\u01D6': - // Ç– [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON] - case '\u01D8': - // ǘ [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE] - case '\u01DA': - // Çš [LATIN SMALL LETTER U WITH DIAERESIS AND CARON] - case '\u01DC': - // Çœ [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE] - case '\u0215': - // È• [LATIN SMALL LETTER U WITH DOUBLE GRAVE] - case '\u0217': - // È— [LATIN SMALL LETTER U WITH INVERTED BREVE] - case '\u0289': - // ʉ [LATIN SMALL LETTER U BAR] - case '\u1D64': - // ᵤ [LATIN SUBSCRIPT SMALL LETTER U] - case '\u1D99': - // á¶™ [LATIN SMALL LETTER U WITH RETROFLEX HOOK] - case '\u1E73': - // á¹³ [LATIN SMALL LETTER U WITH DIAERESIS BELOW] - case '\u1E75': - // á¹µ [LATIN SMALL LETTER U WITH TILDE BELOW] - case '\u1E77': - // á¹· [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW] - case '\u1E79': - // á¹¹ [LATIN SMALL LETTER U WITH TILDE AND ACUTE] - case '\u1E7B': - // á¹» [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS] - case '\u1EE5': - // ụ [LATIN SMALL LETTER U WITH DOT BELOW] - case '\u1EE7': - // á»§ [LATIN SMALL LETTER U WITH HOOK ABOVE] - case '\u1EE9': - // ứ [LATIN SMALL LETTER U WITH HORN AND ACUTE] - case '\u1EEB': - // ừ [LATIN SMALL LETTER U WITH HORN AND GRAVE] - case '\u1EED': - // á»­ [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE] - case '\u1EEF': - // ữ [LATIN SMALL LETTER U WITH HORN AND TILDE] - case '\u1EF1': - // á»± [LATIN SMALL LETTER U WITH HORN AND DOT BELOW] - case '\u24E4': - // ⓤ [CIRCLED LATIN SMALL LETTER U] - case '\uFF55': // u [FULLWIDTH LATIN SMALL LETTER U] - output[opos++] = 'u'; - break; - - case '\u24B0': // â’° [PARENTHESIZED LATIN SMALL LETTER U] - output[opos++] = '('; - output[opos++] = 'u'; - output[opos++] = ')'; - break; - - case '\u1D6B': // ᵫ [LATIN SMALL LETTER UE] - output[opos++] = 'u'; - output[opos++] = 'e'; - break; - - case '\u01B2': - // Ʋ [LATIN CAPITAL LETTER V WITH HOOK] - case '\u0245': - // É… [LATIN CAPITAL LETTER TURNED V] - case '\u1D20': - // á´  [LATIN LETTER SMALL CAPITAL V] - case '\u1E7C': - // á¹¼ [LATIN CAPITAL LETTER V WITH TILDE] - case '\u1E7E': - // á¹¾ [LATIN CAPITAL LETTER V WITH DOT BELOW] - case '\u1EFC': - // Ỽ [LATIN CAPITAL LETTER MIDDLE-WELSH V] - case '\u24CB': - // â“‹ [CIRCLED LATIN CAPITAL LETTER V] - case '\uA75E': - // � [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE] - case '\uA768': - // � [LATIN CAPITAL LETTER VEND] - case '\uFF36': // ï¼¶ [FULLWIDTH LATIN CAPITAL LETTER V] - output[opos++] = 'V'; - break; - - case '\u028B': - // Ê‹ [LATIN SMALL LETTER V WITH HOOK] - case '\u028C': - // ÊŒ [LATIN SMALL LETTER TURNED V] - case '\u1D65': - // áµ¥ [LATIN SUBSCRIPT SMALL LETTER V] - case '\u1D8C': - // á¶Œ [LATIN SMALL LETTER V WITH PALATAL HOOK] - case '\u1E7D': - // á¹½ [LATIN SMALL LETTER V WITH TILDE] - case '\u1E7F': - // ṿ [LATIN SMALL LETTER V WITH DOT BELOW] - case '\u24E5': - // â“¥ [CIRCLED LATIN SMALL LETTER V] - case '\u2C71': - // â±± [LATIN SMALL LETTER V WITH RIGHT HOOK] - case '\u2C74': - // â±´ [LATIN SMALL LETTER V WITH CURL] - case '\uA75F': - // � [LATIN SMALL LETTER V WITH DIAGONAL STROKE] - case '\uFF56': // ï½– [FULLWIDTH LATIN SMALL LETTER V] - output[opos++] = 'v'; - break; - - case '\uA760': // � [LATIN CAPITAL LETTER VY] - output[opos++] = 'V'; - output[opos++] = 'Y'; - break; - - case '\u24B1': // â’± [PARENTHESIZED LATIN SMALL LETTER V] - output[opos++] = '('; - output[opos++] = 'v'; - output[opos++] = ')'; - break; - - case '\uA761': // � [LATIN SMALL LETTER VY] - output[opos++] = 'v'; - output[opos++] = 'y'; - break; - - case '\u0174': - // Å´ [LATIN CAPITAL LETTER W WITH CIRCUMFLEX] - case '\u01F7': - // Ç· http://en.wikipedia.org/wiki/Wynn [LATIN CAPITAL LETTER WYNN] - case '\u1D21': - // á´¡ [LATIN LETTER SMALL CAPITAL W] - case '\u1E80': - // Ẁ [LATIN CAPITAL LETTER W WITH GRAVE] - case '\u1E82': - // Ẃ [LATIN CAPITAL LETTER W WITH ACUTE] - case '\u1E84': - // Ẅ [LATIN CAPITAL LETTER W WITH DIAERESIS] - case '\u1E86': - // Ẇ [LATIN CAPITAL LETTER W WITH DOT ABOVE] - case '\u1E88': - // Ẉ [LATIN CAPITAL LETTER W WITH DOT BELOW] - case '\u24CC': - // Ⓦ [CIRCLED LATIN CAPITAL LETTER W] - case '\u2C72': - // â±² [LATIN CAPITAL LETTER W WITH HOOK] - case '\uFF37': // ï¼· [FULLWIDTH LATIN CAPITAL LETTER W] - output[opos++] = 'W'; - break; - - case '\u0175': - // ŵ [LATIN SMALL LETTER W WITH CIRCUMFLEX] - case '\u01BF': - // Æ¿ http://en.wikipedia.org/wiki/Wynn [LATIN LETTER WYNN] - case '\u028D': - // � [LATIN SMALL LETTER TURNED W] - case '\u1E81': - // � [LATIN SMALL LETTER W WITH GRAVE] - case '\u1E83': - // ẃ [LATIN SMALL LETTER W WITH ACUTE] - case '\u1E85': - // ẅ [LATIN SMALL LETTER W WITH DIAERESIS] - case '\u1E87': - // ẇ [LATIN SMALL LETTER W WITH DOT ABOVE] - case '\u1E89': - // ẉ [LATIN SMALL LETTER W WITH DOT BELOW] - case '\u1E98': - // ẘ [LATIN SMALL LETTER W WITH RING ABOVE] - case '\u24E6': - // ⓦ [CIRCLED LATIN SMALL LETTER W] - case '\u2C73': - // â±³ [LATIN SMALL LETTER W WITH HOOK] - case '\uFF57': // ï½— [FULLWIDTH LATIN SMALL LETTER W] - output[opos++] = 'w'; - break; - - case '\u24B2': // â’² [PARENTHESIZED LATIN SMALL LETTER W] - output[opos++] = '('; - output[opos++] = 'w'; - output[opos++] = ')'; - break; - - case '\u1E8A': - // Ẋ [LATIN CAPITAL LETTER X WITH DOT ABOVE] - case '\u1E8C': - // Ẍ [LATIN CAPITAL LETTER X WITH DIAERESIS] - case '\u24CD': - // � [CIRCLED LATIN CAPITAL LETTER X] - case '\uFF38': // X [FULLWIDTH LATIN CAPITAL LETTER X] - output[opos++] = 'X'; - break; - - case '\u1D8D': - // � [LATIN SMALL LETTER X WITH PALATAL HOOK] - case '\u1E8B': - // ẋ [LATIN SMALL LETTER X WITH DOT ABOVE] - case '\u1E8D': - // � [LATIN SMALL LETTER X WITH DIAERESIS] - case '\u2093': - // â‚“ [LATIN SUBSCRIPT SMALL LETTER X] - case '\u24E7': - // â“§ [CIRCLED LATIN SMALL LETTER X] - case '\uFF58': // x [FULLWIDTH LATIN SMALL LETTER X] - output[opos++] = 'x'; - break; - - case '\u24B3': // â’³ [PARENTHESIZED LATIN SMALL LETTER X] - output[opos++] = '('; - output[opos++] = 'x'; - output[opos++] = ')'; - break; - - case '\u00DD': - // � [LATIN CAPITAL LETTER Y WITH ACUTE] - case '\u0176': - // Ŷ [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX] - case '\u0178': - // Ÿ [LATIN CAPITAL LETTER Y WITH DIAERESIS] - case '\u01B3': - // Ƴ [LATIN CAPITAL LETTER Y WITH HOOK] - case '\u0232': - // Ȳ [LATIN CAPITAL LETTER Y WITH MACRON] - case '\u024E': - // ÉŽ [LATIN CAPITAL LETTER Y WITH STROKE] - case '\u028F': - // � [LATIN LETTER SMALL CAPITAL Y] - case '\u1E8E': - // Ẏ [LATIN CAPITAL LETTER Y WITH DOT ABOVE] - case '\u1EF2': - // Ỳ [LATIN CAPITAL LETTER Y WITH GRAVE] - case '\u1EF4': - // á»´ [LATIN CAPITAL LETTER Y WITH DOT BELOW] - case '\u1EF6': - // á»¶ [LATIN CAPITAL LETTER Y WITH HOOK ABOVE] - case '\u1EF8': - // Ỹ [LATIN CAPITAL LETTER Y WITH TILDE] - case '\u1EFE': - // Ỿ [LATIN CAPITAL LETTER Y WITH LOOP] - case '\u24CE': - // Ⓨ [CIRCLED LATIN CAPITAL LETTER Y] - case '\uFF39': // ï¼¹ [FULLWIDTH LATIN CAPITAL LETTER Y] - output[opos++] = 'Y'; - break; - - case '\u00FD': - // ý [LATIN SMALL LETTER Y WITH ACUTE] - case '\u00FF': - // ÿ [LATIN SMALL LETTER Y WITH DIAERESIS] - case '\u0177': - // Å· [LATIN SMALL LETTER Y WITH CIRCUMFLEX] - case '\u01B4': - // Æ´ [LATIN SMALL LETTER Y WITH HOOK] - case '\u0233': - // ȳ [LATIN SMALL LETTER Y WITH MACRON] - case '\u024F': - // � [LATIN SMALL LETTER Y WITH STROKE] - case '\u028E': - // ÊŽ [LATIN SMALL LETTER TURNED Y] - case '\u1E8F': - // � [LATIN SMALL LETTER Y WITH DOT ABOVE] - case '\u1E99': - // ẙ [LATIN SMALL LETTER Y WITH RING ABOVE] - case '\u1EF3': - // ỳ [LATIN SMALL LETTER Y WITH GRAVE] - case '\u1EF5': - // ỵ [LATIN SMALL LETTER Y WITH DOT BELOW] - case '\u1EF7': - // á»· [LATIN SMALL LETTER Y WITH HOOK ABOVE] - case '\u1EF9': - // ỹ [LATIN SMALL LETTER Y WITH TILDE] - case '\u1EFF': - // ỿ [LATIN SMALL LETTER Y WITH LOOP] - case '\u24E8': - // ⓨ [CIRCLED LATIN SMALL LETTER Y] - case '\uFF59': // ï½™ [FULLWIDTH LATIN SMALL LETTER Y] - output[opos++] = 'y'; - break; - - case '\u24B4': // â’´ [PARENTHESIZED LATIN SMALL LETTER Y] - output[opos++] = '('; - output[opos++] = 'y'; - output[opos++] = ')'; - break; - - case '\u0179': - // Ź [LATIN CAPITAL LETTER Z WITH ACUTE] - case '\u017B': - // Å» [LATIN CAPITAL LETTER Z WITH DOT ABOVE] - case '\u017D': - // Ž [LATIN CAPITAL LETTER Z WITH CARON] - case '\u01B5': - // Ƶ [LATIN CAPITAL LETTER Z WITH STROKE] - case '\u021C': - // Èœ http://en.wikipedia.org/wiki/Yogh [LATIN CAPITAL LETTER YOGH] - case '\u0224': - // Ȥ [LATIN CAPITAL LETTER Z WITH HOOK] - case '\u1D22': - // á´¢ [LATIN LETTER SMALL CAPITAL Z] - case '\u1E90': - // � [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX] - case '\u1E92': - // Ẓ [LATIN CAPITAL LETTER Z WITH DOT BELOW] - case '\u1E94': - // �? [LATIN CAPITAL LETTER Z WITH LINE BELOW] - case '\u24CF': - // � [CIRCLED LATIN CAPITAL LETTER Z] - case '\u2C6B': - // Ⱬ [LATIN CAPITAL LETTER Z WITH DESCENDER] - case '\uA762': - // � [LATIN CAPITAL LETTER VISIGOTHIC Z] - case '\uFF3A': // Z [FULLWIDTH LATIN CAPITAL LETTER Z] - output[opos++] = 'Z'; - break; - - case '\u017A': - // ź [LATIN SMALL LETTER Z WITH ACUTE] - case '\u017C': - // ż [LATIN SMALL LETTER Z WITH DOT ABOVE] - case '\u017E': - // ž [LATIN SMALL LETTER Z WITH CARON] - case '\u01B6': - // ƶ [LATIN SMALL LETTER Z WITH STROKE] - case '\u021D': - // � http://en.wikipedia.org/wiki/Yogh [LATIN SMALL LETTER YOGH] - case '\u0225': - // È¥ [LATIN SMALL LETTER Z WITH HOOK] - case '\u0240': - // É€ [LATIN SMALL LETTER Z WITH SWASH TAIL] - case '\u0290': - // � [LATIN SMALL LETTER Z WITH RETROFLEX HOOK] - case '\u0291': - // Ê‘ [LATIN SMALL LETTER Z WITH CURL] - case '\u1D76': - // áµ¶ [LATIN SMALL LETTER Z WITH MIDDLE TILDE] - case '\u1D8E': - // á¶Ž [LATIN SMALL LETTER Z WITH PALATAL HOOK] - case '\u1E91': - // ẑ [LATIN SMALL LETTER Z WITH CIRCUMFLEX] - case '\u1E93': - // ẓ [LATIN SMALL LETTER Z WITH DOT BELOW] - case '\u1E95': - // ẕ [LATIN SMALL LETTER Z WITH LINE BELOW] - case '\u24E9': - // â“© [CIRCLED LATIN SMALL LETTER Z] - case '\u2C6C': - // ⱬ [LATIN SMALL LETTER Z WITH DESCENDER] - case '\uA763': - // � [LATIN SMALL LETTER VISIGOTHIC Z] - case '\uFF5A': // z [FULLWIDTH LATIN SMALL LETTER Z] - output[opos++] = 'z'; - break; - - case '\u24B5': // â’µ [PARENTHESIZED LATIN SMALL LETTER Z] - output[opos++] = '('; - output[opos++] = 'z'; - output[opos++] = ')'; - break; - - case '\u2070': - // � [SUPERSCRIPT ZERO] - case '\u2080': - // â‚€ [SUBSCRIPT ZERO] - case '\u24EA': - // ⓪ [CIRCLED DIGIT ZERO] - case '\u24FF': - // â“¿ [NEGATIVE CIRCLED DIGIT ZERO] - case '\uFF10': // � [FULLWIDTH DIGIT ZERO] - output[opos++] = '0'; - break; - - case '\u00B9': - // ¹ [SUPERSCRIPT ONE] - case '\u2081': - // � [SUBSCRIPT ONE] - case '\u2460': - // â‘  [CIRCLED DIGIT ONE] - case '\u24F5': - // ⓵ [DOUBLE CIRCLED DIGIT ONE] - case '\u2776': - // � [DINGBAT NEGATIVE CIRCLED DIGIT ONE] - case '\u2780': - // ➀ [DINGBAT CIRCLED SANS-SERIF DIGIT ONE] - case '\u278A': - // ➊ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE] - case '\uFF11': // 1 [FULLWIDTH DIGIT ONE] - output[opos++] = '1'; - break; - - case '\u2488': // â’ˆ [DIGIT ONE FULL STOP] - output[opos++] = '1'; - output[opos++] = '.'; - break; - - case '\u2474': // â‘´ [PARENTHESIZED DIGIT ONE] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = ')'; - break; - - case '\u00B2': - // ² [SUPERSCRIPT TWO] - case '\u2082': - // â‚‚ [SUBSCRIPT TWO] - case '\u2461': - // â‘¡ [CIRCLED DIGIT TWO] - case '\u24F6': - // â“¶ [DOUBLE CIRCLED DIGIT TWO] - case '\u2777': - // � [DINGBAT NEGATIVE CIRCLED DIGIT TWO] - case '\u2781': - // � [DINGBAT CIRCLED SANS-SERIF DIGIT TWO] - case '\u278B': - // âž‹ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO] - case '\uFF12': // ï¼’ [FULLWIDTH DIGIT TWO] - output[opos++] = '2'; - break; - - case '\u2489': // â’‰ [DIGIT TWO FULL STOP] - output[opos++] = '2'; - output[opos++] = '.'; - break; - - case '\u2475': // ⑵ [PARENTHESIZED DIGIT TWO] - output[opos++] = '('; - output[opos++] = '2'; - output[opos++] = ')'; - break; - - case '\u00B3': - // ³ [SUPERSCRIPT THREE] - case '\u2083': - // ₃ [SUBSCRIPT THREE] - case '\u2462': - // â‘¢ [CIRCLED DIGIT THREE] - case '\u24F7': - // â“· [DOUBLE CIRCLED DIGIT THREE] - case '\u2778': - // � [DINGBAT NEGATIVE CIRCLED DIGIT THREE] - case '\u2782': - // âž‚ [DINGBAT CIRCLED SANS-SERIF DIGIT THREE] - case '\u278C': - // ➌ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE] - case '\uFF13': // 3 [FULLWIDTH DIGIT THREE] - output[opos++] = '3'; - break; - - case '\u248A': // â’Š [DIGIT THREE FULL STOP] - output[opos++] = '3'; - output[opos++] = '.'; - break; - - case '\u2476': // â‘¶ [PARENTHESIZED DIGIT THREE] - output[opos++] = '('; - output[opos++] = '3'; - output[opos++] = ')'; - break; - - case '\u2074': - // � [SUPERSCRIPT FOUR] - case '\u2084': - // â‚„ [SUBSCRIPT FOUR] - case '\u2463': - // â‘£ [CIRCLED DIGIT FOUR] - case '\u24F8': - // ⓸ [DOUBLE CIRCLED DIGIT FOUR] - case '\u2779': - // � [DINGBAT NEGATIVE CIRCLED DIGIT FOUR] - case '\u2783': - // ➃ [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR] - case '\u278D': - // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR] - case '\uFF14': // �? [FULLWIDTH DIGIT FOUR] - output[opos++] = '4'; - break; - - case '\u248B': // â’‹ [DIGIT FOUR FULL STOP] - output[opos++] = '4'; - output[opos++] = '.'; - break; - - case '\u2477': // â‘· [PARENTHESIZED DIGIT FOUR] - output[opos++] = '('; - output[opos++] = '4'; - output[opos++] = ')'; - break; - - case '\u2075': - // � [SUPERSCRIPT FIVE] - case '\u2085': - // â‚… [SUBSCRIPT FIVE] - case '\u2464': - // ⑤ [CIRCLED DIGIT FIVE] - case '\u24F9': - // ⓹ [DOUBLE CIRCLED DIGIT FIVE] - case '\u277A': - // � [DINGBAT NEGATIVE CIRCLED DIGIT FIVE] - case '\u2784': - // âž„ [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE] - case '\u278E': - // ➎ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE] - case '\uFF15': // 5 [FULLWIDTH DIGIT FIVE] - output[opos++] = '5'; - break; - - case '\u248C': // â’Œ [DIGIT FIVE FULL STOP] - output[opos++] = '5'; - output[opos++] = '.'; - break; - - case '\u2478': // ⑸ [PARENTHESIZED DIGIT FIVE] - output[opos++] = '('; - output[opos++] = '5'; - output[opos++] = ')'; - break; - - case '\u2076': - // � [SUPERSCRIPT SIX] - case '\u2086': - // ₆ [SUBSCRIPT SIX] - case '\u2465': - // â‘¥ [CIRCLED DIGIT SIX] - case '\u24FA': - // ⓺ [DOUBLE CIRCLED DIGIT SIX] - case '\u277B': - // � [DINGBAT NEGATIVE CIRCLED DIGIT SIX] - case '\u2785': - // âž… [DINGBAT CIRCLED SANS-SERIF DIGIT SIX] - case '\u278F': - // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX] - case '\uFF16': // ï¼– [FULLWIDTH DIGIT SIX] - output[opos++] = '6'; - break; - - case '\u248D': // â’� [DIGIT SIX FULL STOP] - output[opos++] = '6'; - output[opos++] = '.'; - break; - - case '\u2479': // ⑹ [PARENTHESIZED DIGIT SIX] - output[opos++] = '('; - output[opos++] = '6'; - output[opos++] = ')'; - break; - - case '\u2077': - // � [SUPERSCRIPT SEVEN] - case '\u2087': - // ₇ [SUBSCRIPT SEVEN] - case '\u2466': - // ⑦ [CIRCLED DIGIT SEVEN] - case '\u24FB': - // â“» [DOUBLE CIRCLED DIGIT SEVEN] - case '\u277C': - // � [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN] - case '\u2786': - // ➆ [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN] - case '\u2790': - // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN] - case '\uFF17': // ï¼— [FULLWIDTH DIGIT SEVEN] - output[opos++] = '7'; - break; - - case '\u248E': // â’Ž [DIGIT SEVEN FULL STOP] - output[opos++] = '7'; - output[opos++] = '.'; - break; - - case '\u247A': // ⑺ [PARENTHESIZED DIGIT SEVEN] - output[opos++] = '('; - output[opos++] = '7'; - output[opos++] = ')'; - break; - - case '\u2078': - // � [SUPERSCRIPT EIGHT] - case '\u2088': - // ₈ [SUBSCRIPT EIGHT] - case '\u2467': - // â‘§ [CIRCLED DIGIT EIGHT] - case '\u24FC': - // ⓼ [DOUBLE CIRCLED DIGIT EIGHT] - case '\u277D': - // � [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT] - case '\u2787': - // ➇ [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT] - case '\u2791': - // âž‘ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT] - case '\uFF18': // 8 [FULLWIDTH DIGIT EIGHT] - output[opos++] = '8'; - break; - - case '\u248F': // â’� [DIGIT EIGHT FULL STOP] - output[opos++] = '8'; - output[opos++] = '.'; - break; - - case '\u247B': // â‘» [PARENTHESIZED DIGIT EIGHT] - output[opos++] = '('; - output[opos++] = '8'; - output[opos++] = ')'; - break; - - case '\u2079': - // � [SUPERSCRIPT NINE] - case '\u2089': - // ₉ [SUBSCRIPT NINE] - case '\u2468': - // ⑨ [CIRCLED DIGIT NINE] - case '\u24FD': - // ⓽ [DOUBLE CIRCLED DIGIT NINE] - case '\u277E': - // � [DINGBAT NEGATIVE CIRCLED DIGIT NINE] - case '\u2788': - // ➈ [DINGBAT CIRCLED SANS-SERIF DIGIT NINE] - case '\u2792': - // âž’ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE] - case '\uFF19': // ï¼™ [FULLWIDTH DIGIT NINE] - output[opos++] = '9'; - break; - - case '\u2490': // â’� [DIGIT NINE FULL STOP] - output[opos++] = '9'; - output[opos++] = '.'; - break; - - case '\u247C': // ⑼ [PARENTHESIZED DIGIT NINE] - output[opos++] = '('; - output[opos++] = '9'; - output[opos++] = ')'; - break; - - case '\u2469': - // â‘© [CIRCLED NUMBER TEN] - case '\u24FE': - // ⓾ [DOUBLE CIRCLED NUMBER TEN] - case '\u277F': - // � [DINGBAT NEGATIVE CIRCLED NUMBER TEN] - case '\u2789': - // ➉ [DINGBAT CIRCLED SANS-SERIF NUMBER TEN] - case '\u2793': // âž“ [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN] - output[opos++] = '1'; - output[opos++] = '0'; - break; - - case '\u2491': // â’‘ [NUMBER TEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '0'; - output[opos++] = '.'; - break; - - case '\u247D': // ⑽ [PARENTHESIZED NUMBER TEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '0'; - output[opos++] = ')'; - break; - - case '\u246A': - // ⑪ [CIRCLED NUMBER ELEVEN] - case '\u24EB': // â“« [NEGATIVE CIRCLED NUMBER ELEVEN] - output[opos++] = '1'; - output[opos++] = '1'; - break; - - case '\u2492': // â’’ [NUMBER ELEVEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '1'; - output[opos++] = '.'; - break; - - case '\u247E': // ⑾ [PARENTHESIZED NUMBER ELEVEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '1'; - output[opos++] = ')'; - break; - - case '\u246B': - // â‘« [CIRCLED NUMBER TWELVE] - case '\u24EC': // ⓬ [NEGATIVE CIRCLED NUMBER TWELVE] - output[opos++] = '1'; - output[opos++] = '2'; - break; - - case '\u2493': // â’“ [NUMBER TWELVE FULL STOP] - output[opos++] = '1'; - output[opos++] = '2'; - output[opos++] = '.'; - break; - - case '\u247F': // â‘¿ [PARENTHESIZED NUMBER TWELVE] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '2'; - output[opos++] = ')'; - break; - - case '\u246C': - // ⑬ [CIRCLED NUMBER THIRTEEN] - case '\u24ED': // â“­ [NEGATIVE CIRCLED NUMBER THIRTEEN] - output[opos++] = '1'; - output[opos++] = '3'; - break; - - case '\u2494': // â’�? [NUMBER THIRTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '3'; - output[opos++] = '.'; - break; - - case '\u2480': // â’€ [PARENTHESIZED NUMBER THIRTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '3'; - output[opos++] = ')'; - break; - - case '\u246D': - // â‘­ [CIRCLED NUMBER FOURTEEN] - case '\u24EE': // â“® [NEGATIVE CIRCLED NUMBER FOURTEEN] - output[opos++] = '1'; - output[opos++] = '4'; - break; - - case '\u2495': // â’• [NUMBER FOURTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '4'; - output[opos++] = '.'; - break; - - case '\u2481': // â’� [PARENTHESIZED NUMBER FOURTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '4'; - output[opos++] = ')'; - break; - - case '\u246E': - // â‘® [CIRCLED NUMBER FIFTEEN] - case '\u24EF': // ⓯ [NEGATIVE CIRCLED NUMBER FIFTEEN] - output[opos++] = '1'; - output[opos++] = '5'; - break; - - case '\u2496': // â’– [NUMBER FIFTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '5'; - output[opos++] = '.'; - break; - - case '\u2482': // â’‚ [PARENTHESIZED NUMBER FIFTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '5'; - output[opos++] = ')'; - break; - - case '\u246F': - // ⑯ [CIRCLED NUMBER SIXTEEN] - case '\u24F0': // â“° [NEGATIVE CIRCLED NUMBER SIXTEEN] - output[opos++] = '1'; - output[opos++] = '6'; - break; - - case '\u2497': // â’— [NUMBER SIXTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '6'; - output[opos++] = '.'; - break; - - case '\u2483': // â’ƒ [PARENTHESIZED NUMBER SIXTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '6'; - output[opos++] = ')'; - break; - - case '\u2470': - // â‘° [CIRCLED NUMBER SEVENTEEN] - case '\u24F1': // ⓱ [NEGATIVE CIRCLED NUMBER SEVENTEEN] - output[opos++] = '1'; - output[opos++] = '7'; - break; - - case '\u2498': // â’˜ [NUMBER SEVENTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '7'; - output[opos++] = '.'; - break; - - case '\u2484': // â’„ [PARENTHESIZED NUMBER SEVENTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '7'; - output[opos++] = ')'; - break; - - case '\u2471': - // ⑱ [CIRCLED NUMBER EIGHTEEN] - case '\u24F2': // ⓲ [NEGATIVE CIRCLED NUMBER EIGHTEEN] - output[opos++] = '1'; - output[opos++] = '8'; - break; - - case '\u2499': // â’™ [NUMBER EIGHTEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '8'; - output[opos++] = '.'; - break; - - case '\u2485': // â’… [PARENTHESIZED NUMBER EIGHTEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '8'; - output[opos++] = ')'; - break; - - case '\u2472': - // ⑲ [CIRCLED NUMBER NINETEEN] - case '\u24F3': // ⓳ [NEGATIVE CIRCLED NUMBER NINETEEN] - output[opos++] = '1'; - output[opos++] = '9'; - break; - - case '\u249A': // â’š [NUMBER NINETEEN FULL STOP] - output[opos++] = '1'; - output[opos++] = '9'; - output[opos++] = '.'; - break; - - case '\u2486': // â’† [PARENTHESIZED NUMBER NINETEEN] - output[opos++] = '('; - output[opos++] = '1'; - output[opos++] = '9'; - output[opos++] = ')'; - break; - - case '\u2473': - // ⑳ [CIRCLED NUMBER TWENTY] - case '\u24F4': // â“´ [NEGATIVE CIRCLED NUMBER TWENTY] - output[opos++] = '2'; - output[opos++] = '0'; - break; - - case '\u249B': // â’› [NUMBER TWENTY FULL STOP] - output[opos++] = '2'; - output[opos++] = '0'; - output[opos++] = '.'; - break; - - case '\u2487': // â’‡ [PARENTHESIZED NUMBER TWENTY] - output[opos++] = '('; - output[opos++] = '2'; - output[opos++] = '0'; - output[opos++] = ')'; - break; - - case '\u00AB': - // « [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK] - case '\u00BB': - // » [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK] - case '\u201C': - // “ [LEFT DOUBLE QUOTATION MARK] - case '\u201D': - // � [RIGHT DOUBLE QUOTATION MARK] - case '\u201E': - // „ [DOUBLE LOW-9 QUOTATION MARK] - case '\u2033': - // ″ [DOUBLE PRIME] - case '\u2036': - // ‶ [REVERSED DOUBLE PRIME] - case '\u275D': - // � [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT] - case '\u275E': - // � [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT] - case '\u276E': - // � [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT] - case '\u276F': - // � [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT] - case '\uFF02': // " [FULLWIDTH QUOTATION MARK] - output[opos++] = '"'; - break; - - case '\u2018': - // ‘ [LEFT SINGLE QUOTATION MARK] - case '\u2019': - // ’ [RIGHT SINGLE QUOTATION MARK] - case '\u201A': - // ‚ [SINGLE LOW-9 QUOTATION MARK] - case '\u201B': - // ‛ [SINGLE HIGH-REVERSED-9 QUOTATION MARK] - case '\u2032': - // ′ [PRIME] - case '\u2035': - // ‵ [REVERSED PRIME] - case '\u2039': - // ‹ [SINGLE LEFT-POINTING ANGLE QUOTATION MARK] - case '\u203A': - // › [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK] - case '\u275B': - // � [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT] - case '\u275C': - // � [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT] - case '\uFF07': // ' [FULLWIDTH APOSTROPHE] - output[opos++] = '\''; - break; - - case '\u2010': - // � [HYPHEN] - case '\u2011': - // ‑ [NON-BREAKING HYPHEN] - case '\u2012': - // ‒ [FIGURE DASH] - case '\u2013': - // – [EN DASH] - case '\u2014': - // �? [EM DASH] - case '\u207B': - // � [SUPERSCRIPT MINUS] - case '\u208B': - // â‚‹ [SUBSCRIPT MINUS] - case '\uFF0D': // � [FULLWIDTH HYPHEN-MINUS] - output[opos++] = '-'; - break; - - case '\u2045': - // � [LEFT SQUARE BRACKET WITH QUILL] - case '\u2772': - // � [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT] - case '\uFF3B': // ï¼» [FULLWIDTH LEFT SQUARE BRACKET] - output[opos++] = '['; - break; - - case '\u2046': - // � [RIGHT SQUARE BRACKET WITH QUILL] - case '\u2773': - // � [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT] - case '\uFF3D': // ï¼½ [FULLWIDTH RIGHT SQUARE BRACKET] - output[opos++] = ']'; - break; - - case '\u207D': - // � [SUPERSCRIPT LEFT PARENTHESIS] - case '\u208D': - // � [SUBSCRIPT LEFT PARENTHESIS] - case '\u2768': - // � [MEDIUM LEFT PARENTHESIS ORNAMENT] - case '\u276A': - // � [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT] - case '\uFF08': // ( [FULLWIDTH LEFT PARENTHESIS] - output[opos++] = '('; - break; - - case '\u2E28': // ⸨ [LEFT DOUBLE PARENTHESIS] - output[opos++] = '('; - output[opos++] = '('; - break; - - case '\u207E': - // � [SUPERSCRIPT RIGHT PARENTHESIS] - case '\u208E': - // ₎ [SUBSCRIPT RIGHT PARENTHESIS] - case '\u2769': - // � [MEDIUM RIGHT PARENTHESIS ORNAMENT] - case '\u276B': - // � [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT] - case '\uFF09': // ) [FULLWIDTH RIGHT PARENTHESIS] - output[opos++] = ')'; - break; - - case '\u2E29': // ⸩ [RIGHT DOUBLE PARENTHESIS] - output[opos++] = ')'; - output[opos++] = ')'; - break; - - case '\u276C': - // � [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT] - case '\u2770': - // � [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT] - case '\uFF1C': // < [FULLWIDTH LESS-THAN SIGN] - output[opos++] = '<'; - break; - - case '\u276D': - // � [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT] - case '\u2771': - // � [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT] - case '\uFF1E': // > [FULLWIDTH GREATER-THAN SIGN] - output[opos++] = '>'; - break; - - case '\u2774': - // � [MEDIUM LEFT CURLY BRACKET ORNAMENT] - case '\uFF5B': // ï½› [FULLWIDTH LEFT CURLY BRACKET] - output[opos++] = '{'; - break; - - case '\u2775': - // � [MEDIUM RIGHT CURLY BRACKET ORNAMENT] - case '\uFF5D': // � [FULLWIDTH RIGHT CURLY BRACKET] - output[opos++] = '}'; - break; - - case '\u207A': - // � [SUPERSCRIPT PLUS SIGN] - case '\u208A': - // ₊ [SUBSCRIPT PLUS SIGN] - case '\uFF0B': // + [FULLWIDTH PLUS SIGN] - output[opos++] = '+'; - break; - - case '\u207C': - // � [SUPERSCRIPT EQUALS SIGN] - case '\u208C': - // ₌ [SUBSCRIPT EQUALS SIGN] - case '\uFF1D': // � [FULLWIDTH EQUALS SIGN] - output[opos++] = '='; - break; - - case '\uFF01': // � [FULLWIDTH EXCLAMATION MARK] - output[opos++] = '!'; - break; - - case '\u203C': // ‼ [DOUBLE EXCLAMATION MARK] - output[opos++] = '!'; - output[opos++] = '!'; - break; - - case '\u2049': // � [EXCLAMATION QUESTION MARK] - output[opos++] = '!'; - output[opos++] = '?'; - break; - - case '\uFF03': // # [FULLWIDTH NUMBER SIGN] - output[opos++] = '#'; - break; - - case '\uFF04': // $ [FULLWIDTH DOLLAR SIGN] - output[opos++] = '$'; - break; - - case '\u2052': - // � [COMMERCIAL MINUS SIGN] - case '\uFF05': // ï¼… [FULLWIDTH PERCENT SIGN] - output[opos++] = '%'; - break; - - case '\uFF06': // & [FULLWIDTH AMPERSAND] - output[opos++] = '&'; - break; - - case '\u204E': - // � [LOW ASTERISK] - case '\uFF0A': // * [FULLWIDTH ASTERISK] - output[opos++] = '*'; - break; - - case '\uFF0C': // , [FULLWIDTH COMMA] - output[opos++] = ','; - break; - - case '\uFF0E': // . [FULLWIDTH FULL STOP] - output[opos++] = '.'; - break; - - case '\u2044': - // � [FRACTION SLASH] - case '\uFF0F': // � [FULLWIDTH SOLIDUS] - output[opos++] = '/'; - break; - - case '\uFF1A': // : [FULLWIDTH COLON] - output[opos++] = ':'; - break; - - case '\u204F': - // � [REVERSED SEMICOLON] - case '\uFF1B': // ï¼› [FULLWIDTH SEMICOLON] - output[opos++] = ';'; - break; - - case '\uFF1F': // ? [FULLWIDTH QUESTION MARK] - output[opos++] = '?'; - break; - - case '\u2047': // � [DOUBLE QUESTION MARK] - output[opos++] = '?'; - output[opos++] = '?'; - break; - - case '\u2048': // � [QUESTION EXCLAMATION MARK] - output[opos++] = '?'; - output[opos++] = '!'; - break; - - case '\uFF20': // ï¼  [FULLWIDTH COMMERCIAL AT] - output[opos++] = '@'; - break; - - case '\uFF3C': // ï¼¼ [FULLWIDTH REVERSE SOLIDUS] - output[opos++] = '\\'; - break; - - case '\u2038': - // ‸ [CARET] - case '\uFF3E': // ï¼¾ [FULLWIDTH CIRCUMFLEX ACCENT] - output[opos++] = '^'; - break; - - case '\uFF3F': // _ [FULLWIDTH LOW LINE] - output[opos++] = '_'; - break; - - case '\u2053': - // � [SWUNG DASH] - case '\uFF5E': // ~ [FULLWIDTH TILDE] - output[opos++] = '~'; - break; - - // BEGIN CUSTOM TRANSLITERATION OF CYRILIC CHARS - - #region Cyrillic chars - - // russian uppercase "А Б В Г Д Е Ё Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я" - // russian lowercase "а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я" - - // notes - // read http://www.vesic.org/english/blog/c-sharp/transliteration-easy-way-microsoft-transliteration-utility/ - // should we look into MS Transliteration Utility (http://msdn.microsoft.com/en-US/goglobal/bb688104.aspx) - // also UnicodeSharpFork https://bitbucket.org/DimaStefantsov/unidecodesharpfork - // also Transliterator http://transliterator.codeplex.com/ - // - // in any case it would be good to generate all those "case" statements instead of writing them by hand - // time for a T4 template? - // also we should support extensibility so ppl can register more cases in external code - - // TODO: transliterates Анастасия as Anastasiya, and not Anastasia - // Ольга --> Ol'ga, Татьяна --> Tat'yana -- that's bad (?) - // Note: should ä (German umlaut) become a or ae ? - - case '\u0410': // А - output[opos++] = 'A'; - break; - case '\u0430': // а - output[opos++] = 'a'; - break; - case '\u0411': // Б - output[opos++] = 'B'; - break; - case '\u0431': // б - output[opos++] = 'b'; - break; - case '\u0412': // В - output[opos++] = 'V'; - break; - case '\u0432': // в - output[opos++] = 'v'; - break; - case '\u0413': // Г - output[opos++] = 'G'; - break; - case '\u0433': // г - output[opos++] = 'g'; - break; - case '\u0414': // Д - output[opos++] = 'D'; - break; - case '\u0434': // д - output[opos++] = 'd'; - break; - case '\u0415': // Е - output[opos++] = 'E'; - break; - case '\u0435': // е - output[opos++] = 'e'; - break; - case '\u0401': // Ё - output[opos++] = 'E'; // alt. Yo - break; - case '\u0451': // ё - output[opos++] = 'e'; // alt. yo - break; - case '\u0416': // Ж - output[opos++] = 'Z'; - output[opos++] = 'h'; - break; - case '\u0436': // ж - output[opos++] = 'z'; - output[opos++] = 'h'; - break; - case '\u0417': // З - output[opos++] = 'Z'; - break; - case '\u0437': // з - output[opos++] = 'z'; - break; - case '\u0418': // И - output[opos++] = 'I'; - break; - case '\u0438': // и - output[opos++] = 'i'; - break; - case '\u0419': // Й - output[opos++] = 'I'; // alt. Y, J - break; - case '\u0439': // й - output[opos++] = 'i'; // alt. y, j - break; - case '\u041A': // К - output[opos++] = 'K'; - break; - case '\u043A': // к - output[opos++] = 'k'; - break; - case '\u041B': // Л - output[opos++] = 'L'; - break; - case '\u043B': // л - output[opos++] = 'l'; - break; - case '\u041C': // М - output[opos++] = 'M'; - break; - case '\u043C': // м - output[opos++] = 'm'; - break; - case '\u041D': // Н - output[opos++] = 'N'; - break; - case '\u043D': // н - output[opos++] = 'n'; - break; - case '\u041E': // О - output[opos++] = 'O'; - break; - case '\u043E': // о - output[opos++] = 'o'; - break; - case '\u041F': // П - output[opos++] = 'P'; - break; - case '\u043F': // п - output[opos++] = 'p'; - break; - case '\u0420': // Р - output[opos++] = 'R'; - break; - case '\u0440': // р - output[opos++] = 'r'; - break; - case '\u0421': // С - output[opos++] = 'S'; - break; - case '\u0441': // с - output[opos++] = 's'; - break; - case '\u0422': // Т - output[opos++] = 'T'; - break; - case '\u0442': // т - output[opos++] = 't'; - break; - case '\u0423': // У - output[opos++] = 'U'; - break; - case '\u0443': // у - output[opos++] = 'u'; - break; - case '\u0424': // Ф - output[opos++] = 'F'; - break; - case '\u0444': // ф - output[opos++] = 'f'; - break; - case '\u0425': // Х - output[opos++] = 'K'; // alt. X - output[opos++] = 'h'; - break; - case '\u0445': // х - output[opos++] = 'k'; // alt. x - output[opos++] = 'h'; - break; - case '\u0426': // Ц - output[opos++] = 'F'; - break; - case '\u0446': // ц - output[opos++] = 'f'; - break; - case '\u0427': // Ч - output[opos++] = 'C'; // alt. Ts, C - output[opos++] = 'h'; - break; - case '\u0447': // ч - output[opos++] = 'c'; // alt. ts, c - output[opos++] = 'h'; - break; - case '\u0428': // Ш - output[opos++] = 'S'; // alt. Ch, S - output[opos++] = 'h'; - break; - case '\u0448': // ш - output[opos++] = 's'; // alt. ch, s - output[opos++] = 'h'; - break; - case '\u0429': // Щ - output[opos++] = 'S'; // alt. Shch, Sc - output[opos++] = 'h'; - break; - case '\u0449': // щ - output[opos++] = 's'; // alt. shch, sc - output[opos++] = 'h'; - break; - case '\u042A': // Ъ - output[opos++] = '"'; // " - break; - case '\u044A': // ъ - output[opos++] = '"'; // " - break; - case '\u042B': // Ы - output[opos++] = 'Y'; - break; - case '\u044B': // ы - output[opos++] = 'y'; - break; - case '\u042C': // Ь - output[opos++] = '\''; // ' - break; - case '\u044C': // ь - output[opos++] = '\''; // ' - break; - case '\u042D': // Э - output[opos++] = 'E'; - break; - case '\u044D': // э - output[opos++] = 'e'; - break; - case '\u042E': // Ю - output[opos++] = 'Y'; // alt. Ju - output[opos++] = 'u'; - break; - case '\u044E': // ю - output[opos++] = 'y'; // alt. ju - output[opos++] = 'u'; - break; - case '\u042F': // Я - output[opos++] = 'Y'; // alt. Ja - output[opos++] = 'a'; - break; - case '\u044F': // я - output[opos++] = 'y'; // alt. ja - output[opos++] = 'a'; - break; - - #endregion - - // BEGIN EXTRA - /* - case '£': - output[opos++] = 'G'; - output[opos++] = 'B'; - output[opos++] = 'P'; - break; - - case '€': - output[opos++] = 'E'; - output[opos++] = 'U'; - output[opos++] = 'R'; - break; - - case '©': - output[opos++] = '('; - output[opos++] = 'C'; - output[opos++] = ')'; - break; - */ - default: - //if (ToMoreAscii(input, ipos, output, ref opos)) - // break; - - //if (!char.IsLetterOrDigit(c)) // that would not catch eg 汉 unfortunately - // output[opos++] = '?'; - //else - // output[opos++] = c; - - // strict ASCII - output[opos++] = fail; - - break; - } + ToAscii(input, ipos, output, ref opos, fail); } } - //private static bool ToMoreAscii(char[] input, int ipos, char[] output, ref int opos) - //{ - // var c = input[ipos]; - - // switch (c) - // { - // case '£': - // output[opos++] = 'G'; - // output[opos++] = 'B'; - // output[opos++] = 'P'; - // break; - - // case '€': - // output[opos++] = 'E'; - // output[opos++] = 'U'; - // output[opos++] = 'R'; - // break; - - // case '©': - // output[opos++] = '('; - // output[opos++] = 'C'; - // output[opos++] = ')'; - // break; - - // default: - // return false; - // } - - // return true; - //} + return opos; } + + // private static void ToAscii(char[] input, StringBuilder output) + // { + // var chars = new char[5]; + + // for (var ipos = 0; ipos < input.Length; ipos++) + // { + // var opos = 0; + // if (char.IsSurrogate(input[ipos])) + // ipos++; + // else + // { + // ToAscii(input, ipos, chars, ref opos); + // output.Append(chars, 0, opos); + // } + // } + // } + + /// + /// Converts the character at position in input array of Utf8 characters + /// + /// and writes the converted value to output array of Ascii characters at position + /// , + /// and increments that position accordingly. + /// + /// The input array. + /// The input position. + /// The output array. + /// The output position. + /// The character to use to replace characters that cannot properly be converted. + /// + /// Adapted from various sources on the 'net including Lucene.Net.Analysis.ASCIIFoldingFilter. + /// Input should contain Utf8 characters exclusively and NOT Unicode. + /// Removes controls, normalizes whitespaces, replaces symbols by '?'. + /// + private static void ToAscii(char[] input, int ipos, char[] output, ref int opos, char fail = '?') + { + var c = input[ipos]; + + if (char.IsControl(c)) + { + // Control characters are non-printing and formatting characters, such as ACK, BEL, CR, FF, LF, and VT. + // The Unicode standard assigns the following code points to control characters: from \U0000 to \U001F, + // \U007F, and from \U0080 to \U009F. According to the Unicode standard, these values are to be + // interpreted as control characters unless their use is otherwise defined by an application. Valid + // control characters are members of the UnicodeCategory.Control category. + + // we don't want them + } + + // else if (char.IsSeparator(c)) + // { + // // The Unicode standard recognizes three subcategories of separators: + // // - Space separators (the UnicodeCategory.SpaceSeparator category), which includes characters such as \u0020. + // // - Line separators (the UnicodeCategory.LineSeparator category), which includes \u2028. + // // - Paragraph separators (the UnicodeCategory.ParagraphSeparator category), which includes \u2029. + // // + // // Note: The Unicode standard classifies the characters \u000A (LF), \u000C (FF), and \u000A (CR) as control + // // characters (members of the UnicodeCategory.Control category), not as separator characters. + + // // better do it via WhiteSpace + // } + else if (char.IsWhiteSpace(c)) + { + // White space characters are the following Unicode characters: + // - Members of the SpaceSeparator category, which includes the characters SPACE (U+0020), + // OGHAM SPACE MARK (U+1680), MONGOLIAN VOWEL SEPARATOR (U+180E), EN QUAD (U+2000), EM QUAD (U+2001), + // EN SPACE (U+2002), EM SPACE (U+2003), THREE-PER-EM SPACE (U+2004), FOUR-PER-EM SPACE (U+2005), + // SIX-PER-EM SPACE (U+2006), FIGURE SPACE (U+2007), PUNCTUATION SPACE (U+2008), THIN SPACE (U+2009), + // HAIR SPACE (U+200A), NARROW NO-BREAK SPACE (U+202F), MEDIUM MATHEMATICAL SPACE (U+205F), + // and IDEOGRAPHIC SPACE (U+3000). + // - Members of the LineSeparator category, which consists solely of the LINE SEPARATOR character (U+2028). + // - Members of the ParagraphSeparator category, which consists solely of the PARAGRAPH SEPARATOR character (U+2029). + // - The characters CHARACTER TABULATION (U+0009), LINE FEED (U+000A), LINE TABULATION (U+000B), + // FORM FEED (U+000C), CARRIAGE RETURN (U+000D), NEXT LINE (U+0085), and NO-BREAK SPACE (U+00A0). + + // make it a whitespace + output[opos++] = ' '; + } + else if (c < '\u0080') + { + // safe + output[opos++] = c; + } + else + { + switch (c) + { + case '\u00C0': + // À [LATIN CAPITAL LETTER A WITH GRAVE] + case '\u00C1': + // � [LATIN CAPITAL LETTER A WITH ACUTE] + case '\u00C2': + //  [LATIN CAPITAL LETTER A WITH CIRCUMFLEX] + case '\u00C3': + // à [LATIN CAPITAL LETTER A WITH TILDE] + case '\u00C4': + // Ä [LATIN CAPITAL LETTER A WITH DIAERESIS] + case '\u00C5': + // Ã… [LATIN CAPITAL LETTER A WITH RING ABOVE] + case '\u0100': + // Ä€ [LATIN CAPITAL LETTER A WITH MACRON] + case '\u0102': + // Ä‚ [LATIN CAPITAL LETTER A WITH BREVE] + case '\u0104': + // Ä„ [LATIN CAPITAL LETTER A WITH OGONEK] + case '\u018F': + // � http://en.wikipedia.org/wiki/Schwa [LATIN CAPITAL LETTER SCHWA] + case '\u01CD': + // � [LATIN CAPITAL LETTER A WITH CARON] + case '\u01DE': + // Çž [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON] + case '\u01E0': + // Ç  [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON] + case '\u01FA': + // Ǻ [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE] + case '\u0200': + // È€ [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE] + case '\u0202': + // È‚ [LATIN CAPITAL LETTER A WITH INVERTED BREVE] + case '\u0226': + // Ȧ [LATIN CAPITAL LETTER A WITH DOT ABOVE] + case '\u023A': + // Ⱥ [LATIN CAPITAL LETTER A WITH STROKE] + case '\u1D00': + // á´€ [LATIN LETTER SMALL CAPITAL A] + case '\u1E00': + // Ḁ [LATIN CAPITAL LETTER A WITH RING BELOW] + case '\u1EA0': + // Ạ [LATIN CAPITAL LETTER A WITH DOT BELOW] + case '\u1EA2': + // Ả [LATIN CAPITAL LETTER A WITH HOOK ABOVE] + case '\u1EA4': + // Ấ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE] + case '\u1EA6': + // Ầ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE] + case '\u1EA8': + // Ẩ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EAA': + // Ẫ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE] + case '\u1EAC': + // Ậ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EAE': + // Ắ [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE] + case '\u1EB0': + // Ằ [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE] + case '\u1EB2': + // Ẳ [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE] + case '\u1EB4': + // Ẵ [LATIN CAPITAL LETTER A WITH BREVE AND TILDE] + case '\u1EB6': + // Ặ [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW] + case '\u24B6': + // â’¶ [CIRCLED LATIN CAPITAL LETTER A] + case '\uFF21': // A [FULLWIDTH LATIN CAPITAL LETTER A] + output[opos++] = 'A'; + break; + + case '\u00E0': + // à [LATIN SMALL LETTER A WITH GRAVE] + case '\u00E1': + // á [LATIN SMALL LETTER A WITH ACUTE] + case '\u00E2': + // â [LATIN SMALL LETTER A WITH CIRCUMFLEX] + case '\u00E3': + // ã [LATIN SMALL LETTER A WITH TILDE] + case '\u00E4': + // ä [LATIN SMALL LETTER A WITH DIAERESIS] + case '\u00E5': + // Ã¥ [LATIN SMALL LETTER A WITH RING ABOVE] + case '\u0101': + // � [LATIN SMALL LETTER A WITH MACRON] + case '\u0103': + // ă [LATIN SMALL LETTER A WITH BREVE] + case '\u0105': + // Ä… [LATIN SMALL LETTER A WITH OGONEK] + case '\u01CE': + // ÇŽ [LATIN SMALL LETTER A WITH CARON] + case '\u01DF': + // ÇŸ [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON] + case '\u01E1': + // Ç¡ [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON] + case '\u01FB': + // Ç» [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE] + case '\u0201': + // � [LATIN SMALL LETTER A WITH DOUBLE GRAVE] + case '\u0203': + // ȃ [LATIN SMALL LETTER A WITH INVERTED BREVE] + case '\u0227': + // ȧ [LATIN SMALL LETTER A WITH DOT ABOVE] + case '\u0250': + // � [LATIN SMALL LETTER TURNED A] + case '\u0259': + // É™ [LATIN SMALL LETTER SCHWA] + case '\u025A': + // Éš [LATIN SMALL LETTER SCHWA WITH HOOK] + case '\u1D8F': + // � [LATIN SMALL LETTER A WITH RETROFLEX HOOK] + case '\u1D95': + // á¶• [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK] + case '\u1E01': + // ạ [LATIN SMALL LETTER A WITH RING BELOW] + case '\u1E9A': + // ả [LATIN SMALL LETTER A WITH RIGHT HALF RING] + case '\u1EA1': + // ạ [LATIN SMALL LETTER A WITH DOT BELOW] + case '\u1EA3': + // ả [LATIN SMALL LETTER A WITH HOOK ABOVE] + case '\u1EA5': + // ấ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE] + case '\u1EA7': + // ầ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE] + case '\u1EA9': + // ẩ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EAB': + // ẫ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE] + case '\u1EAD': + // ậ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EAF': + // ắ [LATIN SMALL LETTER A WITH BREVE AND ACUTE] + case '\u1EB1': + // ằ [LATIN SMALL LETTER A WITH BREVE AND GRAVE] + case '\u1EB3': + // ẳ [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE] + case '\u1EB5': + // ẵ [LATIN SMALL LETTER A WITH BREVE AND TILDE] + case '\u1EB7': + // ặ [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW] + case '\u2090': + // � [LATIN SUBSCRIPT SMALL LETTER A] + case '\u2094': + // �? [LATIN SUBSCRIPT SMALL LETTER SCHWA] + case '\u24D0': + // � [CIRCLED LATIN SMALL LETTER A] + case '\u2C65': + // â±¥ [LATIN SMALL LETTER A WITH STROKE] + case '\u2C6F': + // Ɐ [LATIN CAPITAL LETTER TURNED A] + case '\uFF41': // � [FULLWIDTH LATIN SMALL LETTER A] + output[opos++] = 'a'; + break; + + case '\uA732': // Ꜳ [LATIN CAPITAL LETTER AA] + output[opos++] = 'A'; + output[opos++] = 'A'; + break; + + case '\u00C6': + // Æ [LATIN CAPITAL LETTER AE] + case '\u01E2': + // Ç¢ [LATIN CAPITAL LETTER AE WITH MACRON] + case '\u01FC': + // Ǽ [LATIN CAPITAL LETTER AE WITH ACUTE] + case '\u1D01': // á´� [LATIN LETTER SMALL CAPITAL AE] + output[opos++] = 'A'; + output[opos++] = 'E'; + break; + + case '\uA734': // Ꜵ [LATIN CAPITAL LETTER AO] + output[opos++] = 'A'; + output[opos++] = 'O'; + break; + + case '\uA736': // Ꜷ [LATIN CAPITAL LETTER AU] + output[opos++] = 'A'; + output[opos++] = 'U'; + break; + + case '\uA738': + // Ꜹ [LATIN CAPITAL LETTER AV] + case '\uA73A': // Ꜻ [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR] + output[opos++] = 'A'; + output[opos++] = 'V'; + break; + + case '\uA73C': // Ꜽ [LATIN CAPITAL LETTER AY] + output[opos++] = 'A'; + output[opos++] = 'Y'; + break; + + case '\u249C': // â’œ [PARENTHESIZED LATIN SMALL LETTER A] + output[opos++] = '('; + output[opos++] = 'a'; + output[opos++] = ')'; + break; + + case '\uA733': // ꜳ [LATIN SMALL LETTER AA] + output[opos++] = 'a'; + output[opos++] = 'a'; + break; + + case '\u00E6': + // æ [LATIN SMALL LETTER AE] + case '\u01E3': + // Ç£ [LATIN SMALL LETTER AE WITH MACRON] + case '\u01FD': + // ǽ [LATIN SMALL LETTER AE WITH ACUTE] + case '\u1D02': // á´‚ [LATIN SMALL LETTER TURNED AE] + output[opos++] = 'a'; + output[opos++] = 'e'; + break; + + case '\uA735': // ꜵ [LATIN SMALL LETTER AO] + output[opos++] = 'a'; + output[opos++] = 'o'; + break; + + case '\uA737': // ꜷ [LATIN SMALL LETTER AU] + output[opos++] = 'a'; + output[opos++] = 'u'; + break; + + case '\uA739': + // ꜹ [LATIN SMALL LETTER AV] + case '\uA73B': // ꜻ [LATIN SMALL LETTER AV WITH HORIZONTAL BAR] + output[opos++] = 'a'; + output[opos++] = 'v'; + break; + + case '\uA73D': // ꜽ [LATIN SMALL LETTER AY] + output[opos++] = 'a'; + output[opos++] = 'y'; + break; + + case '\u0181': + // � [LATIN CAPITAL LETTER B WITH HOOK] + case '\u0182': + // Æ‚ [LATIN CAPITAL LETTER B WITH TOPBAR] + case '\u0243': + // Ƀ [LATIN CAPITAL LETTER B WITH STROKE] + case '\u0299': + // Ê™ [LATIN LETTER SMALL CAPITAL B] + case '\u1D03': + // á´ƒ [LATIN LETTER SMALL CAPITAL BARRED B] + case '\u1E02': + // Ḃ [LATIN CAPITAL LETTER B WITH DOT ABOVE] + case '\u1E04': + // Ḅ [LATIN CAPITAL LETTER B WITH DOT BELOW] + case '\u1E06': + // Ḇ [LATIN CAPITAL LETTER B WITH LINE BELOW] + case '\u24B7': + // â’· [CIRCLED LATIN CAPITAL LETTER B] + case '\uFF22': // ï¼¢ [FULLWIDTH LATIN CAPITAL LETTER B] + output[opos++] = 'B'; + break; + + case '\u0180': + // Æ€ [LATIN SMALL LETTER B WITH STROKE] + case '\u0183': + // ƃ [LATIN SMALL LETTER B WITH TOPBAR] + case '\u0253': + // É“ [LATIN SMALL LETTER B WITH HOOK] + case '\u1D6C': + // ᵬ [LATIN SMALL LETTER B WITH MIDDLE TILDE] + case '\u1D80': + // á¶€ [LATIN SMALL LETTER B WITH PALATAL HOOK] + case '\u1E03': + // ḃ [LATIN SMALL LETTER B WITH DOT ABOVE] + case '\u1E05': + // ḅ [LATIN SMALL LETTER B WITH DOT BELOW] + case '\u1E07': + // ḇ [LATIN SMALL LETTER B WITH LINE BELOW] + case '\u24D1': + // â“‘ [CIRCLED LATIN SMALL LETTER B] + case '\uFF42': // b [FULLWIDTH LATIN SMALL LETTER B] + output[opos++] = 'b'; + break; + + case '\u249D': // â’� [PARENTHESIZED LATIN SMALL LETTER B] + output[opos++] = '('; + output[opos++] = 'b'; + output[opos++] = ')'; + break; + + case '\u00C7': + // Ç [LATIN CAPITAL LETTER C WITH CEDILLA] + case '\u0106': + // Ć [LATIN CAPITAL LETTER C WITH ACUTE] + case '\u0108': + // Ĉ [LATIN CAPITAL LETTER C WITH CIRCUMFLEX] + case '\u010A': + // ÄŠ [LATIN CAPITAL LETTER C WITH DOT ABOVE] + case '\u010C': + // ÄŒ [LATIN CAPITAL LETTER C WITH CARON] + case '\u0187': + // Ƈ [LATIN CAPITAL LETTER C WITH HOOK] + case '\u023B': + // È» [LATIN CAPITAL LETTER C WITH STROKE] + case '\u0297': + // Ê— [LATIN LETTER STRETCHED C] + case '\u1D04': + // á´„ [LATIN LETTER SMALL CAPITAL C] + case '\u1E08': + // Ḉ [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE] + case '\u24B8': + // â’¸ [CIRCLED LATIN CAPITAL LETTER C] + case '\uFF23': // ï¼£ [FULLWIDTH LATIN CAPITAL LETTER C] + output[opos++] = 'C'; + break; + + case '\u00E7': + // ç [LATIN SMALL LETTER C WITH CEDILLA] + case '\u0107': + // ć [LATIN SMALL LETTER C WITH ACUTE] + case '\u0109': + // ĉ [LATIN SMALL LETTER C WITH CIRCUMFLEX] + case '\u010B': + // Ä‹ [LATIN SMALL LETTER C WITH DOT ABOVE] + case '\u010D': + // � [LATIN SMALL LETTER C WITH CARON] + case '\u0188': + // ƈ [LATIN SMALL LETTER C WITH HOOK] + case '\u023C': + // ȼ [LATIN SMALL LETTER C WITH STROKE] + case '\u0255': + // É• [LATIN SMALL LETTER C WITH CURL] + case '\u1E09': + // ḉ [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE] + case '\u2184': + // ↄ [LATIN SMALL LETTER REVERSED C] + case '\u24D2': + // â“’ [CIRCLED LATIN SMALL LETTER C] + case '\uA73E': + // Ꜿ [LATIN CAPITAL LETTER REVERSED C WITH DOT] + case '\uA73F': + // ꜿ [LATIN SMALL LETTER REVERSED C WITH DOT] + case '\uFF43': // c [FULLWIDTH LATIN SMALL LETTER C] + output[opos++] = 'c'; + break; + + case '\u249E': // â’ž [PARENTHESIZED LATIN SMALL LETTER C] + output[opos++] = '('; + output[opos++] = 'c'; + output[opos++] = ')'; + break; + + case '\u00D0': + // � [LATIN CAPITAL LETTER ETH] + case '\u010E': + // ÄŽ [LATIN CAPITAL LETTER D WITH CARON] + case '\u0110': + // � [LATIN CAPITAL LETTER D WITH STROKE] + case '\u0189': + // Ɖ [LATIN CAPITAL LETTER AFRICAN D] + case '\u018A': + // ÆŠ [LATIN CAPITAL LETTER D WITH HOOK] + case '\u018B': + // Æ‹ [LATIN CAPITAL LETTER D WITH TOPBAR] + case '\u1D05': + // á´… [LATIN LETTER SMALL CAPITAL D] + case '\u1D06': + // á´† [LATIN LETTER SMALL CAPITAL ETH] + case '\u1E0A': + // Ḋ [LATIN CAPITAL LETTER D WITH DOT ABOVE] + case '\u1E0C': + // Ḍ [LATIN CAPITAL LETTER D WITH DOT BELOW] + case '\u1E0E': + // Ḏ [LATIN CAPITAL LETTER D WITH LINE BELOW] + case '\u1E10': + // � [LATIN CAPITAL LETTER D WITH CEDILLA] + case '\u1E12': + // Ḓ [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW] + case '\u24B9': + // â’¹ [CIRCLED LATIN CAPITAL LETTER D] + case '\uA779': + // � [LATIN CAPITAL LETTER INSULAR D] + case '\uFF24': // D [FULLWIDTH LATIN CAPITAL LETTER D] + output[opos++] = 'D'; + break; + + case '\u00F0': + // ð [LATIN SMALL LETTER ETH] + case '\u010F': + // � [LATIN SMALL LETTER D WITH CARON] + case '\u0111': + // Ä‘ [LATIN SMALL LETTER D WITH STROKE] + case '\u018C': + // ÆŒ [LATIN SMALL LETTER D WITH TOPBAR] + case '\u0221': + // È¡ [LATIN SMALL LETTER D WITH CURL] + case '\u0256': + // É– [LATIN SMALL LETTER D WITH TAIL] + case '\u0257': + // É— [LATIN SMALL LETTER D WITH HOOK] + case '\u1D6D': + // áµ­ [LATIN SMALL LETTER D WITH MIDDLE TILDE] + case '\u1D81': + // � [LATIN SMALL LETTER D WITH PALATAL HOOK] + case '\u1D91': + // á¶‘ [LATIN SMALL LETTER D WITH HOOK AND TAIL] + case '\u1E0B': + // ḋ [LATIN SMALL LETTER D WITH DOT ABOVE] + case '\u1E0D': + // � [LATIN SMALL LETTER D WITH DOT BELOW] + case '\u1E0F': + // � [LATIN SMALL LETTER D WITH LINE BELOW] + case '\u1E11': + // ḑ [LATIN SMALL LETTER D WITH CEDILLA] + case '\u1E13': + // ḓ [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW] + case '\u24D3': + // â““ [CIRCLED LATIN SMALL LETTER D] + case '\uA77A': + // � [LATIN SMALL LETTER INSULAR D] + case '\uFF44': // d [FULLWIDTH LATIN SMALL LETTER D] + output[opos++] = 'd'; + break; + + case '\u01C4': + // Ç„ [LATIN CAPITAL LETTER DZ WITH CARON] + case '\u01F1': // DZ [LATIN CAPITAL LETTER DZ] + output[opos++] = 'D'; + output[opos++] = 'Z'; + break; + + case '\u01C5': + // Ç… [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON] + case '\u01F2': // Dz [LATIN CAPITAL LETTER D WITH SMALL LETTER Z] + output[opos++] = 'D'; + output[opos++] = 'z'; + break; + + case '\u249F': // â’Ÿ [PARENTHESIZED LATIN SMALL LETTER D] + output[opos++] = '('; + output[opos++] = 'd'; + output[opos++] = ')'; + break; + + case '\u0238': // ȸ [LATIN SMALL LETTER DB DIGRAPH] + output[opos++] = 'd'; + output[opos++] = 'b'; + break; + + case '\u01C6': + // dž [LATIN SMALL LETTER DZ WITH CARON] + case '\u01F3': + // dz [LATIN SMALL LETTER DZ] + case '\u02A3': + // Ê£ [LATIN SMALL LETTER DZ DIGRAPH] + case '\u02A5': // Ê¥ [LATIN SMALL LETTER DZ DIGRAPH WITH CURL] + output[opos++] = 'd'; + output[opos++] = 'z'; + break; + + case '\u00C8': + // È [LATIN CAPITAL LETTER E WITH GRAVE] + case '\u00C9': + // É [LATIN CAPITAL LETTER E WITH ACUTE] + case '\u00CA': + // Ê [LATIN CAPITAL LETTER E WITH CIRCUMFLEX] + case '\u00CB': + // Ë [LATIN CAPITAL LETTER E WITH DIAERESIS] + case '\u0112': + // Ä’ [LATIN CAPITAL LETTER E WITH MACRON] + case '\u0114': + // �? [LATIN CAPITAL LETTER E WITH BREVE] + case '\u0116': + // Ä– [LATIN CAPITAL LETTER E WITH DOT ABOVE] + case '\u0118': + // Ę [LATIN CAPITAL LETTER E WITH OGONEK] + case '\u011A': + // Äš [LATIN CAPITAL LETTER E WITH CARON] + case '\u018E': + // ÆŽ [LATIN CAPITAL LETTER REVERSED E] + case '\u0190': + // � [LATIN CAPITAL LETTER OPEN E] + case '\u0204': + // È„ [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE] + case '\u0206': + // Ȇ [LATIN CAPITAL LETTER E WITH INVERTED BREVE] + case '\u0228': + // Ȩ [LATIN CAPITAL LETTER E WITH CEDILLA] + case '\u0246': + // Ɇ [LATIN CAPITAL LETTER E WITH STROKE] + case '\u1D07': + // á´‡ [LATIN LETTER SMALL CAPITAL E] + case '\u1E14': + // �? [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE] + case '\u1E16': + // Ḗ [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE] + case '\u1E18': + // Ḙ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW] + case '\u1E1A': + // Ḛ [LATIN CAPITAL LETTER E WITH TILDE BELOW] + case '\u1E1C': + // Ḝ [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE] + case '\u1EB8': + // Ẹ [LATIN CAPITAL LETTER E WITH DOT BELOW] + case '\u1EBA': + // Ẻ [LATIN CAPITAL LETTER E WITH HOOK ABOVE] + case '\u1EBC': + // Ẽ [LATIN CAPITAL LETTER E WITH TILDE] + case '\u1EBE': + // Ế [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE] + case '\u1EC0': + // Ề [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE] + case '\u1EC2': + // Ể [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EC4': + // Ễ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE] + case '\u1EC6': + // Ệ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW] + case '\u24BA': + // â’º [CIRCLED LATIN CAPITAL LETTER E] + case '\u2C7B': + // â±» [LATIN LETTER SMALL CAPITAL TURNED E] + case '\uFF25': // ï¼¥ [FULLWIDTH LATIN CAPITAL LETTER E] + output[opos++] = 'E'; + break; + + case '\u00E8': + // è [LATIN SMALL LETTER E WITH GRAVE] + case '\u00E9': + // é [LATIN SMALL LETTER E WITH ACUTE] + case '\u00EA': + // ê [LATIN SMALL LETTER E WITH CIRCUMFLEX] + case '\u00EB': + // ë [LATIN SMALL LETTER E WITH DIAERESIS] + case '\u0113': + // Ä“ [LATIN SMALL LETTER E WITH MACRON] + case '\u0115': + // Ä• [LATIN SMALL LETTER E WITH BREVE] + case '\u0117': + // Ä— [LATIN SMALL LETTER E WITH DOT ABOVE] + case '\u0119': + // Ä™ [LATIN SMALL LETTER E WITH OGONEK] + case '\u011B': + // Ä› [LATIN SMALL LETTER E WITH CARON] + case '\u01DD': + // � [LATIN SMALL LETTER TURNED E] + case '\u0205': + // È… [LATIN SMALL LETTER E WITH DOUBLE GRAVE] + case '\u0207': + // ȇ [LATIN SMALL LETTER E WITH INVERTED BREVE] + case '\u0229': + // È© [LATIN SMALL LETTER E WITH CEDILLA] + case '\u0247': + // ɇ [LATIN SMALL LETTER E WITH STROKE] + case '\u0258': + // ɘ [LATIN SMALL LETTER REVERSED E] + case '\u025B': + // É› [LATIN SMALL LETTER OPEN E] + case '\u025C': + // Éœ [LATIN SMALL LETTER REVERSED OPEN E] + case '\u025D': + // � [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK] + case '\u025E': + // Éž [LATIN SMALL LETTER CLOSED REVERSED OPEN E] + case '\u029A': + // Êš [LATIN SMALL LETTER CLOSED OPEN E] + case '\u1D08': + // á´ˆ [LATIN SMALL LETTER TURNED OPEN E] + case '\u1D92': + // á¶’ [LATIN SMALL LETTER E WITH RETROFLEX HOOK] + case '\u1D93': + // á¶“ [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK] + case '\u1D94': + // �? [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK] + case '\u1E15': + // ḕ [LATIN SMALL LETTER E WITH MACRON AND GRAVE] + case '\u1E17': + // ḗ [LATIN SMALL LETTER E WITH MACRON AND ACUTE] + case '\u1E19': + // ḙ [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW] + case '\u1E1B': + // ḛ [LATIN SMALL LETTER E WITH TILDE BELOW] + case '\u1E1D': + // � [LATIN SMALL LETTER E WITH CEDILLA AND BREVE] + case '\u1EB9': + // ẹ [LATIN SMALL LETTER E WITH DOT BELOW] + case '\u1EBB': + // ẻ [LATIN SMALL LETTER E WITH HOOK ABOVE] + case '\u1EBD': + // ẽ [LATIN SMALL LETTER E WITH TILDE] + case '\u1EBF': + // ế [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE] + case '\u1EC1': + // � [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE] + case '\u1EC3': + // ể [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1EC5': + // á»… [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE] + case '\u1EC7': + // ệ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW] + case '\u2091': + // â‚‘ [LATIN SUBSCRIPT SMALL LETTER E] + case '\u24D4': + // �? [CIRCLED LATIN SMALL LETTER E] + case '\u2C78': + // ⱸ [LATIN SMALL LETTER E WITH NOTCH] + case '\uFF45': // ï½… [FULLWIDTH LATIN SMALL LETTER E] + output[opos++] = 'e'; + break; + + case '\u24A0': // â’  [PARENTHESIZED LATIN SMALL LETTER E] + output[opos++] = '('; + output[opos++] = 'e'; + output[opos++] = ')'; + break; + + case '\u0191': + // Æ‘ [LATIN CAPITAL LETTER F WITH HOOK] + case '\u1E1E': + // Ḟ [LATIN CAPITAL LETTER F WITH DOT ABOVE] + case '\u24BB': + // â’» [CIRCLED LATIN CAPITAL LETTER F] + case '\uA730': + // ꜰ [LATIN LETTER SMALL CAPITAL F] + case '\uA77B': + // � [LATIN CAPITAL LETTER INSULAR F] + case '\uA7FB': + // ꟻ [LATIN EPIGRAPHIC LETTER REVERSED F] + case '\uFF26': // F [FULLWIDTH LATIN CAPITAL LETTER F] + output[opos++] = 'F'; + break; + + case '\u0192': + // Æ’ [LATIN SMALL LETTER F WITH HOOK] + case '\u1D6E': + // áµ® [LATIN SMALL LETTER F WITH MIDDLE TILDE] + case '\u1D82': + // á¶‚ [LATIN SMALL LETTER F WITH PALATAL HOOK] + case '\u1E1F': + // ḟ [LATIN SMALL LETTER F WITH DOT ABOVE] + case '\u1E9B': + // ẛ [LATIN SMALL LETTER LONG S WITH DOT ABOVE] + case '\u24D5': + // â“• [CIRCLED LATIN SMALL LETTER F] + case '\uA77C': + // � [LATIN SMALL LETTER INSULAR F] + case '\uFF46': // f [FULLWIDTH LATIN SMALL LETTER F] + output[opos++] = 'f'; + break; + + case '\u24A1': // â’¡ [PARENTHESIZED LATIN SMALL LETTER F] + output[opos++] = '('; + output[opos++] = 'f'; + output[opos++] = ')'; + break; + + case '\uFB00': // ff [LATIN SMALL LIGATURE FF] + output[opos++] = 'f'; + output[opos++] = 'f'; + break; + + case '\uFB03': // ffi [LATIN SMALL LIGATURE FFI] + output[opos++] = 'f'; + output[opos++] = 'f'; + output[opos++] = 'i'; + break; + + case '\uFB04': // ffl [LATIN SMALL LIGATURE FFL] + output[opos++] = 'f'; + output[opos++] = 'f'; + output[opos++] = 'l'; + break; + + case '\uFB01': // � [LATIN SMALL LIGATURE FI] + output[opos++] = 'f'; + output[opos++] = 'i'; + break; + + case '\uFB02': // fl [LATIN SMALL LIGATURE FL] + output[opos++] = 'f'; + output[opos++] = 'l'; + break; + + case '\u011C': + // Äœ [LATIN CAPITAL LETTER G WITH CIRCUMFLEX] + case '\u011E': + // Äž [LATIN CAPITAL LETTER G WITH BREVE] + case '\u0120': + // Ä  [LATIN CAPITAL LETTER G WITH DOT ABOVE] + case '\u0122': + // Ä¢ [LATIN CAPITAL LETTER G WITH CEDILLA] + case '\u0193': + // Æ“ [LATIN CAPITAL LETTER G WITH HOOK] + case '\u01E4': + // Ǥ [LATIN CAPITAL LETTER G WITH STROKE] + case '\u01E5': + // Ç¥ [LATIN SMALL LETTER G WITH STROKE] + case '\u01E6': + // Ǧ [LATIN CAPITAL LETTER G WITH CARON] + case '\u01E7': + // ǧ [LATIN SMALL LETTER G WITH CARON] + case '\u01F4': + // Ç´ [LATIN CAPITAL LETTER G WITH ACUTE] + case '\u0262': + // É¢ [LATIN LETTER SMALL CAPITAL G] + case '\u029B': + // Ê› [LATIN LETTER SMALL CAPITAL G WITH HOOK] + case '\u1E20': + // Ḡ [LATIN CAPITAL LETTER G WITH MACRON] + case '\u24BC': + // â’¼ [CIRCLED LATIN CAPITAL LETTER G] + case '\uA77D': + // � [LATIN CAPITAL LETTER INSULAR G] + case '\uA77E': + // � [LATIN CAPITAL LETTER TURNED INSULAR G] + case '\uFF27': // ï¼§ [FULLWIDTH LATIN CAPITAL LETTER G] + output[opos++] = 'G'; + break; + + case '\u011D': + // � [LATIN SMALL LETTER G WITH CIRCUMFLEX] + case '\u011F': + // ÄŸ [LATIN SMALL LETTER G WITH BREVE] + case '\u0121': + // Ä¡ [LATIN SMALL LETTER G WITH DOT ABOVE] + case '\u0123': + // Ä£ [LATIN SMALL LETTER G WITH CEDILLA] + case '\u01F5': + // ǵ [LATIN SMALL LETTER G WITH ACUTE] + case '\u0260': + // É  [LATIN SMALL LETTER G WITH HOOK] + case '\u0261': + // É¡ [LATIN SMALL LETTER SCRIPT G] + case '\u1D77': + // áµ· [LATIN SMALL LETTER TURNED G] + case '\u1D79': + // áµ¹ [LATIN SMALL LETTER INSULAR G] + case '\u1D83': + // ᶃ [LATIN SMALL LETTER G WITH PALATAL HOOK] + case '\u1E21': + // ḡ [LATIN SMALL LETTER G WITH MACRON] + case '\u24D6': + // â“– [CIRCLED LATIN SMALL LETTER G] + case '\uA77F': + // � [LATIN SMALL LETTER TURNED INSULAR G] + case '\uFF47': // g [FULLWIDTH LATIN SMALL LETTER G] + output[opos++] = 'g'; + break; + + case '\u24A2': // â’¢ [PARENTHESIZED LATIN SMALL LETTER G] + output[opos++] = '('; + output[opos++] = 'g'; + output[opos++] = ')'; + break; + + case '\u0124': + // Ĥ [LATIN CAPITAL LETTER H WITH CIRCUMFLEX] + case '\u0126': + // Ħ [LATIN CAPITAL LETTER H WITH STROKE] + case '\u021E': + // Èž [LATIN CAPITAL LETTER H WITH CARON] + case '\u029C': + // Êœ [LATIN LETTER SMALL CAPITAL H] + case '\u1E22': + // Ḣ [LATIN CAPITAL LETTER H WITH DOT ABOVE] + case '\u1E24': + // Ḥ [LATIN CAPITAL LETTER H WITH DOT BELOW] + case '\u1E26': + // Ḧ [LATIN CAPITAL LETTER H WITH DIAERESIS] + case '\u1E28': + // Ḩ [LATIN CAPITAL LETTER H WITH CEDILLA] + case '\u1E2A': + // Ḫ [LATIN CAPITAL LETTER H WITH BREVE BELOW] + case '\u24BD': + // â’½ [CIRCLED LATIN CAPITAL LETTER H] + case '\u2C67': + // â±§ [LATIN CAPITAL LETTER H WITH DESCENDER] + case '\u2C75': + // â±µ [LATIN CAPITAL LETTER HALF H] + case '\uFF28': // H [FULLWIDTH LATIN CAPITAL LETTER H] + output[opos++] = 'H'; + break; + + case '\u0125': + // Ä¥ [LATIN SMALL LETTER H WITH CIRCUMFLEX] + case '\u0127': + // ħ [LATIN SMALL LETTER H WITH STROKE] + case '\u021F': + // ÈŸ [LATIN SMALL LETTER H WITH CARON] + case '\u0265': + // É¥ [LATIN SMALL LETTER TURNED H] + case '\u0266': + // ɦ [LATIN SMALL LETTER H WITH HOOK] + case '\u02AE': + // Ê® [LATIN SMALL LETTER TURNED H WITH FISHHOOK] + case '\u02AF': + // ʯ [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL] + case '\u1E23': + // ḣ [LATIN SMALL LETTER H WITH DOT ABOVE] + case '\u1E25': + // ḥ [LATIN SMALL LETTER H WITH DOT BELOW] + case '\u1E27': + // ḧ [LATIN SMALL LETTER H WITH DIAERESIS] + case '\u1E29': + // ḩ [LATIN SMALL LETTER H WITH CEDILLA] + case '\u1E2B': + // ḫ [LATIN SMALL LETTER H WITH BREVE BELOW] + case '\u1E96': + // ẖ [LATIN SMALL LETTER H WITH LINE BELOW] + case '\u24D7': + // â“— [CIRCLED LATIN SMALL LETTER H] + case '\u2C68': + // ⱨ [LATIN SMALL LETTER H WITH DESCENDER] + case '\u2C76': + // â±¶ [LATIN SMALL LETTER HALF H] + case '\uFF48': // h [FULLWIDTH LATIN SMALL LETTER H] + output[opos++] = 'h'; + break; + + case '\u01F6': // Ƕ http://en.wikipedia.org/wiki/Hwair [LATIN CAPITAL LETTER HWAIR] + output[opos++] = 'H'; + output[opos++] = 'V'; + break; + + case '\u24A3': // â’£ [PARENTHESIZED LATIN SMALL LETTER H] + output[opos++] = '('; + output[opos++] = 'h'; + output[opos++] = ')'; + break; + + case '\u0195': // Æ• [LATIN SMALL LETTER HV] + output[opos++] = 'h'; + output[opos++] = 'v'; + break; + + case '\u00CC': + // ÃŒ [LATIN CAPITAL LETTER I WITH GRAVE] + case '\u00CD': + // � [LATIN CAPITAL LETTER I WITH ACUTE] + case '\u00CE': + // ÃŽ [LATIN CAPITAL LETTER I WITH CIRCUMFLEX] + case '\u00CF': + // � [LATIN CAPITAL LETTER I WITH DIAERESIS] + case '\u0128': + // Ĩ [LATIN CAPITAL LETTER I WITH TILDE] + case '\u012A': + // Ī [LATIN CAPITAL LETTER I WITH MACRON] + case '\u012C': + // Ĭ [LATIN CAPITAL LETTER I WITH BREVE] + case '\u012E': + // Ä® [LATIN CAPITAL LETTER I WITH OGONEK] + case '\u0130': + // İ [LATIN CAPITAL LETTER I WITH DOT ABOVE] + case '\u0196': + // Æ– [LATIN CAPITAL LETTER IOTA] + case '\u0197': + // Æ— [LATIN CAPITAL LETTER I WITH STROKE] + case '\u01CF': + // � [LATIN CAPITAL LETTER I WITH CARON] + case '\u0208': + // Ȉ [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE] + case '\u020A': + // ÈŠ [LATIN CAPITAL LETTER I WITH INVERTED BREVE] + case '\u026A': + // ɪ [LATIN LETTER SMALL CAPITAL I] + case '\u1D7B': + // áµ» [LATIN SMALL CAPITAL LETTER I WITH STROKE] + case '\u1E2C': + // Ḭ [LATIN CAPITAL LETTER I WITH TILDE BELOW] + case '\u1E2E': + // Ḯ [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE] + case '\u1EC8': + // Ỉ [LATIN CAPITAL LETTER I WITH HOOK ABOVE] + case '\u1ECA': + // Ị [LATIN CAPITAL LETTER I WITH DOT BELOW] + case '\u24BE': + // â’¾ [CIRCLED LATIN CAPITAL LETTER I] + case '\uA7FE': + // ꟾ [LATIN EPIGRAPHIC LETTER I LONGA] + case '\uFF29': // I [FULLWIDTH LATIN CAPITAL LETTER I] + output[opos++] = 'I'; + break; + + case '\u00EC': + // ì [LATIN SMALL LETTER I WITH GRAVE] + case '\u00ED': + // í [LATIN SMALL LETTER I WITH ACUTE] + case '\u00EE': + // î [LATIN SMALL LETTER I WITH CIRCUMFLEX] + case '\u00EF': + // ï [LATIN SMALL LETTER I WITH DIAERESIS] + case '\u0129': + // Ä© [LATIN SMALL LETTER I WITH TILDE] + case '\u012B': + // Ä« [LATIN SMALL LETTER I WITH MACRON] + case '\u012D': + // Ä­ [LATIN SMALL LETTER I WITH BREVE] + case '\u012F': + // į [LATIN SMALL LETTER I WITH OGONEK] + case '\u0131': + // ı [LATIN SMALL LETTER DOTLESS I] + case '\u01D0': + // � [LATIN SMALL LETTER I WITH CARON] + case '\u0209': + // ȉ [LATIN SMALL LETTER I WITH DOUBLE GRAVE] + case '\u020B': + // È‹ [LATIN SMALL LETTER I WITH INVERTED BREVE] + case '\u0268': + // ɨ [LATIN SMALL LETTER I WITH STROKE] + case '\u1D09': + // á´‰ [LATIN SMALL LETTER TURNED I] + case '\u1D62': + // áµ¢ [LATIN SUBSCRIPT SMALL LETTER I] + case '\u1D7C': + // áµ¼ [LATIN SMALL LETTER IOTA WITH STROKE] + case '\u1D96': + // á¶– [LATIN SMALL LETTER I WITH RETROFLEX HOOK] + case '\u1E2D': + // ḭ [LATIN SMALL LETTER I WITH TILDE BELOW] + case '\u1E2F': + // ḯ [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE] + case '\u1EC9': + // ỉ [LATIN SMALL LETTER I WITH HOOK ABOVE] + case '\u1ECB': + // ị [LATIN SMALL LETTER I WITH DOT BELOW] + case '\u2071': + // � [SUPERSCRIPT LATIN SMALL LETTER I] + case '\u24D8': + // ⓘ [CIRCLED LATIN SMALL LETTER I] + case '\uFF49': // i [FULLWIDTH LATIN SMALL LETTER I] + output[opos++] = 'i'; + break; + + case '\u0132': // IJ [LATIN CAPITAL LIGATURE IJ] + output[opos++] = 'I'; + output[opos++] = 'J'; + break; + + case '\u24A4': // â’¤ [PARENTHESIZED LATIN SMALL LETTER I] + output[opos++] = '('; + output[opos++] = 'i'; + output[opos++] = ')'; + break; + + case '\u0133': // ij [LATIN SMALL LIGATURE IJ] + output[opos++] = 'i'; + output[opos++] = 'j'; + break; + + case '\u0134': + // Ä´ [LATIN CAPITAL LETTER J WITH CIRCUMFLEX] + case '\u0248': + // Ɉ [LATIN CAPITAL LETTER J WITH STROKE] + case '\u1D0A': + // á´Š [LATIN LETTER SMALL CAPITAL J] + case '\u24BF': + // â’¿ [CIRCLED LATIN CAPITAL LETTER J] + case '\uFF2A': // J [FULLWIDTH LATIN CAPITAL LETTER J] + output[opos++] = 'J'; + break; + + case '\u0135': + // ĵ [LATIN SMALL LETTER J WITH CIRCUMFLEX] + case '\u01F0': + // ǰ [LATIN SMALL LETTER J WITH CARON] + case '\u0237': + // È· [LATIN SMALL LETTER DOTLESS J] + case '\u0249': + // ɉ [LATIN SMALL LETTER J WITH STROKE] + case '\u025F': + // ÉŸ [LATIN SMALL LETTER DOTLESS J WITH STROKE] + case '\u0284': + // Ê„ [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK] + case '\u029D': + // � [LATIN SMALL LETTER J WITH CROSSED-TAIL] + case '\u24D9': + // â“™ [CIRCLED LATIN SMALL LETTER J] + case '\u2C7C': + // â±¼ [LATIN SUBSCRIPT SMALL LETTER J] + case '\uFF4A': // j [FULLWIDTH LATIN SMALL LETTER J] + output[opos++] = 'j'; + break; + + case '\u24A5': // â’¥ [PARENTHESIZED LATIN SMALL LETTER J] + output[opos++] = '('; + output[opos++] = 'j'; + output[opos++] = ')'; + break; + + case '\u0136': + // Ķ [LATIN CAPITAL LETTER K WITH CEDILLA] + case '\u0198': + // Ƙ [LATIN CAPITAL LETTER K WITH HOOK] + case '\u01E8': + // Ǩ [LATIN CAPITAL LETTER K WITH CARON] + case '\u1D0B': + // á´‹ [LATIN LETTER SMALL CAPITAL K] + case '\u1E30': + // Ḱ [LATIN CAPITAL LETTER K WITH ACUTE] + case '\u1E32': + // Ḳ [LATIN CAPITAL LETTER K WITH DOT BELOW] + case '\u1E34': + // Ḵ [LATIN CAPITAL LETTER K WITH LINE BELOW] + case '\u24C0': + // â“€ [CIRCLED LATIN CAPITAL LETTER K] + case '\u2C69': + // Ⱪ [LATIN CAPITAL LETTER K WITH DESCENDER] + case '\uA740': + // � [LATIN CAPITAL LETTER K WITH STROKE] + case '\uA742': + // � [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE] + case '\uA744': + // � [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE] + case '\uFF2B': // K [FULLWIDTH LATIN CAPITAL LETTER K] + output[opos++] = 'K'; + break; + + case '\u0137': + // Ä· [LATIN SMALL LETTER K WITH CEDILLA] + case '\u0199': + // Æ™ [LATIN SMALL LETTER K WITH HOOK] + case '\u01E9': + // Ç© [LATIN SMALL LETTER K WITH CARON] + case '\u029E': + // Êž [LATIN SMALL LETTER TURNED K] + case '\u1D84': + // á¶„ [LATIN SMALL LETTER K WITH PALATAL HOOK] + case '\u1E31': + // ḱ [LATIN SMALL LETTER K WITH ACUTE] + case '\u1E33': + // ḳ [LATIN SMALL LETTER K WITH DOT BELOW] + case '\u1E35': + // ḵ [LATIN SMALL LETTER K WITH LINE BELOW] + case '\u24DA': + // ⓚ [CIRCLED LATIN SMALL LETTER K] + case '\u2C6A': + // ⱪ [LATIN SMALL LETTER K WITH DESCENDER] + case '\uA741': + // � [LATIN SMALL LETTER K WITH STROKE] + case '\uA743': + // � [LATIN SMALL LETTER K WITH DIAGONAL STROKE] + case '\uA745': + // � [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE] + case '\uFF4B': // k [FULLWIDTH LATIN SMALL LETTER K] + output[opos++] = 'k'; + break; + + case '\u24A6': // â’¦ [PARENTHESIZED LATIN SMALL LETTER K] + output[opos++] = '('; + output[opos++] = 'k'; + output[opos++] = ')'; + break; + + case '\u0139': + // Ĺ [LATIN CAPITAL LETTER L WITH ACUTE] + case '\u013B': + // Ä» [LATIN CAPITAL LETTER L WITH CEDILLA] + case '\u013D': + // Ľ [LATIN CAPITAL LETTER L WITH CARON] + case '\u013F': + // Ä¿ [LATIN CAPITAL LETTER L WITH MIDDLE DOT] + case '\u0141': + // � [LATIN CAPITAL LETTER L WITH STROKE] + case '\u023D': + // Ƚ [LATIN CAPITAL LETTER L WITH BAR] + case '\u029F': + // ÊŸ [LATIN LETTER SMALL CAPITAL L] + case '\u1D0C': + // á´Œ [LATIN LETTER SMALL CAPITAL L WITH STROKE] + case '\u1E36': + // Ḷ [LATIN CAPITAL LETTER L WITH DOT BELOW] + case '\u1E38': + // Ḹ [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON] + case '\u1E3A': + // Ḻ [LATIN CAPITAL LETTER L WITH LINE BELOW] + case '\u1E3C': + // Ḽ [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW] + case '\u24C1': + // � [CIRCLED LATIN CAPITAL LETTER L] + case '\u2C60': + // â±  [LATIN CAPITAL LETTER L WITH DOUBLE BAR] + case '\u2C62': + // â±¢ [LATIN CAPITAL LETTER L WITH MIDDLE TILDE] + case '\uA746': + // � [LATIN CAPITAL LETTER BROKEN L] + case '\uA748': + // � [LATIN CAPITAL LETTER L WITH HIGH STROKE] + case '\uA780': + // Ꞁ [LATIN CAPITAL LETTER TURNED L] + case '\uFF2C': // L [FULLWIDTH LATIN CAPITAL LETTER L] + output[opos++] = 'L'; + break; + + case '\u013A': + // ĺ [LATIN SMALL LETTER L WITH ACUTE] + case '\u013C': + // ļ [LATIN SMALL LETTER L WITH CEDILLA] + case '\u013E': + // ľ [LATIN SMALL LETTER L WITH CARON] + case '\u0140': + // Å€ [LATIN SMALL LETTER L WITH MIDDLE DOT] + case '\u0142': + // Å‚ [LATIN SMALL LETTER L WITH STROKE] + case '\u019A': + // Æš [LATIN SMALL LETTER L WITH BAR] + case '\u0234': + // È´ [LATIN SMALL LETTER L WITH CURL] + case '\u026B': + // É« [LATIN SMALL LETTER L WITH MIDDLE TILDE] + case '\u026C': + // ɬ [LATIN SMALL LETTER L WITH BELT] + case '\u026D': + // É­ [LATIN SMALL LETTER L WITH RETROFLEX HOOK] + case '\u1D85': + // á¶… [LATIN SMALL LETTER L WITH PALATAL HOOK] + case '\u1E37': + // ḷ [LATIN SMALL LETTER L WITH DOT BELOW] + case '\u1E39': + // ḹ [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON] + case '\u1E3B': + // ḻ [LATIN SMALL LETTER L WITH LINE BELOW] + case '\u1E3D': + // ḽ [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW] + case '\u24DB': + // â“› [CIRCLED LATIN SMALL LETTER L] + case '\u2C61': + // ⱡ [LATIN SMALL LETTER L WITH DOUBLE BAR] + case '\uA747': + // � [LATIN SMALL LETTER BROKEN L] + case '\uA749': + // � [LATIN SMALL LETTER L WITH HIGH STROKE] + case '\uA781': + // � [LATIN SMALL LETTER TURNED L] + case '\uFF4C': // l [FULLWIDTH LATIN SMALL LETTER L] + output[opos++] = 'l'; + break; + + case '\u01C7': // LJ [LATIN CAPITAL LETTER LJ] + output[opos++] = 'L'; + output[opos++] = 'J'; + break; + + case '\u1EFA': // Ỻ [LATIN CAPITAL LETTER MIDDLE-WELSH LL] + output[opos++] = 'L'; + output[opos++] = 'L'; + break; + + case '\u01C8': // Lj [LATIN CAPITAL LETTER L WITH SMALL LETTER J] + output[opos++] = 'L'; + output[opos++] = 'j'; + break; + + case '\u24A7': // â’§ [PARENTHESIZED LATIN SMALL LETTER L] + output[opos++] = '('; + output[opos++] = 'l'; + output[opos++] = ')'; + break; + + case '\u01C9': // lj [LATIN SMALL LETTER LJ] + output[opos++] = 'l'; + output[opos++] = 'j'; + break; + + case '\u1EFB': // á»» [LATIN SMALL LETTER MIDDLE-WELSH LL] + output[opos++] = 'l'; + output[opos++] = 'l'; + break; + + case '\u02AA': // ʪ [LATIN SMALL LETTER LS DIGRAPH] + output[opos++] = 'l'; + output[opos++] = 's'; + break; + + case '\u02AB': // Ê« [LATIN SMALL LETTER LZ DIGRAPH] + output[opos++] = 'l'; + output[opos++] = 'z'; + break; + + case '\u019C': + // Æœ [LATIN CAPITAL LETTER TURNED M] + case '\u1D0D': + // á´� [LATIN LETTER SMALL CAPITAL M] + case '\u1E3E': + // Ḿ [LATIN CAPITAL LETTER M WITH ACUTE] + case '\u1E40': + // á¹€ [LATIN CAPITAL LETTER M WITH DOT ABOVE] + case '\u1E42': + // Ṃ [LATIN CAPITAL LETTER M WITH DOT BELOW] + case '\u24C2': + // â“‚ [CIRCLED LATIN CAPITAL LETTER M] + case '\u2C6E': + // â±® [LATIN CAPITAL LETTER M WITH HOOK] + case '\uA7FD': + // ꟽ [LATIN EPIGRAPHIC LETTER INVERTED M] + case '\uA7FF': + // ꟿ [LATIN EPIGRAPHIC LETTER ARCHAIC M] + case '\uFF2D': // ï¼­ [FULLWIDTH LATIN CAPITAL LETTER M] + output[opos++] = 'M'; + break; + + case '\u026F': + // ɯ [LATIN SMALL LETTER TURNED M] + case '\u0270': + // ɰ [LATIN SMALL LETTER TURNED M WITH LONG LEG] + case '\u0271': + // ɱ [LATIN SMALL LETTER M WITH HOOK] + case '\u1D6F': + // ᵯ [LATIN SMALL LETTER M WITH MIDDLE TILDE] + case '\u1D86': + // ᶆ [LATIN SMALL LETTER M WITH PALATAL HOOK] + case '\u1E3F': + // ḿ [LATIN SMALL LETTER M WITH ACUTE] + case '\u1E41': + // � [LATIN SMALL LETTER M WITH DOT ABOVE] + case '\u1E43': + // ṃ [LATIN SMALL LETTER M WITH DOT BELOW] + case '\u24DC': + // ⓜ [CIRCLED LATIN SMALL LETTER M] + case '\uFF4D': // � [FULLWIDTH LATIN SMALL LETTER M] + output[opos++] = 'm'; + break; + + case '\u24A8': // â’¨ [PARENTHESIZED LATIN SMALL LETTER M] + output[opos++] = '('; + output[opos++] = 'm'; + output[opos++] = ')'; + break; + + case '\u00D1': + // Ñ [LATIN CAPITAL LETTER N WITH TILDE] + case '\u0143': + // Ã…Æ’ [LATIN CAPITAL LETTER N WITH ACUTE] + case '\u0145': + // Å… [LATIN CAPITAL LETTER N WITH CEDILLA] + case '\u0147': + // Ň [LATIN CAPITAL LETTER N WITH CARON] + case '\u014A': + // Ã…Å  http://en.wikipedia.org/wiki/Eng_(letter) [LATIN CAPITAL LETTER ENG] + case '\u019D': + // � [LATIN CAPITAL LETTER N WITH LEFT HOOK] + case '\u01F8': + // Ǹ [LATIN CAPITAL LETTER N WITH GRAVE] + case '\u0220': + // È  [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG] + case '\u0274': + // É´ [LATIN LETTER SMALL CAPITAL N] + case '\u1D0E': + // á´Ž [LATIN LETTER SMALL CAPITAL REVERSED N] + case '\u1E44': + // Ṅ [LATIN CAPITAL LETTER N WITH DOT ABOVE] + case '\u1E46': + // Ṇ [LATIN CAPITAL LETTER N WITH DOT BELOW] + case '\u1E48': + // Ṉ [LATIN CAPITAL LETTER N WITH LINE BELOW] + case '\u1E4A': + // Ṋ [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW] + case '\u24C3': + // Ⓝ [CIRCLED LATIN CAPITAL LETTER N] + case '\uFF2E': // ï¼® [FULLWIDTH LATIN CAPITAL LETTER N] + output[opos++] = 'N'; + break; + + case '\u00F1': + // ñ [LATIN SMALL LETTER N WITH TILDE] + case '\u0144': + // Å„ [LATIN SMALL LETTER N WITH ACUTE] + case '\u0146': + // ņ [LATIN SMALL LETTER N WITH CEDILLA] + case '\u0148': + // ň [LATIN SMALL LETTER N WITH CARON] + case '\u0149': + // ʼn [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE] + case '\u014B': + // Å‹ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN SMALL LETTER ENG] + case '\u019E': + // Æž [LATIN SMALL LETTER N WITH LONG RIGHT LEG] + case '\u01F9': + // ǹ [LATIN SMALL LETTER N WITH GRAVE] + case '\u0235': + // ȵ [LATIN SMALL LETTER N WITH CURL] + case '\u0272': + // ɲ [LATIN SMALL LETTER N WITH LEFT HOOK] + case '\u0273': + // ɳ [LATIN SMALL LETTER N WITH RETROFLEX HOOK] + case '\u1D70': + // áµ° [LATIN SMALL LETTER N WITH MIDDLE TILDE] + case '\u1D87': + // ᶇ [LATIN SMALL LETTER N WITH PALATAL HOOK] + case '\u1E45': + // á¹… [LATIN SMALL LETTER N WITH DOT ABOVE] + case '\u1E47': + // ṇ [LATIN SMALL LETTER N WITH DOT BELOW] + case '\u1E49': + // ṉ [LATIN SMALL LETTER N WITH LINE BELOW] + case '\u1E4B': + // ṋ [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW] + case '\u207F': + // � [SUPERSCRIPT LATIN SMALL LETTER N] + case '\u24DD': + // � [CIRCLED LATIN SMALL LETTER N] + case '\uFF4E': // n [FULLWIDTH LATIN SMALL LETTER N] + output[opos++] = 'n'; + break; + + case '\u01CA': // ÇŠ [LATIN CAPITAL LETTER NJ] + output[opos++] = 'N'; + output[opos++] = 'J'; + break; + + case '\u01CB': // Ç‹ [LATIN CAPITAL LETTER N WITH SMALL LETTER J] + output[opos++] = 'N'; + output[opos++] = 'j'; + break; + + case '\u24A9': // â’© [PARENTHESIZED LATIN SMALL LETTER N] + output[opos++] = '('; + output[opos++] = 'n'; + output[opos++] = ')'; + break; + + case '\u01CC': // ÇŒ [LATIN SMALL LETTER NJ] + output[opos++] = 'n'; + output[opos++] = 'j'; + break; + + case '\u00D2': + // Ã’ [LATIN CAPITAL LETTER O WITH GRAVE] + case '\u00D3': + // Ó [LATIN CAPITAL LETTER O WITH ACUTE] + case '\u00D4': + // �? [LATIN CAPITAL LETTER O WITH CIRCUMFLEX] + case '\u00D5': + // Õ [LATIN CAPITAL LETTER O WITH TILDE] + case '\u00D6': + // Ö [LATIN CAPITAL LETTER O WITH DIAERESIS] + case '\u00D8': + // Ø [LATIN CAPITAL LETTER O WITH STROKE] + case '\u014C': + // Ã…Å’ [LATIN CAPITAL LETTER O WITH MACRON] + case '\u014E': + // ÅŽ [LATIN CAPITAL LETTER O WITH BREVE] + case '\u0150': + // � [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE] + case '\u0186': + // Ɔ [LATIN CAPITAL LETTER OPEN O] + case '\u019F': + // ÆŸ [LATIN CAPITAL LETTER O WITH MIDDLE TILDE] + case '\u01A0': + // Æ  [LATIN CAPITAL LETTER O WITH HORN] + case '\u01D1': + // Ç‘ [LATIN CAPITAL LETTER O WITH CARON] + case '\u01EA': + // Ǫ [LATIN CAPITAL LETTER O WITH OGONEK] + case '\u01EC': + // Ǭ [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON] + case '\u01FE': + // Ǿ [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE] + case '\u020C': + // ÈŒ [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE] + case '\u020E': + // ÈŽ [LATIN CAPITAL LETTER O WITH INVERTED BREVE] + case '\u022A': + // Ȫ [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON] + case '\u022C': + // Ȭ [LATIN CAPITAL LETTER O WITH TILDE AND MACRON] + case '\u022E': + // È® [LATIN CAPITAL LETTER O WITH DOT ABOVE] + case '\u0230': + // Ȱ [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON] + case '\u1D0F': + // á´� [LATIN LETTER SMALL CAPITAL O] + case '\u1D10': + // á´� [LATIN LETTER SMALL CAPITAL OPEN O] + case '\u1E4C': + // Ṍ [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE] + case '\u1E4E': + // Ṏ [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS] + case '\u1E50': + // � [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE] + case '\u1E52': + // á¹’ [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE] + case '\u1ECC': + // Ọ [LATIN CAPITAL LETTER O WITH DOT BELOW] + case '\u1ECE': + // Ỏ [LATIN CAPITAL LETTER O WITH HOOK ABOVE] + case '\u1ED0': + // � [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE] + case '\u1ED2': + // á»’ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE] + case '\u1ED4': + // �? [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1ED6': + // á»– [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE] + case '\u1ED8': + // Ộ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EDA': + // Ớ [LATIN CAPITAL LETTER O WITH HORN AND ACUTE] + case '\u1EDC': + // Ờ [LATIN CAPITAL LETTER O WITH HORN AND GRAVE] + case '\u1EDE': + // Ở [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE] + case '\u1EE0': + // á»  [LATIN CAPITAL LETTER O WITH HORN AND TILDE] + case '\u1EE2': + // Ợ [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW] + case '\u24C4': + // â“„ [CIRCLED LATIN CAPITAL LETTER O] + case '\uA74A': + // � [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY] + case '\uA74C': + // � [LATIN CAPITAL LETTER O WITH LOOP] + case '\uFF2F': // O [FULLWIDTH LATIN CAPITAL LETTER O] + output[opos++] = 'O'; + break; + + case '\u00F2': + // ò [LATIN SMALL LETTER O WITH GRAVE] + case '\u00F3': + // ó [LATIN SMALL LETTER O WITH ACUTE] + case '\u00F4': + // ô [LATIN SMALL LETTER O WITH CIRCUMFLEX] + case '\u00F5': + // õ [LATIN SMALL LETTER O WITH TILDE] + case '\u00F6': + // ö [LATIN SMALL LETTER O WITH DIAERESIS] + case '\u00F8': + // ø [LATIN SMALL LETTER O WITH STROKE] + case '\u014D': + // � [LATIN SMALL LETTER O WITH MACRON] + case '\u014F': + // � [LATIN SMALL LETTER O WITH BREVE] + case '\u0151': + // Å‘ [LATIN SMALL LETTER O WITH DOUBLE ACUTE] + case '\u01A1': + // Æ¡ [LATIN SMALL LETTER O WITH HORN] + case '\u01D2': + // Ç’ [LATIN SMALL LETTER O WITH CARON] + case '\u01EB': + // Ç« [LATIN SMALL LETTER O WITH OGONEK] + case '\u01ED': + // Ç­ [LATIN SMALL LETTER O WITH OGONEK AND MACRON] + case '\u01FF': + // Ç¿ [LATIN SMALL LETTER O WITH STROKE AND ACUTE] + case '\u020D': + // � [LATIN SMALL LETTER O WITH DOUBLE GRAVE] + case '\u020F': + // � [LATIN SMALL LETTER O WITH INVERTED BREVE] + case '\u022B': + // È« [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON] + case '\u022D': + // È­ [LATIN SMALL LETTER O WITH TILDE AND MACRON] + case '\u022F': + // ȯ [LATIN SMALL LETTER O WITH DOT ABOVE] + case '\u0231': + // ȱ [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON] + case '\u0254': + // �? [LATIN SMALL LETTER OPEN O] + case '\u0275': + // ɵ [LATIN SMALL LETTER BARRED O] + case '\u1D16': + // á´– [LATIN SMALL LETTER TOP HALF O] + case '\u1D17': + // á´— [LATIN SMALL LETTER BOTTOM HALF O] + case '\u1D97': + // á¶— [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK] + case '\u1E4D': + // � [LATIN SMALL LETTER O WITH TILDE AND ACUTE] + case '\u1E4F': + // � [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS] + case '\u1E51': + // ṑ [LATIN SMALL LETTER O WITH MACRON AND GRAVE] + case '\u1E53': + // ṓ [LATIN SMALL LETTER O WITH MACRON AND ACUTE] + case '\u1ECD': + // � [LATIN SMALL LETTER O WITH DOT BELOW] + case '\u1ECF': + // � [LATIN SMALL LETTER O WITH HOOK ABOVE] + case '\u1ED1': + // ố [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE] + case '\u1ED3': + // ồ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE] + case '\u1ED5': + // ổ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] + case '\u1ED7': + // á»— [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE] + case '\u1ED9': + // á»™ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW] + case '\u1EDB': + // á»› [LATIN SMALL LETTER O WITH HORN AND ACUTE] + case '\u1EDD': + // � [LATIN SMALL LETTER O WITH HORN AND GRAVE] + case '\u1EDF': + // ở [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE] + case '\u1EE1': + // ỡ [LATIN SMALL LETTER O WITH HORN AND TILDE] + case '\u1EE3': + // ợ [LATIN SMALL LETTER O WITH HORN AND DOT BELOW] + case '\u2092': + // â‚’ [LATIN SUBSCRIPT SMALL LETTER O] + case '\u24DE': + // ⓞ [CIRCLED LATIN SMALL LETTER O] + case '\u2C7A': + // ⱺ [LATIN SMALL LETTER O WITH LOW RING INSIDE] + case '\uA74B': + // � [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY] + case '\uA74D': + // � [LATIN SMALL LETTER O WITH LOOP] + case '\uFF4F': // � [FULLWIDTH LATIN SMALL LETTER O] + output[opos++] = 'o'; + break; + + case '\u0152': + // Å’ [LATIN CAPITAL LIGATURE OE] + case '\u0276': // ɶ [LATIN LETTER SMALL CAPITAL OE] + output[opos++] = 'O'; + output[opos++] = 'E'; + break; + + case '\uA74E': // � [LATIN CAPITAL LETTER OO] + output[opos++] = 'O'; + output[opos++] = 'O'; + break; + + case '\u0222': + // È¢ http://en.wikipedia.org/wiki/OU [LATIN CAPITAL LETTER OU] + case '\u1D15': // á´• [LATIN LETTER SMALL CAPITAL OU] + output[opos++] = 'O'; + output[opos++] = 'U'; + break; + + case '\u24AA': // â’ª [PARENTHESIZED LATIN SMALL LETTER O] + output[opos++] = '('; + output[opos++] = 'o'; + output[opos++] = ')'; + break; + + case '\u0153': + // Å“ [LATIN SMALL LIGATURE OE] + case '\u1D14': // á´�? [LATIN SMALL LETTER TURNED OE] + output[opos++] = 'o'; + output[opos++] = 'e'; + break; + + case '\uA74F': // � [LATIN SMALL LETTER OO] + output[opos++] = 'o'; + output[opos++] = 'o'; + break; + + case '\u0223': // È£ http://en.wikipedia.org/wiki/OU [LATIN SMALL LETTER OU] + output[opos++] = 'o'; + output[opos++] = 'u'; + break; + + case '\u01A4': + // Ƥ [LATIN CAPITAL LETTER P WITH HOOK] + case '\u1D18': + // á´˜ [LATIN LETTER SMALL CAPITAL P] + case '\u1E54': + // �? [LATIN CAPITAL LETTER P WITH ACUTE] + case '\u1E56': + // á¹– [LATIN CAPITAL LETTER P WITH DOT ABOVE] + case '\u24C5': + // â“… [CIRCLED LATIN CAPITAL LETTER P] + case '\u2C63': + // â±£ [LATIN CAPITAL LETTER P WITH STROKE] + case '\uA750': + // � [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER] + case '\uA752': + // � [LATIN CAPITAL LETTER P WITH FLOURISH] + case '\uA754': + // �? [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL] + case '\uFF30': // ï¼° [FULLWIDTH LATIN CAPITAL LETTER P] + output[opos++] = 'P'; + break; + + case '\u01A5': + // Æ¥ [LATIN SMALL LETTER P WITH HOOK] + case '\u1D71': + // áµ± [LATIN SMALL LETTER P WITH MIDDLE TILDE] + case '\u1D7D': + // áµ½ [LATIN SMALL LETTER P WITH STROKE] + case '\u1D88': + // ᶈ [LATIN SMALL LETTER P WITH PALATAL HOOK] + case '\u1E55': + // ṕ [LATIN SMALL LETTER P WITH ACUTE] + case '\u1E57': + // á¹— [LATIN SMALL LETTER P WITH DOT ABOVE] + case '\u24DF': + // ⓟ [CIRCLED LATIN SMALL LETTER P] + case '\uA751': + // � [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER] + case '\uA753': + // � [LATIN SMALL LETTER P WITH FLOURISH] + case '\uA755': + // � [LATIN SMALL LETTER P WITH SQUIRREL TAIL] + case '\uA7FC': + // ꟼ [LATIN EPIGRAPHIC LETTER REVERSED P] + case '\uFF50': // � [FULLWIDTH LATIN SMALL LETTER P] + output[opos++] = 'p'; + break; + + case '\u24AB': // â’« [PARENTHESIZED LATIN SMALL LETTER P] + output[opos++] = '('; + output[opos++] = 'p'; + output[opos++] = ')'; + break; + + case '\u024A': + // ÉŠ [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL] + case '\u24C6': + // Ⓠ [CIRCLED LATIN CAPITAL LETTER Q] + case '\uA756': + // � [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER] + case '\uA758': + // � [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE] + case '\uFF31': // ï¼± [FULLWIDTH LATIN CAPITAL LETTER Q] + output[opos++] = 'Q'; + break; + + case '\u0138': + // ĸ http://en.wikipedia.org/wiki/Kra_(letter) [LATIN SMALL LETTER KRA] + case '\u024B': + // É‹ [LATIN SMALL LETTER Q WITH HOOK TAIL] + case '\u02A0': + // Ê  [LATIN SMALL LETTER Q WITH HOOK] + case '\u24E0': + // â“  [CIRCLED LATIN SMALL LETTER Q] + case '\uA757': + // � [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER] + case '\uA759': + // � [LATIN SMALL LETTER Q WITH DIAGONAL STROKE] + case '\uFF51': // q [FULLWIDTH LATIN SMALL LETTER Q] + output[opos++] = 'q'; + break; + + case '\u24AC': // â’¬ [PARENTHESIZED LATIN SMALL LETTER Q] + output[opos++] = '('; + output[opos++] = 'q'; + output[opos++] = ')'; + break; + + case '\u0239': // ȹ [LATIN SMALL LETTER QP DIGRAPH] + output[opos++] = 'q'; + output[opos++] = 'p'; + break; + + case '\u0154': + // �? [LATIN CAPITAL LETTER R WITH ACUTE] + case '\u0156': + // Å– [LATIN CAPITAL LETTER R WITH CEDILLA] + case '\u0158': + // Ã…Ëœ [LATIN CAPITAL LETTER R WITH CARON] + case '\u0210': + // È’ [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE] + case '\u0212': + // È’ [LATIN CAPITAL LETTER R WITH INVERTED BREVE] + case '\u024C': + // ÉŒ [LATIN CAPITAL LETTER R WITH STROKE] + case '\u0280': + // Ê€ [LATIN LETTER SMALL CAPITAL R] + case '\u0281': + // � [LATIN LETTER SMALL CAPITAL INVERTED R] + case '\u1D19': + // á´™ [LATIN LETTER SMALL CAPITAL REVERSED R] + case '\u1D1A': + // á´š [LATIN LETTER SMALL CAPITAL TURNED R] + case '\u1E58': + // Ṙ [LATIN CAPITAL LETTER R WITH DOT ABOVE] + case '\u1E5A': + // Ṛ [LATIN CAPITAL LETTER R WITH DOT BELOW] + case '\u1E5C': + // Ṝ [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON] + case '\u1E5E': + // Ṟ [LATIN CAPITAL LETTER R WITH LINE BELOW] + case '\u24C7': + // Ⓡ [CIRCLED LATIN CAPITAL LETTER R] + case '\u2C64': + // Ɽ [LATIN CAPITAL LETTER R WITH TAIL] + case '\uA75A': + // � [LATIN CAPITAL LETTER R ROTUNDA] + case '\uA782': + // êž‚ [LATIN CAPITAL LETTER INSULAR R] + case '\uFF32': // ï¼² [FULLWIDTH LATIN CAPITAL LETTER R] + output[opos++] = 'R'; + break; + + case '\u0155': + // Å• [LATIN SMALL LETTER R WITH ACUTE] + case '\u0157': + // Å— [LATIN SMALL LETTER R WITH CEDILLA] + case '\u0159': + // Ã…â„¢ [LATIN SMALL LETTER R WITH CARON] + case '\u0211': + // È‘ [LATIN SMALL LETTER R WITH DOUBLE GRAVE] + case '\u0213': + // È“ [LATIN SMALL LETTER R WITH INVERTED BREVE] + case '\u024D': + // � [LATIN SMALL LETTER R WITH STROKE] + case '\u027C': + // ɼ [LATIN SMALL LETTER R WITH LONG LEG] + case '\u027D': + // ɽ [LATIN SMALL LETTER R WITH TAIL] + case '\u027E': + // ɾ [LATIN SMALL LETTER R WITH FISHHOOK] + case '\u027F': + // É¿ [LATIN SMALL LETTER REVERSED R WITH FISHHOOK] + case '\u1D63': + // áµ£ [LATIN SUBSCRIPT SMALL LETTER R] + case '\u1D72': + // áµ² [LATIN SMALL LETTER R WITH MIDDLE TILDE] + case '\u1D73': + // áµ³ [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE] + case '\u1D89': + // ᶉ [LATIN SMALL LETTER R WITH PALATAL HOOK] + case '\u1E59': + // á¹™ [LATIN SMALL LETTER R WITH DOT ABOVE] + case '\u1E5B': + // á¹› [LATIN SMALL LETTER R WITH DOT BELOW] + case '\u1E5D': + // � [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON] + case '\u1E5F': + // ṟ [LATIN SMALL LETTER R WITH LINE BELOW] + case '\u24E1': + // â“¡ [CIRCLED LATIN SMALL LETTER R] + case '\uA75B': + // � [LATIN SMALL LETTER R ROTUNDA] + case '\uA783': + // ꞃ [LATIN SMALL LETTER INSULAR R] + case '\uFF52': // ï½’ [FULLWIDTH LATIN SMALL LETTER R] + output[opos++] = 'r'; + break; + + case '\u24AD': // â’­ [PARENTHESIZED LATIN SMALL LETTER R] + output[opos++] = '('; + output[opos++] = 'r'; + output[opos++] = ')'; + break; + + case '\u015A': + // Ã…Å¡ [LATIN CAPITAL LETTER S WITH ACUTE] + case '\u015C': + // Ã…Å“ [LATIN CAPITAL LETTER S WITH CIRCUMFLEX] + case '\u015E': + // Åž [LATIN CAPITAL LETTER S WITH CEDILLA] + case '\u0160': + // Å  [LATIN CAPITAL LETTER S WITH CARON] + case '\u0218': + // Ș [LATIN CAPITAL LETTER S WITH COMMA BELOW] + case '\u1E60': + // á¹  [LATIN CAPITAL LETTER S WITH DOT ABOVE] + case '\u1E62': + // á¹¢ [LATIN CAPITAL LETTER S WITH DOT BELOW] + case '\u1E64': + // Ṥ [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE] + case '\u1E66': + // Ṧ [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE] + case '\u1E68': + // Ṩ [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE] + case '\u24C8': + // Ⓢ [CIRCLED LATIN CAPITAL LETTER S] + case '\uA731': + // ꜱ [LATIN LETTER SMALL CAPITAL S] + case '\uA785': + // êž… [LATIN SMALL LETTER INSULAR S] + case '\uFF33': // ï¼³ [FULLWIDTH LATIN CAPITAL LETTER S] + output[opos++] = 'S'; + break; + + case '\u015B': + // Å› [LATIN SMALL LETTER S WITH ACUTE] + case '\u015D': + // � [LATIN SMALL LETTER S WITH CIRCUMFLEX] + case '\u015F': + // ÅŸ [LATIN SMALL LETTER S WITH CEDILLA] + case '\u0161': + // Å¡ [LATIN SMALL LETTER S WITH CARON] + case '\u017F': + // Å¿ http://en.wikipedia.org/wiki/Long_S [LATIN SMALL LETTER LONG S] + case '\u0219': + // È™ [LATIN SMALL LETTER S WITH COMMA BELOW] + case '\u023F': + // È¿ [LATIN SMALL LETTER S WITH SWASH TAIL] + case '\u0282': + // Ê‚ [LATIN SMALL LETTER S WITH HOOK] + case '\u1D74': + // áµ´ [LATIN SMALL LETTER S WITH MIDDLE TILDE] + case '\u1D8A': + // á¶Š [LATIN SMALL LETTER S WITH PALATAL HOOK] + case '\u1E61': + // ṡ [LATIN SMALL LETTER S WITH DOT ABOVE] + case '\u1E63': + // á¹£ [LATIN SMALL LETTER S WITH DOT BELOW] + case '\u1E65': + // á¹¥ [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE] + case '\u1E67': + // á¹§ [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE] + case '\u1E69': + // ṩ [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE] + case '\u1E9C': + // ẜ [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE] + case '\u1E9D': + // � [LATIN SMALL LETTER LONG S WITH HIGH STROKE] + case '\u24E2': + // â“¢ [CIRCLED LATIN SMALL LETTER S] + case '\uA784': + // êž„ [LATIN CAPITAL LETTER INSULAR S] + case '\uFF53': // s [FULLWIDTH LATIN SMALL LETTER S] + output[opos++] = 's'; + break; + + case '\u1E9E': // ẞ [LATIN CAPITAL LETTER SHARP S] + output[opos++] = 'S'; + output[opos++] = 'S'; + break; + + case '\u24AE': // â’® [PARENTHESIZED LATIN SMALL LETTER S] + output[opos++] = '('; + output[opos++] = 's'; + output[opos++] = ')'; + break; + + case '\u00DF': // ß [LATIN SMALL LETTER SHARP S] + output[opos++] = 's'; + output[opos++] = 's'; + break; + + case '\uFB06': // st [LATIN SMALL LIGATURE ST] + output[opos++] = 's'; + output[opos++] = 't'; + break; + + case '\u0162': + // Å¢ [LATIN CAPITAL LETTER T WITH CEDILLA] + case '\u0164': + // Ť [LATIN CAPITAL LETTER T WITH CARON] + case '\u0166': + // Ŧ [LATIN CAPITAL LETTER T WITH STROKE] + case '\u01AC': + // Ƭ [LATIN CAPITAL LETTER T WITH HOOK] + case '\u01AE': + // Æ® [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK] + case '\u021A': + // Èš [LATIN CAPITAL LETTER T WITH COMMA BELOW] + case '\u023E': + // Ⱦ [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE] + case '\u1D1B': + // á´› [LATIN LETTER SMALL CAPITAL T] + case '\u1E6A': + // Ṫ [LATIN CAPITAL LETTER T WITH DOT ABOVE] + case '\u1E6C': + // Ṭ [LATIN CAPITAL LETTER T WITH DOT BELOW] + case '\u1E6E': + // á¹® [LATIN CAPITAL LETTER T WITH LINE BELOW] + case '\u1E70': + // á¹° [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW] + case '\u24C9': + // Ⓣ [CIRCLED LATIN CAPITAL LETTER T] + case '\uA786': + // Ꞇ [LATIN CAPITAL LETTER INSULAR T] + case '\uFF34': // ï¼´ [FULLWIDTH LATIN CAPITAL LETTER T] + output[opos++] = 'T'; + break; + + case '\u0163': + // Å£ [LATIN SMALL LETTER T WITH CEDILLA] + case '\u0165': + // Ã…Â¥ [LATIN SMALL LETTER T WITH CARON] + case '\u0167': + // ŧ [LATIN SMALL LETTER T WITH STROKE] + case '\u01AB': + // Æ« [LATIN SMALL LETTER T WITH PALATAL HOOK] + case '\u01AD': + // Æ­ [LATIN SMALL LETTER T WITH HOOK] + case '\u021B': + // È› [LATIN SMALL LETTER T WITH COMMA BELOW] + case '\u0236': + // ȶ [LATIN SMALL LETTER T WITH CURL] + case '\u0287': + // ʇ [LATIN SMALL LETTER TURNED T] + case '\u0288': + // ʈ [LATIN SMALL LETTER T WITH RETROFLEX HOOK] + case '\u1D75': + // áµµ [LATIN SMALL LETTER T WITH MIDDLE TILDE] + case '\u1E6B': + // ṫ [LATIN SMALL LETTER T WITH DOT ABOVE] + case '\u1E6D': + // á¹­ [LATIN SMALL LETTER T WITH DOT BELOW] + case '\u1E6F': + // ṯ [LATIN SMALL LETTER T WITH LINE BELOW] + case '\u1E71': + // á¹± [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW] + case '\u1E97': + // ẗ [LATIN SMALL LETTER T WITH DIAERESIS] + case '\u24E3': + // â“£ [CIRCLED LATIN SMALL LETTER T] + case '\u2C66': + // ⱦ [LATIN SMALL LETTER T WITH DIAGONAL STROKE] + case '\uFF54': // �? [FULLWIDTH LATIN SMALL LETTER T] + output[opos++] = 't'; + break; + + case '\u00DE': + // Þ [LATIN CAPITAL LETTER THORN] + case '\uA766': // � [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER] + output[opos++] = 'T'; + output[opos++] = 'H'; + break; + + case '\uA728': // Ꜩ [LATIN CAPITAL LETTER TZ] + output[opos++] = 'T'; + output[opos++] = 'Z'; + break; + + case '\u24AF': // â’¯ [PARENTHESIZED LATIN SMALL LETTER T] + output[opos++] = '('; + output[opos++] = 't'; + output[opos++] = ')'; + break; + + case '\u02A8': // ʨ [LATIN SMALL LETTER TC DIGRAPH WITH CURL] + output[opos++] = 't'; + output[opos++] = 'c'; + break; + + case '\u00FE': + // þ [LATIN SMALL LETTER THORN] + case '\u1D7A': + // ᵺ [LATIN SMALL LETTER TH WITH STRIKETHROUGH] + case '\uA767': // � [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER] + output[opos++] = 't'; + output[opos++] = 'h'; + break; + + case '\u02A6': // ʦ [LATIN SMALL LETTER TS DIGRAPH] + output[opos++] = 't'; + output[opos++] = 's'; + break; + + case '\uA729': // ꜩ [LATIN SMALL LETTER TZ] + output[opos++] = 't'; + output[opos++] = 'z'; + break; + + case '\u00D9': + // Ù [LATIN CAPITAL LETTER U WITH GRAVE] + case '\u00DA': + // Ú [LATIN CAPITAL LETTER U WITH ACUTE] + case '\u00DB': + // Û [LATIN CAPITAL LETTER U WITH CIRCUMFLEX] + case '\u00DC': + // Ü [LATIN CAPITAL LETTER U WITH DIAERESIS] + case '\u0168': + // Ũ [LATIN CAPITAL LETTER U WITH TILDE] + case '\u016A': + // Ū [LATIN CAPITAL LETTER U WITH MACRON] + case '\u016C': + // Ŭ [LATIN CAPITAL LETTER U WITH BREVE] + case '\u016E': + // Å® [LATIN CAPITAL LETTER U WITH RING ABOVE] + case '\u0170': + // Ű [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE] + case '\u0172': + // Ų [LATIN CAPITAL LETTER U WITH OGONEK] + case '\u01AF': + // Ư [LATIN CAPITAL LETTER U WITH HORN] + case '\u01D3': + // Ç“ [LATIN CAPITAL LETTER U WITH CARON] + case '\u01D5': + // Ç• [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON] + case '\u01D7': + // Ç— [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE] + case '\u01D9': + // Ç™ [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON] + case '\u01DB': + // Ç› [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE] + case '\u0214': + // �? [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE] + case '\u0216': + // È– [LATIN CAPITAL LETTER U WITH INVERTED BREVE] + case '\u0244': + // É„ [LATIN CAPITAL LETTER U BAR] + case '\u1D1C': + // á´œ [LATIN LETTER SMALL CAPITAL U] + case '\u1D7E': + // áµ¾ [LATIN SMALL CAPITAL LETTER U WITH STROKE] + case '\u1E72': + // á¹² [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW] + case '\u1E74': + // á¹´ [LATIN CAPITAL LETTER U WITH TILDE BELOW] + case '\u1E76': + // á¹¶ [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW] + case '\u1E78': + // Ṹ [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE] + case '\u1E7A': + // Ṻ [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS] + case '\u1EE4': + // Ụ [LATIN CAPITAL LETTER U WITH DOT BELOW] + case '\u1EE6': + // Ủ [LATIN CAPITAL LETTER U WITH HOOK ABOVE] + case '\u1EE8': + // Ứ [LATIN CAPITAL LETTER U WITH HORN AND ACUTE] + case '\u1EEA': + // Ừ [LATIN CAPITAL LETTER U WITH HORN AND GRAVE] + case '\u1EEC': + // Ử [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE] + case '\u1EEE': + // á»® [LATIN CAPITAL LETTER U WITH HORN AND TILDE] + case '\u1EF0': + // á»° [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW] + case '\u24CA': + // Ⓤ [CIRCLED LATIN CAPITAL LETTER U] + case '\uFF35': // ï¼µ [FULLWIDTH LATIN CAPITAL LETTER U] + output[opos++] = 'U'; + break; + + case '\u00F9': + // ù [LATIN SMALL LETTER U WITH GRAVE] + case '\u00FA': + // ú [LATIN SMALL LETTER U WITH ACUTE] + case '\u00FB': + // û [LATIN SMALL LETTER U WITH CIRCUMFLEX] + case '\u00FC': + // ü [LATIN SMALL LETTER U WITH DIAERESIS] + case '\u0169': + // Å© [LATIN SMALL LETTER U WITH TILDE] + case '\u016B': + // Å« [LATIN SMALL LETTER U WITH MACRON] + case '\u016D': + // Å­ [LATIN SMALL LETTER U WITH BREVE] + case '\u016F': + // ů [LATIN SMALL LETTER U WITH RING ABOVE] + case '\u0171': + // ű [LATIN SMALL LETTER U WITH DOUBLE ACUTE] + case '\u0173': + // ų [LATIN SMALL LETTER U WITH OGONEK] + case '\u01B0': + // ư [LATIN SMALL LETTER U WITH HORN] + case '\u01D4': + // �? [LATIN SMALL LETTER U WITH CARON] + case '\u01D6': + // Ç– [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON] + case '\u01D8': + // ǘ [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE] + case '\u01DA': + // Çš [LATIN SMALL LETTER U WITH DIAERESIS AND CARON] + case '\u01DC': + // Çœ [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE] + case '\u0215': + // È• [LATIN SMALL LETTER U WITH DOUBLE GRAVE] + case '\u0217': + // È— [LATIN SMALL LETTER U WITH INVERTED BREVE] + case '\u0289': + // ʉ [LATIN SMALL LETTER U BAR] + case '\u1D64': + // ᵤ [LATIN SUBSCRIPT SMALL LETTER U] + case '\u1D99': + // á¶™ [LATIN SMALL LETTER U WITH RETROFLEX HOOK] + case '\u1E73': + // á¹³ [LATIN SMALL LETTER U WITH DIAERESIS BELOW] + case '\u1E75': + // á¹µ [LATIN SMALL LETTER U WITH TILDE BELOW] + case '\u1E77': + // á¹· [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW] + case '\u1E79': + // á¹¹ [LATIN SMALL LETTER U WITH TILDE AND ACUTE] + case '\u1E7B': + // á¹» [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS] + case '\u1EE5': + // ụ [LATIN SMALL LETTER U WITH DOT BELOW] + case '\u1EE7': + // á»§ [LATIN SMALL LETTER U WITH HOOK ABOVE] + case '\u1EE9': + // ứ [LATIN SMALL LETTER U WITH HORN AND ACUTE] + case '\u1EEB': + // ừ [LATIN SMALL LETTER U WITH HORN AND GRAVE] + case '\u1EED': + // á»­ [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE] + case '\u1EEF': + // ữ [LATIN SMALL LETTER U WITH HORN AND TILDE] + case '\u1EF1': + // á»± [LATIN SMALL LETTER U WITH HORN AND DOT BELOW] + case '\u24E4': + // ⓤ [CIRCLED LATIN SMALL LETTER U] + case '\uFF55': // u [FULLWIDTH LATIN SMALL LETTER U] + output[opos++] = 'u'; + break; + + case '\u24B0': // â’° [PARENTHESIZED LATIN SMALL LETTER U] + output[opos++] = '('; + output[opos++] = 'u'; + output[opos++] = ')'; + break; + + case '\u1D6B': // ᵫ [LATIN SMALL LETTER UE] + output[opos++] = 'u'; + output[opos++] = 'e'; + break; + + case '\u01B2': + // Ʋ [LATIN CAPITAL LETTER V WITH HOOK] + case '\u0245': + // É… [LATIN CAPITAL LETTER TURNED V] + case '\u1D20': + // á´  [LATIN LETTER SMALL CAPITAL V] + case '\u1E7C': + // á¹¼ [LATIN CAPITAL LETTER V WITH TILDE] + case '\u1E7E': + // á¹¾ [LATIN CAPITAL LETTER V WITH DOT BELOW] + case '\u1EFC': + // Ỽ [LATIN CAPITAL LETTER MIDDLE-WELSH V] + case '\u24CB': + // â“‹ [CIRCLED LATIN CAPITAL LETTER V] + case '\uA75E': + // � [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE] + case '\uA768': + // � [LATIN CAPITAL LETTER VEND] + case '\uFF36': // ï¼¶ [FULLWIDTH LATIN CAPITAL LETTER V] + output[opos++] = 'V'; + break; + + case '\u028B': + // Ê‹ [LATIN SMALL LETTER V WITH HOOK] + case '\u028C': + // ÊŒ [LATIN SMALL LETTER TURNED V] + case '\u1D65': + // áµ¥ [LATIN SUBSCRIPT SMALL LETTER V] + case '\u1D8C': + // á¶Œ [LATIN SMALL LETTER V WITH PALATAL HOOK] + case '\u1E7D': + // á¹½ [LATIN SMALL LETTER V WITH TILDE] + case '\u1E7F': + // ṿ [LATIN SMALL LETTER V WITH DOT BELOW] + case '\u24E5': + // â“¥ [CIRCLED LATIN SMALL LETTER V] + case '\u2C71': + // â±± [LATIN SMALL LETTER V WITH RIGHT HOOK] + case '\u2C74': + // â±´ [LATIN SMALL LETTER V WITH CURL] + case '\uA75F': + // � [LATIN SMALL LETTER V WITH DIAGONAL STROKE] + case '\uFF56': // ï½– [FULLWIDTH LATIN SMALL LETTER V] + output[opos++] = 'v'; + break; + + case '\uA760': // � [LATIN CAPITAL LETTER VY] + output[opos++] = 'V'; + output[opos++] = 'Y'; + break; + + case '\u24B1': // â’± [PARENTHESIZED LATIN SMALL LETTER V] + output[opos++] = '('; + output[opos++] = 'v'; + output[opos++] = ')'; + break; + + case '\uA761': // � [LATIN SMALL LETTER VY] + output[opos++] = 'v'; + output[opos++] = 'y'; + break; + + case '\u0174': + // Å´ [LATIN CAPITAL LETTER W WITH CIRCUMFLEX] + case '\u01F7': + // Ç· http://en.wikipedia.org/wiki/Wynn [LATIN CAPITAL LETTER WYNN] + case '\u1D21': + // á´¡ [LATIN LETTER SMALL CAPITAL W] + case '\u1E80': + // Ẁ [LATIN CAPITAL LETTER W WITH GRAVE] + case '\u1E82': + // Ẃ [LATIN CAPITAL LETTER W WITH ACUTE] + case '\u1E84': + // Ẅ [LATIN CAPITAL LETTER W WITH DIAERESIS] + case '\u1E86': + // Ẇ [LATIN CAPITAL LETTER W WITH DOT ABOVE] + case '\u1E88': + // Ẉ [LATIN CAPITAL LETTER W WITH DOT BELOW] + case '\u24CC': + // Ⓦ [CIRCLED LATIN CAPITAL LETTER W] + case '\u2C72': + // â±² [LATIN CAPITAL LETTER W WITH HOOK] + case '\uFF37': // ï¼· [FULLWIDTH LATIN CAPITAL LETTER W] + output[opos++] = 'W'; + break; + + case '\u0175': + // ŵ [LATIN SMALL LETTER W WITH CIRCUMFLEX] + case '\u01BF': + // Æ¿ http://en.wikipedia.org/wiki/Wynn [LATIN LETTER WYNN] + case '\u028D': + // � [LATIN SMALL LETTER TURNED W] + case '\u1E81': + // � [LATIN SMALL LETTER W WITH GRAVE] + case '\u1E83': + // ẃ [LATIN SMALL LETTER W WITH ACUTE] + case '\u1E85': + // ẅ [LATIN SMALL LETTER W WITH DIAERESIS] + case '\u1E87': + // ẇ [LATIN SMALL LETTER W WITH DOT ABOVE] + case '\u1E89': + // ẉ [LATIN SMALL LETTER W WITH DOT BELOW] + case '\u1E98': + // ẘ [LATIN SMALL LETTER W WITH RING ABOVE] + case '\u24E6': + // ⓦ [CIRCLED LATIN SMALL LETTER W] + case '\u2C73': + // â±³ [LATIN SMALL LETTER W WITH HOOK] + case '\uFF57': // ï½— [FULLWIDTH LATIN SMALL LETTER W] + output[opos++] = 'w'; + break; + + case '\u24B2': // â’² [PARENTHESIZED LATIN SMALL LETTER W] + output[opos++] = '('; + output[opos++] = 'w'; + output[opos++] = ')'; + break; + + case '\u1E8A': + // Ẋ [LATIN CAPITAL LETTER X WITH DOT ABOVE] + case '\u1E8C': + // Ẍ [LATIN CAPITAL LETTER X WITH DIAERESIS] + case '\u24CD': + // � [CIRCLED LATIN CAPITAL LETTER X] + case '\uFF38': // X [FULLWIDTH LATIN CAPITAL LETTER X] + output[opos++] = 'X'; + break; + + case '\u1D8D': + // � [LATIN SMALL LETTER X WITH PALATAL HOOK] + case '\u1E8B': + // ẋ [LATIN SMALL LETTER X WITH DOT ABOVE] + case '\u1E8D': + // � [LATIN SMALL LETTER X WITH DIAERESIS] + case '\u2093': + // â‚“ [LATIN SUBSCRIPT SMALL LETTER X] + case '\u24E7': + // â“§ [CIRCLED LATIN SMALL LETTER X] + case '\uFF58': // x [FULLWIDTH LATIN SMALL LETTER X] + output[opos++] = 'x'; + break; + + case '\u24B3': // â’³ [PARENTHESIZED LATIN SMALL LETTER X] + output[opos++] = '('; + output[opos++] = 'x'; + output[opos++] = ')'; + break; + + case '\u00DD': + // � [LATIN CAPITAL LETTER Y WITH ACUTE] + case '\u0176': + // Ŷ [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX] + case '\u0178': + // Ÿ [LATIN CAPITAL LETTER Y WITH DIAERESIS] + case '\u01B3': + // Ƴ [LATIN CAPITAL LETTER Y WITH HOOK] + case '\u0232': + // Ȳ [LATIN CAPITAL LETTER Y WITH MACRON] + case '\u024E': + // ÉŽ [LATIN CAPITAL LETTER Y WITH STROKE] + case '\u028F': + // � [LATIN LETTER SMALL CAPITAL Y] + case '\u1E8E': + // Ẏ [LATIN CAPITAL LETTER Y WITH DOT ABOVE] + case '\u1EF2': + // Ỳ [LATIN CAPITAL LETTER Y WITH GRAVE] + case '\u1EF4': + // á»´ [LATIN CAPITAL LETTER Y WITH DOT BELOW] + case '\u1EF6': + // á»¶ [LATIN CAPITAL LETTER Y WITH HOOK ABOVE] + case '\u1EF8': + // Ỹ [LATIN CAPITAL LETTER Y WITH TILDE] + case '\u1EFE': + // Ỿ [LATIN CAPITAL LETTER Y WITH LOOP] + case '\u24CE': + // Ⓨ [CIRCLED LATIN CAPITAL LETTER Y] + case '\uFF39': // ï¼¹ [FULLWIDTH LATIN CAPITAL LETTER Y] + output[opos++] = 'Y'; + break; + + case '\u00FD': + // ý [LATIN SMALL LETTER Y WITH ACUTE] + case '\u00FF': + // ÿ [LATIN SMALL LETTER Y WITH DIAERESIS] + case '\u0177': + // Å· [LATIN SMALL LETTER Y WITH CIRCUMFLEX] + case '\u01B4': + // Æ´ [LATIN SMALL LETTER Y WITH HOOK] + case '\u0233': + // ȳ [LATIN SMALL LETTER Y WITH MACRON] + case '\u024F': + // � [LATIN SMALL LETTER Y WITH STROKE] + case '\u028E': + // ÊŽ [LATIN SMALL LETTER TURNED Y] + case '\u1E8F': + // � [LATIN SMALL LETTER Y WITH DOT ABOVE] + case '\u1E99': + // ẙ [LATIN SMALL LETTER Y WITH RING ABOVE] + case '\u1EF3': + // ỳ [LATIN SMALL LETTER Y WITH GRAVE] + case '\u1EF5': + // ỵ [LATIN SMALL LETTER Y WITH DOT BELOW] + case '\u1EF7': + // á»· [LATIN SMALL LETTER Y WITH HOOK ABOVE] + case '\u1EF9': + // ỹ [LATIN SMALL LETTER Y WITH TILDE] + case '\u1EFF': + // ỿ [LATIN SMALL LETTER Y WITH LOOP] + case '\u24E8': + // ⓨ [CIRCLED LATIN SMALL LETTER Y] + case '\uFF59': // ï½™ [FULLWIDTH LATIN SMALL LETTER Y] + output[opos++] = 'y'; + break; + + case '\u24B4': // â’´ [PARENTHESIZED LATIN SMALL LETTER Y] + output[opos++] = '('; + output[opos++] = 'y'; + output[opos++] = ')'; + break; + + case '\u0179': + // Ź [LATIN CAPITAL LETTER Z WITH ACUTE] + case '\u017B': + // Å» [LATIN CAPITAL LETTER Z WITH DOT ABOVE] + case '\u017D': + // Ž [LATIN CAPITAL LETTER Z WITH CARON] + case '\u01B5': + // Ƶ [LATIN CAPITAL LETTER Z WITH STROKE] + case '\u021C': + // Èœ http://en.wikipedia.org/wiki/Yogh [LATIN CAPITAL LETTER YOGH] + case '\u0224': + // Ȥ [LATIN CAPITAL LETTER Z WITH HOOK] + case '\u1D22': + // á´¢ [LATIN LETTER SMALL CAPITAL Z] + case '\u1E90': + // � [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX] + case '\u1E92': + // Ẓ [LATIN CAPITAL LETTER Z WITH DOT BELOW] + case '\u1E94': + // �? [LATIN CAPITAL LETTER Z WITH LINE BELOW] + case '\u24CF': + // � [CIRCLED LATIN CAPITAL LETTER Z] + case '\u2C6B': + // Ⱬ [LATIN CAPITAL LETTER Z WITH DESCENDER] + case '\uA762': + // � [LATIN CAPITAL LETTER VISIGOTHIC Z] + case '\uFF3A': // Z [FULLWIDTH LATIN CAPITAL LETTER Z] + output[opos++] = 'Z'; + break; + + case '\u017A': + // ź [LATIN SMALL LETTER Z WITH ACUTE] + case '\u017C': + // ż [LATIN SMALL LETTER Z WITH DOT ABOVE] + case '\u017E': + // ž [LATIN SMALL LETTER Z WITH CARON] + case '\u01B6': + // ƶ [LATIN SMALL LETTER Z WITH STROKE] + case '\u021D': + // � http://en.wikipedia.org/wiki/Yogh [LATIN SMALL LETTER YOGH] + case '\u0225': + // È¥ [LATIN SMALL LETTER Z WITH HOOK] + case '\u0240': + // É€ [LATIN SMALL LETTER Z WITH SWASH TAIL] + case '\u0290': + // � [LATIN SMALL LETTER Z WITH RETROFLEX HOOK] + case '\u0291': + // Ê‘ [LATIN SMALL LETTER Z WITH CURL] + case '\u1D76': + // áµ¶ [LATIN SMALL LETTER Z WITH MIDDLE TILDE] + case '\u1D8E': + // á¶Ž [LATIN SMALL LETTER Z WITH PALATAL HOOK] + case '\u1E91': + // ẑ [LATIN SMALL LETTER Z WITH CIRCUMFLEX] + case '\u1E93': + // ẓ [LATIN SMALL LETTER Z WITH DOT BELOW] + case '\u1E95': + // ẕ [LATIN SMALL LETTER Z WITH LINE BELOW] + case '\u24E9': + // â“© [CIRCLED LATIN SMALL LETTER Z] + case '\u2C6C': + // ⱬ [LATIN SMALL LETTER Z WITH DESCENDER] + case '\uA763': + // � [LATIN SMALL LETTER VISIGOTHIC Z] + case '\uFF5A': // z [FULLWIDTH LATIN SMALL LETTER Z] + output[opos++] = 'z'; + break; + + case '\u24B5': // â’µ [PARENTHESIZED LATIN SMALL LETTER Z] + output[opos++] = '('; + output[opos++] = 'z'; + output[opos++] = ')'; + break; + + case '\u2070': + // � [SUPERSCRIPT ZERO] + case '\u2080': + // â‚€ [SUBSCRIPT ZERO] + case '\u24EA': + // ⓪ [CIRCLED DIGIT ZERO] + case '\u24FF': + // â“¿ [NEGATIVE CIRCLED DIGIT ZERO] + case '\uFF10': // � [FULLWIDTH DIGIT ZERO] + output[opos++] = '0'; + break; + + case '\u00B9': + // ¹ [SUPERSCRIPT ONE] + case '\u2081': + // � [SUBSCRIPT ONE] + case '\u2460': + // â‘  [CIRCLED DIGIT ONE] + case '\u24F5': + // ⓵ [DOUBLE CIRCLED DIGIT ONE] + case '\u2776': + // � [DINGBAT NEGATIVE CIRCLED DIGIT ONE] + case '\u2780': + // ➀ [DINGBAT CIRCLED SANS-SERIF DIGIT ONE] + case '\u278A': + // ➊ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE] + case '\uFF11': // 1 [FULLWIDTH DIGIT ONE] + output[opos++] = '1'; + break; + + case '\u2488': // â’ˆ [DIGIT ONE FULL STOP] + output[opos++] = '1'; + output[opos++] = '.'; + break; + + case '\u2474': // â‘´ [PARENTHESIZED DIGIT ONE] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = ')'; + break; + + case '\u00B2': + // ² [SUPERSCRIPT TWO] + case '\u2082': + // â‚‚ [SUBSCRIPT TWO] + case '\u2461': + // â‘¡ [CIRCLED DIGIT TWO] + case '\u24F6': + // â“¶ [DOUBLE CIRCLED DIGIT TWO] + case '\u2777': + // � [DINGBAT NEGATIVE CIRCLED DIGIT TWO] + case '\u2781': + // � [DINGBAT CIRCLED SANS-SERIF DIGIT TWO] + case '\u278B': + // âž‹ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO] + case '\uFF12': // ï¼’ [FULLWIDTH DIGIT TWO] + output[opos++] = '2'; + break; + + case '\u2489': // â’‰ [DIGIT TWO FULL STOP] + output[opos++] = '2'; + output[opos++] = '.'; + break; + + case '\u2475': // ⑵ [PARENTHESIZED DIGIT TWO] + output[opos++] = '('; + output[opos++] = '2'; + output[opos++] = ')'; + break; + + case '\u00B3': + // ³ [SUPERSCRIPT THREE] + case '\u2083': + // ₃ [SUBSCRIPT THREE] + case '\u2462': + // â‘¢ [CIRCLED DIGIT THREE] + case '\u24F7': + // â“· [DOUBLE CIRCLED DIGIT THREE] + case '\u2778': + // � [DINGBAT NEGATIVE CIRCLED DIGIT THREE] + case '\u2782': + // âž‚ [DINGBAT CIRCLED SANS-SERIF DIGIT THREE] + case '\u278C': + // ➌ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE] + case '\uFF13': // 3 [FULLWIDTH DIGIT THREE] + output[opos++] = '3'; + break; + + case '\u248A': // â’Š [DIGIT THREE FULL STOP] + output[opos++] = '3'; + output[opos++] = '.'; + break; + + case '\u2476': // â‘¶ [PARENTHESIZED DIGIT THREE] + output[opos++] = '('; + output[opos++] = '3'; + output[opos++] = ')'; + break; + + case '\u2074': + // � [SUPERSCRIPT FOUR] + case '\u2084': + // â‚„ [SUBSCRIPT FOUR] + case '\u2463': + // â‘£ [CIRCLED DIGIT FOUR] + case '\u24F8': + // ⓸ [DOUBLE CIRCLED DIGIT FOUR] + case '\u2779': + // � [DINGBAT NEGATIVE CIRCLED DIGIT FOUR] + case '\u2783': + // ➃ [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR] + case '\u278D': + // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR] + case '\uFF14': // �? [FULLWIDTH DIGIT FOUR] + output[opos++] = '4'; + break; + + case '\u248B': // â’‹ [DIGIT FOUR FULL STOP] + output[opos++] = '4'; + output[opos++] = '.'; + break; + + case '\u2477': // â‘· [PARENTHESIZED DIGIT FOUR] + output[opos++] = '('; + output[opos++] = '4'; + output[opos++] = ')'; + break; + + case '\u2075': + // � [SUPERSCRIPT FIVE] + case '\u2085': + // â‚… [SUBSCRIPT FIVE] + case '\u2464': + // ⑤ [CIRCLED DIGIT FIVE] + case '\u24F9': + // ⓹ [DOUBLE CIRCLED DIGIT FIVE] + case '\u277A': + // � [DINGBAT NEGATIVE CIRCLED DIGIT FIVE] + case '\u2784': + // âž„ [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE] + case '\u278E': + // ➎ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE] + case '\uFF15': // 5 [FULLWIDTH DIGIT FIVE] + output[opos++] = '5'; + break; + + case '\u248C': // â’Œ [DIGIT FIVE FULL STOP] + output[opos++] = '5'; + output[opos++] = '.'; + break; + + case '\u2478': // ⑸ [PARENTHESIZED DIGIT FIVE] + output[opos++] = '('; + output[opos++] = '5'; + output[opos++] = ')'; + break; + + case '\u2076': + // � [SUPERSCRIPT SIX] + case '\u2086': + // ₆ [SUBSCRIPT SIX] + case '\u2465': + // â‘¥ [CIRCLED DIGIT SIX] + case '\u24FA': + // ⓺ [DOUBLE CIRCLED DIGIT SIX] + case '\u277B': + // � [DINGBAT NEGATIVE CIRCLED DIGIT SIX] + case '\u2785': + // âž… [DINGBAT CIRCLED SANS-SERIF DIGIT SIX] + case '\u278F': + // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX] + case '\uFF16': // ï¼– [FULLWIDTH DIGIT SIX] + output[opos++] = '6'; + break; + + case '\u248D': // â’� [DIGIT SIX FULL STOP] + output[opos++] = '6'; + output[opos++] = '.'; + break; + + case '\u2479': // ⑹ [PARENTHESIZED DIGIT SIX] + output[opos++] = '('; + output[opos++] = '6'; + output[opos++] = ')'; + break; + + case '\u2077': + // � [SUPERSCRIPT SEVEN] + case '\u2087': + // ₇ [SUBSCRIPT SEVEN] + case '\u2466': + // ⑦ [CIRCLED DIGIT SEVEN] + case '\u24FB': + // â“» [DOUBLE CIRCLED DIGIT SEVEN] + case '\u277C': + // � [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN] + case '\u2786': + // ➆ [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN] + case '\u2790': + // � [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN] + case '\uFF17': // ï¼— [FULLWIDTH DIGIT SEVEN] + output[opos++] = '7'; + break; + + case '\u248E': // â’Ž [DIGIT SEVEN FULL STOP] + output[opos++] = '7'; + output[opos++] = '.'; + break; + + case '\u247A': // ⑺ [PARENTHESIZED DIGIT SEVEN] + output[opos++] = '('; + output[opos++] = '7'; + output[opos++] = ')'; + break; + + case '\u2078': + // � [SUPERSCRIPT EIGHT] + case '\u2088': + // ₈ [SUBSCRIPT EIGHT] + case '\u2467': + // â‘§ [CIRCLED DIGIT EIGHT] + case '\u24FC': + // ⓼ [DOUBLE CIRCLED DIGIT EIGHT] + case '\u277D': + // � [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT] + case '\u2787': + // ➇ [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT] + case '\u2791': + // âž‘ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT] + case '\uFF18': // 8 [FULLWIDTH DIGIT EIGHT] + output[opos++] = '8'; + break; + + case '\u248F': // â’� [DIGIT EIGHT FULL STOP] + output[opos++] = '8'; + output[opos++] = '.'; + break; + + case '\u247B': // â‘» [PARENTHESIZED DIGIT EIGHT] + output[opos++] = '('; + output[opos++] = '8'; + output[opos++] = ')'; + break; + + case '\u2079': + // � [SUPERSCRIPT NINE] + case '\u2089': + // ₉ [SUBSCRIPT NINE] + case '\u2468': + // ⑨ [CIRCLED DIGIT NINE] + case '\u24FD': + // ⓽ [DOUBLE CIRCLED DIGIT NINE] + case '\u277E': + // � [DINGBAT NEGATIVE CIRCLED DIGIT NINE] + case '\u2788': + // ➈ [DINGBAT CIRCLED SANS-SERIF DIGIT NINE] + case '\u2792': + // âž’ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE] + case '\uFF19': // ï¼™ [FULLWIDTH DIGIT NINE] + output[opos++] = '9'; + break; + + case '\u2490': // â’� [DIGIT NINE FULL STOP] + output[opos++] = '9'; + output[opos++] = '.'; + break; + + case '\u247C': // ⑼ [PARENTHESIZED DIGIT NINE] + output[opos++] = '('; + output[opos++] = '9'; + output[opos++] = ')'; + break; + + case '\u2469': + // â‘© [CIRCLED NUMBER TEN] + case '\u24FE': + // ⓾ [DOUBLE CIRCLED NUMBER TEN] + case '\u277F': + // � [DINGBAT NEGATIVE CIRCLED NUMBER TEN] + case '\u2789': + // ➉ [DINGBAT CIRCLED SANS-SERIF NUMBER TEN] + case '\u2793': // âž“ [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN] + output[opos++] = '1'; + output[opos++] = '0'; + break; + + case '\u2491': // â’‘ [NUMBER TEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '0'; + output[opos++] = '.'; + break; + + case '\u247D': // ⑽ [PARENTHESIZED NUMBER TEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '0'; + output[opos++] = ')'; + break; + + case '\u246A': + // ⑪ [CIRCLED NUMBER ELEVEN] + case '\u24EB': // â“« [NEGATIVE CIRCLED NUMBER ELEVEN] + output[opos++] = '1'; + output[opos++] = '1'; + break; + + case '\u2492': // â’’ [NUMBER ELEVEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '1'; + output[opos++] = '.'; + break; + + case '\u247E': // ⑾ [PARENTHESIZED NUMBER ELEVEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '1'; + output[opos++] = ')'; + break; + + case '\u246B': + // â‘« [CIRCLED NUMBER TWELVE] + case '\u24EC': // ⓬ [NEGATIVE CIRCLED NUMBER TWELVE] + output[opos++] = '1'; + output[opos++] = '2'; + break; + + case '\u2493': // â’“ [NUMBER TWELVE FULL STOP] + output[opos++] = '1'; + output[opos++] = '2'; + output[opos++] = '.'; + break; + + case '\u247F': // â‘¿ [PARENTHESIZED NUMBER TWELVE] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '2'; + output[opos++] = ')'; + break; + + case '\u246C': + // ⑬ [CIRCLED NUMBER THIRTEEN] + case '\u24ED': // â“­ [NEGATIVE CIRCLED NUMBER THIRTEEN] + output[opos++] = '1'; + output[opos++] = '3'; + break; + + case '\u2494': // â’�? [NUMBER THIRTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '3'; + output[opos++] = '.'; + break; + + case '\u2480': // â’€ [PARENTHESIZED NUMBER THIRTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '3'; + output[opos++] = ')'; + break; + + case '\u246D': + // â‘­ [CIRCLED NUMBER FOURTEEN] + case '\u24EE': // â“® [NEGATIVE CIRCLED NUMBER FOURTEEN] + output[opos++] = '1'; + output[opos++] = '4'; + break; + + case '\u2495': // â’• [NUMBER FOURTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '4'; + output[opos++] = '.'; + break; + + case '\u2481': // â’� [PARENTHESIZED NUMBER FOURTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '4'; + output[opos++] = ')'; + break; + + case '\u246E': + // â‘® [CIRCLED NUMBER FIFTEEN] + case '\u24EF': // ⓯ [NEGATIVE CIRCLED NUMBER FIFTEEN] + output[opos++] = '1'; + output[opos++] = '5'; + break; + + case '\u2496': // â’– [NUMBER FIFTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '5'; + output[opos++] = '.'; + break; + + case '\u2482': // â’‚ [PARENTHESIZED NUMBER FIFTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '5'; + output[opos++] = ')'; + break; + + case '\u246F': + // ⑯ [CIRCLED NUMBER SIXTEEN] + case '\u24F0': // â“° [NEGATIVE CIRCLED NUMBER SIXTEEN] + output[opos++] = '1'; + output[opos++] = '6'; + break; + + case '\u2497': // â’— [NUMBER SIXTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '6'; + output[opos++] = '.'; + break; + + case '\u2483': // â’ƒ [PARENTHESIZED NUMBER SIXTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '6'; + output[opos++] = ')'; + break; + + case '\u2470': + // â‘° [CIRCLED NUMBER SEVENTEEN] + case '\u24F1': // ⓱ [NEGATIVE CIRCLED NUMBER SEVENTEEN] + output[opos++] = '1'; + output[opos++] = '7'; + break; + + case '\u2498': // â’˜ [NUMBER SEVENTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '7'; + output[opos++] = '.'; + break; + + case '\u2484': // â’„ [PARENTHESIZED NUMBER SEVENTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '7'; + output[opos++] = ')'; + break; + + case '\u2471': + // ⑱ [CIRCLED NUMBER EIGHTEEN] + case '\u24F2': // ⓲ [NEGATIVE CIRCLED NUMBER EIGHTEEN] + output[opos++] = '1'; + output[opos++] = '8'; + break; + + case '\u2499': // â’™ [NUMBER EIGHTEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '8'; + output[opos++] = '.'; + break; + + case '\u2485': // â’… [PARENTHESIZED NUMBER EIGHTEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '8'; + output[opos++] = ')'; + break; + + case '\u2472': + // ⑲ [CIRCLED NUMBER NINETEEN] + case '\u24F3': // ⓳ [NEGATIVE CIRCLED NUMBER NINETEEN] + output[opos++] = '1'; + output[opos++] = '9'; + break; + + case '\u249A': // â’š [NUMBER NINETEEN FULL STOP] + output[opos++] = '1'; + output[opos++] = '9'; + output[opos++] = '.'; + break; + + case '\u2486': // â’† [PARENTHESIZED NUMBER NINETEEN] + output[opos++] = '('; + output[opos++] = '1'; + output[opos++] = '9'; + output[opos++] = ')'; + break; + + case '\u2473': + // ⑳ [CIRCLED NUMBER TWENTY] + case '\u24F4': // â“´ [NEGATIVE CIRCLED NUMBER TWENTY] + output[opos++] = '2'; + output[opos++] = '0'; + break; + + case '\u249B': // â’› [NUMBER TWENTY FULL STOP] + output[opos++] = '2'; + output[opos++] = '0'; + output[opos++] = '.'; + break; + + case '\u2487': // â’‡ [PARENTHESIZED NUMBER TWENTY] + output[opos++] = '('; + output[opos++] = '2'; + output[opos++] = '0'; + output[opos++] = ')'; + break; + + case '\u00AB': + // « [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK] + case '\u00BB': + // » [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK] + case '\u201C': + // “ [LEFT DOUBLE QUOTATION MARK] + case '\u201D': + // � [RIGHT DOUBLE QUOTATION MARK] + case '\u201E': + // „ [DOUBLE LOW-9 QUOTATION MARK] + case '\u2033': + // ″ [DOUBLE PRIME] + case '\u2036': + // ‶ [REVERSED DOUBLE PRIME] + case '\u275D': + // � [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT] + case '\u275E': + // � [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT] + case '\u276E': + // � [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT] + case '\u276F': + // � [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT] + case '\uFF02': // " [FULLWIDTH QUOTATION MARK] + output[opos++] = '"'; + break; + + case '\u2018': + // ‘ [LEFT SINGLE QUOTATION MARK] + case '\u2019': + // ’ [RIGHT SINGLE QUOTATION MARK] + case '\u201A': + // ‚ [SINGLE LOW-9 QUOTATION MARK] + case '\u201B': + // ‛ [SINGLE HIGH-REVERSED-9 QUOTATION MARK] + case '\u2032': + // ′ [PRIME] + case '\u2035': + // ‵ [REVERSED PRIME] + case '\u2039': + // ‹ [SINGLE LEFT-POINTING ANGLE QUOTATION MARK] + case '\u203A': + // › [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK] + case '\u275B': + // � [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT] + case '\u275C': + // � [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT] + case '\uFF07': // ' [FULLWIDTH APOSTROPHE] + output[opos++] = '\''; + break; + + case '\u2010': + // � [HYPHEN] + case '\u2011': + // ‑ [NON-BREAKING HYPHEN] + case '\u2012': + // ‒ [FIGURE DASH] + case '\u2013': + // – [EN DASH] + case '\u2014': + // �? [EM DASH] + case '\u207B': + // � [SUPERSCRIPT MINUS] + case '\u208B': + // â‚‹ [SUBSCRIPT MINUS] + case '\uFF0D': // � [FULLWIDTH HYPHEN-MINUS] + output[opos++] = '-'; + break; + + case '\u2045': + // � [LEFT SQUARE BRACKET WITH QUILL] + case '\u2772': + // � [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT] + case '\uFF3B': // ï¼» [FULLWIDTH LEFT SQUARE BRACKET] + output[opos++] = '['; + break; + + case '\u2046': + // � [RIGHT SQUARE BRACKET WITH QUILL] + case '\u2773': + // � [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT] + case '\uFF3D': // ï¼½ [FULLWIDTH RIGHT SQUARE BRACKET] + output[opos++] = ']'; + break; + + case '\u207D': + // � [SUPERSCRIPT LEFT PARENTHESIS] + case '\u208D': + // � [SUBSCRIPT LEFT PARENTHESIS] + case '\u2768': + // � [MEDIUM LEFT PARENTHESIS ORNAMENT] + case '\u276A': + // � [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT] + case '\uFF08': // ( [FULLWIDTH LEFT PARENTHESIS] + output[opos++] = '('; + break; + + case '\u2E28': // ⸨ [LEFT DOUBLE PARENTHESIS] + output[opos++] = '('; + output[opos++] = '('; + break; + + case '\u207E': + // � [SUPERSCRIPT RIGHT PARENTHESIS] + case '\u208E': + // ₎ [SUBSCRIPT RIGHT PARENTHESIS] + case '\u2769': + // � [MEDIUM RIGHT PARENTHESIS ORNAMENT] + case '\u276B': + // � [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT] + case '\uFF09': // ) [FULLWIDTH RIGHT PARENTHESIS] + output[opos++] = ')'; + break; + + case '\u2E29': // ⸩ [RIGHT DOUBLE PARENTHESIS] + output[opos++] = ')'; + output[opos++] = ')'; + break; + + case '\u276C': + // � [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT] + case '\u2770': + // � [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT] + case '\uFF1C': // < [FULLWIDTH LESS-THAN SIGN] + output[opos++] = '<'; + break; + + case '\u276D': + // � [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT] + case '\u2771': + // � [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT] + case '\uFF1E': // > [FULLWIDTH GREATER-THAN SIGN] + output[opos++] = '>'; + break; + + case '\u2774': + // � [MEDIUM LEFT CURLY BRACKET ORNAMENT] + case '\uFF5B': // ï½› [FULLWIDTH LEFT CURLY BRACKET] + output[opos++] = '{'; + break; + + case '\u2775': + // � [MEDIUM RIGHT CURLY BRACKET ORNAMENT] + case '\uFF5D': // � [FULLWIDTH RIGHT CURLY BRACKET] + output[opos++] = '}'; + break; + + case '\u207A': + // � [SUPERSCRIPT PLUS SIGN] + case '\u208A': + // ₊ [SUBSCRIPT PLUS SIGN] + case '\uFF0B': // + [FULLWIDTH PLUS SIGN] + output[opos++] = '+'; + break; + + case '\u207C': + // � [SUPERSCRIPT EQUALS SIGN] + case '\u208C': + // ₌ [SUBSCRIPT EQUALS SIGN] + case '\uFF1D': // � [FULLWIDTH EQUALS SIGN] + output[opos++] = '='; + break; + + case '\uFF01': // � [FULLWIDTH EXCLAMATION MARK] + output[opos++] = '!'; + break; + + case '\u203C': // ‼ [DOUBLE EXCLAMATION MARK] + output[opos++] = '!'; + output[opos++] = '!'; + break; + + case '\u2049': // � [EXCLAMATION QUESTION MARK] + output[opos++] = '!'; + output[opos++] = '?'; + break; + + case '\uFF03': // # [FULLWIDTH NUMBER SIGN] + output[opos++] = '#'; + break; + + case '\uFF04': // $ [FULLWIDTH DOLLAR SIGN] + output[opos++] = '$'; + break; + + case '\u2052': + // � [COMMERCIAL MINUS SIGN] + case '\uFF05': // ï¼… [FULLWIDTH PERCENT SIGN] + output[opos++] = '%'; + break; + + case '\uFF06': // & [FULLWIDTH AMPERSAND] + output[opos++] = '&'; + break; + + case '\u204E': + // � [LOW ASTERISK] + case '\uFF0A': // * [FULLWIDTH ASTERISK] + output[opos++] = '*'; + break; + + case '\uFF0C': // , [FULLWIDTH COMMA] + output[opos++] = ','; + break; + + case '\uFF0E': // . [FULLWIDTH FULL STOP] + output[opos++] = '.'; + break; + + case '\u2044': + // � [FRACTION SLASH] + case '\uFF0F': // � [FULLWIDTH SOLIDUS] + output[opos++] = '/'; + break; + + case '\uFF1A': // : [FULLWIDTH COLON] + output[opos++] = ':'; + break; + + case '\u204F': + // � [REVERSED SEMICOLON] + case '\uFF1B': // ï¼› [FULLWIDTH SEMICOLON] + output[opos++] = ';'; + break; + + case '\uFF1F': // ? [FULLWIDTH QUESTION MARK] + output[opos++] = '?'; + break; + + case '\u2047': // � [DOUBLE QUESTION MARK] + output[opos++] = '?'; + output[opos++] = '?'; + break; + + case '\u2048': // � [QUESTION EXCLAMATION MARK] + output[opos++] = '?'; + output[opos++] = '!'; + break; + + case '\uFF20': // ï¼  [FULLWIDTH COMMERCIAL AT] + output[opos++] = '@'; + break; + + case '\uFF3C': // ï¼¼ [FULLWIDTH REVERSE SOLIDUS] + output[opos++] = '\\'; + break; + + case '\u2038': + // ‸ [CARET] + case '\uFF3E': // ï¼¾ [FULLWIDTH CIRCUMFLEX ACCENT] + output[opos++] = '^'; + break; + + case '\uFF3F': // _ [FULLWIDTH LOW LINE] + output[opos++] = '_'; + break; + + case '\u2053': + // � [SWUNG DASH] + case '\uFF5E': // ~ [FULLWIDTH TILDE] + output[opos++] = '~'; + break; + + // BEGIN CUSTOM TRANSLITERATION OF CYRILIC CHARS + + // russian uppercase "А Б В Г Д Е Ё Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я" + // russian lowercase "а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я" + + // notes + // read http://www.vesic.org/english/blog/c-sharp/transliteration-easy-way-microsoft-transliteration-utility/ + // should we look into MS Transliteration Utility (http://msdn.microsoft.com/en-US/goglobal/bb688104.aspx) + // also UnicodeSharpFork https://bitbucket.org/DimaStefantsov/unidecodesharpfork + // also Transliterator http://transliterator.codeplex.com/ + // + // in any case it would be good to generate all those "case" statements instead of writing them by hand + // time for a T4 template? + // also we should support extensibility so ppl can register more cases in external code + + // TODO: transliterates Анастасия as Anastasiya, and not Anastasia + // Ольга --> Ol'ga, Татьяна --> Tat'yana -- that's bad (?) + // Note: should ä (German umlaut) become a or ae ? + case '\u0410': // А + output[opos++] = 'A'; + break; + case '\u0430': // а + output[opos++] = 'a'; + break; + case '\u0411': // Б + output[opos++] = 'B'; + break; + case '\u0431': // б + output[opos++] = 'b'; + break; + case '\u0412': // В + output[opos++] = 'V'; + break; + case '\u0432': // в + output[opos++] = 'v'; + break; + case '\u0413': // Г + output[opos++] = 'G'; + break; + case '\u0433': // г + output[opos++] = 'g'; + break; + case '\u0414': // Д + output[opos++] = 'D'; + break; + case '\u0434': // д + output[opos++] = 'd'; + break; + case '\u0415': // Е + output[opos++] = 'E'; + break; + case '\u0435': // е + output[opos++] = 'e'; + break; + case '\u0401': // Ё + output[opos++] = 'E'; // alt. Yo + break; + case '\u0451': // ё + output[opos++] = 'e'; // alt. yo + break; + case '\u0416': // Ж + output[opos++] = 'Z'; + output[opos++] = 'h'; + break; + case '\u0436': // ж + output[opos++] = 'z'; + output[opos++] = 'h'; + break; + case '\u0417': // З + output[opos++] = 'Z'; + break; + case '\u0437': // з + output[opos++] = 'z'; + break; + case '\u0418': // И + output[opos++] = 'I'; + break; + case '\u0438': // и + output[opos++] = 'i'; + break; + case '\u0419': // Й + output[opos++] = 'I'; // alt. Y, J + break; + case '\u0439': // й + output[opos++] = 'i'; // alt. y, j + break; + case '\u041A': // К + output[opos++] = 'K'; + break; + case '\u043A': // к + output[opos++] = 'k'; + break; + case '\u041B': // Л + output[opos++] = 'L'; + break; + case '\u043B': // л + output[opos++] = 'l'; + break; + case '\u041C': // М + output[opos++] = 'M'; + break; + case '\u043C': // м + output[opos++] = 'm'; + break; + case '\u041D': // Н + output[opos++] = 'N'; + break; + case '\u043D': // н + output[opos++] = 'n'; + break; + case '\u041E': // О + output[opos++] = 'O'; + break; + case '\u043E': // о + output[opos++] = 'o'; + break; + case '\u041F': // П + output[opos++] = 'P'; + break; + case '\u043F': // п + output[opos++] = 'p'; + break; + case '\u0420': // Р + output[opos++] = 'R'; + break; + case '\u0440': // р + output[opos++] = 'r'; + break; + case '\u0421': // С + output[opos++] = 'S'; + break; + case '\u0441': // с + output[opos++] = 's'; + break; + case '\u0422': // Т + output[opos++] = 'T'; + break; + case '\u0442': // т + output[opos++] = 't'; + break; + case '\u0423': // У + output[opos++] = 'U'; + break; + case '\u0443': // у + output[opos++] = 'u'; + break; + case '\u0424': // Ф + output[opos++] = 'F'; + break; + case '\u0444': // ф + output[opos++] = 'f'; + break; + case '\u0425': // Х + output[opos++] = 'K'; // alt. X + output[opos++] = 'h'; + break; + case '\u0445': // х + output[opos++] = 'k'; // alt. x + output[opos++] = 'h'; + break; + case '\u0426': // Ц + output[opos++] = 'F'; + break; + case '\u0446': // ц + output[opos++] = 'f'; + break; + case '\u0427': // Ч + output[opos++] = 'C'; // alt. Ts, C + output[opos++] = 'h'; + break; + case '\u0447': // ч + output[opos++] = 'c'; // alt. ts, c + output[opos++] = 'h'; + break; + case '\u0428': // Ш + output[opos++] = 'S'; // alt. Ch, S + output[opos++] = 'h'; + break; + case '\u0448': // ш + output[opos++] = 's'; // alt. ch, s + output[opos++] = 'h'; + break; + case '\u0429': // Щ + output[opos++] = 'S'; // alt. Shch, Sc + output[opos++] = 'h'; + break; + case '\u0449': // щ + output[opos++] = 's'; // alt. shch, sc + output[opos++] = 'h'; + break; + case '\u042A': // Ъ + output[opos++] = '"'; // " + break; + case '\u044A': // ъ + output[opos++] = '"'; // " + break; + case '\u042B': // Ы + output[opos++] = 'Y'; + break; + case '\u044B': // ы + output[opos++] = 'y'; + break; + case '\u042C': // Ь + output[opos++] = '\''; // ' + break; + case '\u044C': // ь + output[opos++] = '\''; // ' + break; + case '\u042D': // Э + output[opos++] = 'E'; + break; + case '\u044D': // э + output[opos++] = 'e'; + break; + case '\u042E': // Ю + output[opos++] = 'Y'; // alt. Ju + output[opos++] = 'u'; + break; + case '\u044E': // ю + output[opos++] = 'y'; // alt. ju + output[opos++] = 'u'; + break; + case '\u042F': // Я + output[opos++] = 'Y'; // alt. Ja + output[opos++] = 'a'; + break; + case '\u044F': // я + output[opos++] = 'y'; // alt. ja + output[opos++] = 'a'; + break; + + // BEGIN EXTRA + /* + case '£': + output[opos++] = 'G'; + output[opos++] = 'B'; + output[opos++] = 'P'; + break; + + case '€': + output[opos++] = 'E'; + output[opos++] = 'U'; + output[opos++] = 'R'; + break; + + case '©': + output[opos++] = '('; + output[opos++] = 'C'; + output[opos++] = ')'; + break; + */ + default: + // if (ToMoreAscii(input, ipos, output, ref opos)) + // break; + + // if (!char.IsLetterOrDigit(c)) // that would not catch eg 汉 unfortunately + // output[opos++] = '?'; + // else + // output[opos++] = c; + + // strict ASCII + output[opos++] = fail; + + break; + } + } + } + + // private static bool ToMoreAscii(char[] input, int ipos, char[] output, ref int opos) + // { + // var c = input[ipos]; + + // switch (c) + // { + // case '£': + // output[opos++] = 'G'; + // output[opos++] = 'B'; + // output[opos++] = 'P'; + // break; + + // case '€': + // output[opos++] = 'E'; + // output[opos++] = 'U'; + // output[opos++] = 'R'; + // break; + + // case '©': + // output[opos++] = '('; + // output[opos++] = 'C'; + // output[opos++] = ')'; + // break; + + // default: + // return false; + // } + + // return true; + // } } diff --git a/src/Umbraco.Core/Sync/ElectedServerRoleAccessor.cs b/src/Umbraco.Core/Sync/ElectedServerRoleAccessor.cs index 340de80c96..09c904b7bc 100644 --- a/src/Umbraco.Core/Sync/ElectedServerRoleAccessor.cs +++ b/src/Umbraco.Core/Sync/ElectedServerRoleAccessor.cs @@ -1,29 +1,30 @@ -using System; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Gets the current server's based on active servers registered with +/// +/// +/// +/// This is the default service which determines a server's role by using a master election process. +/// The scheduling publisher election process doesn't occur until just after startup so this election process doesn't +/// really affect the primary startup phase. +/// +public sealed class ElectedServerRoleAccessor : IServerRoleAccessor { + private readonly IServerRegistrationService _registrationService; + /// - /// Gets the current server's based on active servers registered with + /// Initializes a new instance of the class. /// - /// - /// This is the default service which determines a server's role by using a master election process. - /// The scheduling publisher election process doesn't occur until just after startup so this election process doesn't really affect the primary startup phase. - /// - public sealed class ElectedServerRoleAccessor : IServerRoleAccessor - { - private readonly IServerRegistrationService _registrationService; + /// The registration service. + /// Some options. + public ElectedServerRoleAccessor(IServerRegistrationService registrationService) => _registrationService = + registrationService ?? throw new ArgumentNullException(nameof(registrationService)); - /// - /// Initializes a new instance of the class. - /// - /// The registration service. - /// Some options. - public ElectedServerRoleAccessor(IServerRegistrationService registrationService) => _registrationService = registrationService ?? throw new ArgumentNullException(nameof(registrationService)); - - /// - /// Gets the role of the current server in the application environment. - /// - public ServerRole CurrentServerRole => _registrationService.GetCurrentServerRole(); - } + /// + /// Gets the role of the current server in the application environment. + /// + public ServerRole CurrentServerRole => _registrationService.GetCurrentServerRole(); } diff --git a/src/Umbraco.Core/Sync/IServerAddress.cs b/src/Umbraco.Core/Sync/IServerAddress.cs index 4de7490d8f..cc9da01db0 100644 --- a/src/Umbraco.Core/Sync/IServerAddress.cs +++ b/src/Umbraco.Core/Sync/IServerAddress.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Provides the address of a server. +/// +public interface IServerAddress { /// - /// Provides the address of a server. + /// Gets the server address. /// - public interface IServerAddress - { - /// - /// Gets the server address. - /// - string? ServerAddress { get; } + string? ServerAddress { get; } - // TODO: Should probably add things like port, protocol, server name, app id - } + // TODO: Should probably add things like port, protocol, server name, app id } diff --git a/src/Umbraco.Core/Sync/IServerMessenger.cs b/src/Umbraco.Core/Sync/IServerMessenger.cs index e58cfe9bc0..49cd397e2d 100644 --- a/src/Umbraco.Core/Sync/IServerMessenger.cs +++ b/src/Umbraco.Core/Sync/IServerMessenger.cs @@ -1,83 +1,81 @@ -using System; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Transmits distributed cache notifications for all servers of a load balanced environment. +/// +/// Also ensures that the notification is processed on the local environment. +public interface IServerMessenger { /// - /// Transmits distributed cache notifications for all servers of a load balanced environment. + /// Called to synchronize a server with queued notifications /// - /// Also ensures that the notification is processed on the local environment. - public interface IServerMessenger - { - /// - /// Called to synchronize a server with queued notifications - /// - void Sync(); + void Sync(); - /// - /// Called to send/commit the queued messages created with the Perform methods - /// - void SendMessages(); + /// + /// Called to send/commit the queued messages created with the Perform methods + /// + void SendMessages(); - /// - /// Notifies the distributed cache, for a specified . - /// - /// The ICacheRefresher. - /// The notification content. - void QueueRefresh(ICacheRefresher refresher, TPayload[] payload); + /// + /// Notifies the distributed cache, for a specified . + /// + /// The ICacheRefresher. + /// The notification content. + void QueueRefresh(ICacheRefresher refresher, TPayload[] payload); - /// - /// Notifies the distributed cache of specified item invalidation, for a specified . - /// - /// The type of the invalidated items. - /// The ICacheRefresher. - /// A function returning the unique identifier of items. - /// The invalidated items. - void QueueRefresh(ICacheRefresher refresher, Func getNumericId, params T[] instances); + /// + /// Notifies the distributed cache of specified item invalidation, for a specified . + /// + /// The type of the invalidated items. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. + void QueueRefresh(ICacheRefresher refresher, Func getNumericId, params T[] instances); - /// - /// Notifies the distributed cache of specified item invalidation, for a specified . - /// - /// The type of the invalidated items. - /// The ICacheRefresher. - /// A function returning the unique identifier of items. - /// The invalidated items. - void QueueRefresh(ICacheRefresher refresher, Func getGuidId, params T[] instances); + /// + /// Notifies the distributed cache of specified item invalidation, for a specified . + /// + /// The type of the invalidated items. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. + void QueueRefresh(ICacheRefresher refresher, Func getGuidId, params T[] instances); - /// - /// Notifies all servers of specified items removal, for a specified . - /// - /// The type of the removed items. - /// The ICacheRefresher. - /// A function returning the unique identifier of items. - /// The removed items. - void QueueRemove(ICacheRefresher refresher, Func getNumericId, params T[] instances); + /// + /// Notifies all servers of specified items removal, for a specified . + /// + /// The type of the removed items. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The removed items. + void QueueRemove(ICacheRefresher refresher, Func getNumericId, params T[] instances); - /// - /// Notifies all servers of specified items removal, for a specified . - /// - /// The ICacheRefresher. - /// The unique identifiers of the removed items. - void QueueRemove(ICacheRefresher refresher, params int[] numericIds); + /// + /// Notifies all servers of specified items removal, for a specified . + /// + /// The ICacheRefresher. + /// The unique identifiers of the removed items. + void QueueRemove(ICacheRefresher refresher, params int[] numericIds); - /// - /// Notifies all servers of specified items invalidation, for a specified . - /// - /// The ICacheRefresher. - /// The unique identifiers of the invalidated items. - void QueueRefresh(ICacheRefresher refresher, params int[] numericIds); + /// + /// Notifies all servers of specified items invalidation, for a specified . + /// + /// The ICacheRefresher. + /// The unique identifiers of the invalidated items. + void QueueRefresh(ICacheRefresher refresher, params int[] numericIds); - /// - /// Notifies all servers of specified items invalidation, for a specified . - /// - /// The ICacheRefresher. - /// The unique identifiers of the invalidated items. - void QueueRefresh(ICacheRefresher refresher, params Guid[] guidIds); + /// + /// Notifies all servers of specified items invalidation, for a specified . + /// + /// The ICacheRefresher. + /// The unique identifiers of the invalidated items. + void QueueRefresh(ICacheRefresher refresher, params Guid[] guidIds); - /// - /// Notifies all servers of a global invalidation for a specified . - /// - /// The ICacheRefresher. - void QueueRefreshAll(ICacheRefresher refresher); - } + /// + /// Notifies all servers of a global invalidation for a specified . + /// + /// The ICacheRefresher. + void QueueRefreshAll(ICacheRefresher refresher); } diff --git a/src/Umbraco.Core/Sync/IServerRoleAccessor.cs b/src/Umbraco.Core/Sync/IServerRoleAccessor.cs index 1ebd59b26d..aed70b0f50 100644 --- a/src/Umbraco.Core/Sync/IServerRoleAccessor.cs +++ b/src/Umbraco.Core/Sync/IServerRoleAccessor.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Gets the current server's +/// +public interface IServerRoleAccessor { /// - /// Gets the current server's + /// Gets the role of the current server in the application environment. /// - public interface IServerRoleAccessor - { - /// - /// Gets the role of the current server in the application environment. - /// - ServerRole CurrentServerRole { get; } - } + ServerRole CurrentServerRole { get; } } diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs index 0c616a4e68..1d7d085f90 100644 --- a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Retrieve the for the application during startup +/// +public interface ISyncBootStateAccessor { /// - /// Retrieve the for the application during startup + /// Get the /// - public interface ISyncBootStateAccessor - { - /// - /// Get the - /// - /// - SyncBootState GetSyncBootState(); - } + /// + SyncBootState GetSyncBootState(); } diff --git a/src/Umbraco.Core/Sync/MessageType.cs b/src/Umbraco.Core/Sync/MessageType.cs index 5164428632..282aebeb54 100644 --- a/src/Umbraco.Core/Sync/MessageType.cs +++ b/src/Umbraco.Core/Sync/MessageType.cs @@ -1,16 +1,15 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// The message type to be used for syncing across servers. +/// +public enum MessageType { - /// - /// The message type to be used for syncing across servers. - /// - public enum MessageType - { - RefreshAll, - RefreshById, - RefreshByJson, - RemoveById, - RefreshByInstance, - RemoveByInstance, - RefreshByPayload - } + RefreshAll, + RefreshById, + RefreshByJson, + RemoveById, + RefreshByInstance, + RemoveByInstance, + RefreshByPayload, } diff --git a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs index 0dcfa471db..4040edd8f7 100644 --- a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Boot state implementation for when umbraco is not in the run state +/// +public sealed class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor { - /// - /// Boot state implementation for when umbraco is not in the run state - /// - public sealed class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor - { - public SyncBootState GetSyncBootState() => SyncBootState.Unknown; - } + public SyncBootState GetSyncBootState() => SyncBootState.Unknown; } diff --git a/src/Umbraco.Core/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs index b8609410ab..2a80dbf95f 100644 --- a/src/Umbraco.Core/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -1,217 +1,220 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Serialization; -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +[Serializable] +public class RefreshInstruction { - [Serializable] - public class RefreshInstruction + // NOTE + // that class should be refactored + // but at the moment it is exposed in CacheRefresher webservice + // so for the time being we keep it as-is for backward compatibility reasons + + // need this public, parameter-less constructor so the web service messenger + // can de-serialize the instructions it receives + + /// + /// Initializes a new instance of the class. + /// + /// + /// Need this public, parameter-less constructor so the web service messenger can de-serialize the instructions it + /// receives. + /// + public RefreshInstruction() => + + // Set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db + JsonIdCount = 1; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Need this public one so it can be de-serialized - used by the Json thing + /// otherwise, should use GetInstructions(...) + /// + public RefreshInstruction(Guid refresherId, RefreshMethodType refreshType, Guid guidId, int intId, string jsonIds, string jsonPayload) + : this() { - // NOTE - // that class should be refactored - // but at the moment it is exposed in CacheRefresher webservice - // so for the time being we keep it as-is for backward compatibility reasons - - // need this public, parameter-less constructor so the web service messenger - // can de-serialize the instructions it receives - - /// - /// Initializes a new instance of the class. - /// - /// - /// Need this public, parameter-less constructor so the web service messenger can de-serialize the instructions it receives. - /// - public RefreshInstruction() => - - // Set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db - JsonIdCount = 1; - - /// - /// Initializes a new instance of the class. - /// - /// - /// Need this public one so it can be de-serialized - used by the Json thing - /// otherwise, should use GetInstructions(...) - /// - public RefreshInstruction(Guid refresherId, RefreshMethodType refreshType, Guid guidId, int intId, string jsonIds, string jsonPayload) - : this() - { - RefresherId = refresherId; - RefreshType = refreshType; - GuidId = guidId; - IntId = intId; - JsonIds = jsonIds; - JsonPayload = jsonPayload; - } - - private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) - : this() - { - RefresherId = refresher.RefresherUniqueId; - RefreshType = refreshType; - } - - private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, Guid guidId) - : this(refresher, refreshType) => GuidId = guidId; - - private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, int intId) - : this(refresher, refreshType) => IntId = intId; - - /// - /// A private constructor to create a new instance - /// - /// - /// When the refresh method is we know how many Ids are being refreshed so we know the instruction - /// count which will be taken into account when we store this count in the database. - /// - private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, string? json, int idCount = 1) - : this(refresher, refreshType) - { - JsonIdCount = idCount; - - if (refreshType == RefreshMethodType.RefreshByJson) - { - JsonPayload = json; - } - else - { - JsonIds = json; - } - } - - public static IEnumerable GetInstructions( - ICacheRefresher refresher, - IJsonSerializer jsonSerializer, - MessageType messageType, - IEnumerable? ids, - Type? idType, - string? json) - { - switch (messageType) - { - case MessageType.RefreshAll: - return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshAll) }; - - case MessageType.RefreshByJson: - return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByJson, json) }; - - case MessageType.RefreshById: - if (idType == null) - { - throw new InvalidOperationException("Cannot refresh by id if idType is null."); - } - - if (idType == typeof(int)) - { - // Bulk of ints is supported - var intIds = ids?.Cast().ToArray(); - return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, jsonSerializer.Serialize(intIds), intIds?.Length ?? 0) }; - } - - // Else must be guids, bulk of guids is not supported, so iterate. - return ids?.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RefreshByGuid, (Guid) x)) ?? Enumerable.Empty(); - - case MessageType.RemoveById: - if (idType == null) - { - throw new InvalidOperationException("Cannot remove by id if idType is null."); - } - - // Must be ints, bulk-remove is not supported, so iterate. - return ids?.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RemoveById, (int) x)) ?? Enumerable.Empty(); - //return new[] { new RefreshInstruction(refresher, RefreshMethodType.RemoveByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; - - default: - //case MessageType.RefreshByInstance: - //case MessageType.RemoveByInstance: - throw new ArgumentOutOfRangeException("messageType"); - } - } - - /// - /// Gets or sets the refresh action type. - /// - public RefreshMethodType RefreshType { get; set; } - - /// - /// Gets or sets the refresher unique identifier. - /// - public Guid RefresherId { get; set; } - - /// - /// Gets or sets the Guid data value. - /// - public Guid GuidId { get; set; } - - /// - /// Gets or sets the int data value. - /// - public int IntId { get; set; } - - /// - /// Gets or sets the ids data value. - /// - public string? JsonIds { get; set; } - - /// - /// Gets or sets the number of Ids contained in the JsonIds json value. - /// - /// - /// This is used to determine the instruction count per row. - /// - public int JsonIdCount { get; set; } - - /// - /// Gets or sets the payload data value. - /// - public string? JsonPayload { get; set; } - - protected bool Equals(RefreshInstruction other) => - RefreshType == other.RefreshType - && RefresherId.Equals(other.RefresherId) - && GuidId.Equals(other.GuidId) - && IntId == other.IntId - && string.Equals(JsonIds, other.JsonIds) - && string.Equals(JsonPayload, other.JsonPayload); - - public override bool Equals(object? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - if (other.GetType() != GetType()) - { - return false; - } - - return Equals((RefreshInstruction) other); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = (int) RefreshType; - hashCode = (hashCode*397) ^ RefresherId.GetHashCode(); - hashCode = (hashCode*397) ^ GuidId.GetHashCode(); - hashCode = (hashCode*397) ^ IntId; - hashCode = (hashCode*397) ^ (JsonIds != null ? JsonIds.GetHashCode() : 0); - hashCode = (hashCode*397) ^ (JsonPayload != null ? JsonPayload.GetHashCode() : 0); - return hashCode; - } - } - - public static bool operator ==(RefreshInstruction left, RefreshInstruction right) => Equals(left, right); - - public static bool operator !=(RefreshInstruction left, RefreshInstruction right) => Equals(left, right) == false; + RefresherId = refresherId; + RefreshType = refreshType; + GuidId = guidId; + IntId = intId; + JsonIds = jsonIds; + JsonPayload = jsonPayload; } + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) + : this() + { + RefresherId = refresher.RefresherUniqueId; + RefreshType = refreshType; + } + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, Guid guidId) + : this(refresher, refreshType) => GuidId = guidId; + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, int intId) + : this(refresher, refreshType) => IntId = intId; + + /// + /// A private constructor to create a new instance + /// + /// + /// When the refresh method is we know how many Ids are being refreshed + /// so we know the instruction + /// count which will be taken into account when we store this count in the database. + /// + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, string? json, int idCount = 1) + : this(refresher, refreshType) + { + JsonIdCount = idCount; + + if (refreshType == RefreshMethodType.RefreshByJson) + { + JsonPayload = json; + } + else + { + JsonIds = json; + } + } + + /// + /// Gets or sets the refresh action type. + /// + public RefreshMethodType RefreshType { get; set; } + + /// + /// Gets or sets the refresher unique identifier. + /// + public Guid RefresherId { get; set; } + + /// + /// Gets or sets the Guid data value. + /// + public Guid GuidId { get; set; } + + /// + /// Gets or sets the int data value. + /// + public int IntId { get; set; } + + /// + /// Gets or sets the ids data value. + /// + public string? JsonIds { get; set; } + + /// + /// Gets or sets the number of Ids contained in the JsonIds json value. + /// + /// + /// This is used to determine the instruction count per row. + /// + public int JsonIdCount { get; set; } + + /// + /// Gets or sets the payload data value. + /// + public string? JsonPayload { get; set; } + + public static bool operator ==(RefreshInstruction left, RefreshInstruction right) => Equals(left, right); + + public static IEnumerable GetInstructions( + ICacheRefresher refresher, + IJsonSerializer jsonSerializer, + MessageType messageType, + IEnumerable? ids, + Type? idType, + string? json) + { + switch (messageType) + { + case MessageType.RefreshAll: + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshAll) }; + + case MessageType.RefreshByJson: + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByJson, json) }; + + case MessageType.RefreshById: + if (idType == null) + { + throw new InvalidOperationException("Cannot refresh by id if idType is null."); + } + + if (idType == typeof(int)) + { + // Bulk of ints is supported + var intIds = ids?.Cast().ToArray(); + return new[] + { + new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, jsonSerializer.Serialize(intIds), intIds?.Length ?? 0), + }; + } + + // Else must be guids, bulk of guids is not supported, so iterate. + return ids?.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RefreshByGuid, (Guid)x)) ?? + Enumerable.Empty(); + + case MessageType.RemoveById: + if (idType == null) + { + throw new InvalidOperationException("Cannot remove by id if idType is null."); + } + + // Must be ints, bulk-remove is not supported, so iterate. + return ids?.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RemoveById, (int)x)) ?? + Enumerable.Empty(); + + // return new[] { new RefreshInstruction(refresher, RefreshMethodType.RemoveByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; + default: + // case MessageType.RefreshByInstance: + // case MessageType.RemoveByInstance: + throw new ArgumentOutOfRangeException("messageType"); + } + } + + public override bool Equals(object? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + { + return false; + } + + return Equals((RefreshInstruction)other); + } + + protected bool Equals(RefreshInstruction other) => + RefreshType == other.RefreshType + && RefresherId.Equals(other.RefresherId) + && GuidId.Equals(other.GuidId) + && IntId == other.IntId + && string.Equals(JsonIds, other.JsonIds) + && string.Equals(JsonPayload, other.JsonPayload); + + public override int GetHashCode() + { + unchecked + { + var hashCode = (int)RefreshType; + hashCode = (hashCode * 397) ^ RefresherId.GetHashCode(); + hashCode = (hashCode * 397) ^ GuidId.GetHashCode(); + hashCode = (hashCode * 397) ^ IntId; + hashCode = (hashCode * 397) ^ (JsonIds != null ? JsonIds.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (JsonPayload != null ? JsonPayload.GetHashCode() : 0); + return hashCode; + } + } + + public static bool operator !=(RefreshInstruction left, RefreshInstruction right) => Equals(left, right) == false; } diff --git a/src/Umbraco.Core/Sync/RefreshMethodType.cs b/src/Umbraco.Core/Sync/RefreshMethodType.cs index bf72423c1f..f249a4701e 100644 --- a/src/Umbraco.Core/Sync/RefreshMethodType.cs +++ b/src/Umbraco.Core/Sync/RefreshMethodType.cs @@ -1,44 +1,40 @@ -using System; +namespace Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Core.Sync +/// +/// Describes refresh action type. +/// +[Serializable] +public enum RefreshMethodType { - /// - /// Describes refresh action type. - /// - [Serializable] - public enum RefreshMethodType - { - // NOTE - // that enum should get merged somehow with MessageType and renamed somehow - // but at the moment it is exposed in CacheRefresher webservice through RefreshInstruction - // so for the time being we keep it as-is for backward compatibility reasons + // NOTE + // that enum should get merged somehow with MessageType and renamed somehow + // but at the moment it is exposed in CacheRefresher webservice through RefreshInstruction + // so for the time being we keep it as-is for backward compatibility reasons + RefreshAll, + RefreshByGuid, + RefreshById, + RefreshByIds, + RefreshByJson, + RemoveById, - RefreshAll, - RefreshByGuid, - RefreshById, - RefreshByIds, - RefreshByJson, - RemoveById, + // would adding values break backward compatibility? + // RemoveByIds - // would adding values break backward compatibility? - //RemoveByIds + // these are MessageType values + // note that AnythingByInstance are local messages and cannot be distributed + /* + RefreshAll, + RefreshById, + RefreshByJson, + RemoveById, + RefreshByInstance, + RemoveByInstance + */ - // these are MessageType values - // note that AnythingByInstance are local messages and cannot be distributed - /* - RefreshAll, - RefreshById, - RefreshByJson, - RemoveById, - RefreshByInstance, - RemoveByInstance - */ - - // NOTE - // in the future we want - // RefreshAll - // RefreshById / ByInstance (support enumeration of int or guid) - // RemoveById / ByInstance (support enumeration of int or guid) - // Notify (for everything JSON) - } + // NOTE + // in the future we want + // RefreshAll + // RefreshById / ByInstance (support enumeration of int or guid) + // RemoveById / ByInstance (support enumeration of int or guid) + // Notify (for everything JSON) } diff --git a/src/Umbraco.Core/Sync/ServerRole.cs b/src/Umbraco.Core/Sync/ServerRole.cs index 9bfd4469b3..15f546fc35 100644 --- a/src/Umbraco.Core/Sync/ServerRole.cs +++ b/src/Umbraco.Core/Sync/ServerRole.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// The role of a server in an application environment. +/// +public enum ServerRole : byte { /// - /// The role of a server in an application environment. + /// The server role is unknown. /// - public enum ServerRole : byte - { - /// - /// The server role is unknown. - /// - Unknown = 0, + Unknown = 0, - /// - /// The server is the single server of a single-server environment. - /// - Single = 1, + /// + /// The server is the single server of a single-server environment. + /// + Single = 1, - /// - /// In a multi-servers environment, the server is a Subscriber server. - /// - Subscriber = 2, + /// + /// In a multi-servers environment, the server is a Subscriber server. + /// + Subscriber = 2, - /// - /// In a multi-servers environment, the server is the Scheduling Publisher. - /// - SchedulingPublisher = 3 - } + /// + /// In a multi-servers environment, the server is the Scheduling Publisher. + /// + SchedulingPublisher = 3, } diff --git a/src/Umbraco.Core/Sync/SingleServerRoleAccessor.cs b/src/Umbraco.Core/Sync/SingleServerRoleAccessor.cs index 2f4e85c5b1..f03f27d9e7 100644 --- a/src/Umbraco.Core/Sync/SingleServerRoleAccessor.cs +++ b/src/Umbraco.Core/Sync/SingleServerRoleAccessor.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +/// +/// Can be used when Umbraco is definitely not operating in a Load Balanced scenario to micro-optimize some startup +/// performance +/// +/// +/// The micro optimization is specifically to avoid a DB query just after the app starts up to determine the +/// +/// which by default is done with scheduling publisher election by a database query. The master election process +/// doesn't occur until just after startup +/// so this micro optimization doesn't really affect the primary startup phase. +/// +public class SingleServerRoleAccessor : IServerRoleAccessor { - /// - /// Can be used when Umbraco is definitely not operating in a Load Balanced scenario to micro-optimize some startup performance - /// - /// - /// The micro optimization is specifically to avoid a DB query just after the app starts up to determine the - /// which by default is done with scheduling publisher election by a database query. The master election process doesn't occur until just after startup - /// so this micro optimization doesn't really affect the primary startup phase. - /// - public class SingleServerRoleAccessor : IServerRoleAccessor - { - public ServerRole CurrentServerRole => ServerRole.Single; - } + public ServerRole CurrentServerRole => ServerRole.Single; } diff --git a/src/Umbraco.Core/Sync/SyncBootState.cs b/src/Umbraco.Core/Sync/SyncBootState.cs index 670930de31..6233ace01a 100644 --- a/src/Umbraco.Core/Sync/SyncBootState.cs +++ b/src/Umbraco.Core/Sync/SyncBootState.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Core.Sync +namespace Umbraco.Cms.Core.Sync; + +public enum SyncBootState { - public enum SyncBootState - { - /// - /// Unknown state. Treat as WarmBoot - /// - Unknown = 0, + /// + /// Unknown state. Treat as WarmBoot + /// + Unknown = 0, - /// - /// Cold boot. No Sync state - /// - ColdBoot = 1, + /// + /// Cold boot. No Sync state + /// + ColdBoot = 1, - /// - /// Warm boot. Sync state present - /// - WarmBoot = 2 - } + /// + /// Warm boot. Sync state present + /// + WarmBoot = 2, } diff --git a/src/Umbraco.Core/SystemLock.cs b/src/Umbraco.Core/SystemLock.cs index d39d6ecbce..0e47096c2e 100644 --- a/src/Umbraco.Core/SystemLock.cs +++ b/src/Umbraco.Core/SystemLock.cs @@ -1,194 +1,181 @@ -using System; -using System.Runtime.ConstrainedExecution; -using System.Threading; -using System.Threading.Tasks; +using System.Runtime.ConstrainedExecution; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-6-asynclock/ +// +// notes: +// - this is NOT a reader/writer lock +// - this is NOT a recursive lock +// +// using a named Semaphore here and not a Mutex because mutexes have thread +// affinity which does not work with async situations +// +// it is important that managed code properly release the Semaphore before +// going down else it will maintain the lock - however note that when the +// whole process (w3wp.exe) goes down and all handles to the Semaphore have +// been closed, the Semaphore system object is destroyed - so in any case +// an iisreset should clean up everything +// +public class SystemLock { - // https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-6-asynclock/ - // - // notes: - // - this is NOT a reader/writer lock - // - this is NOT a recursive lock - // - // using a named Semaphore here and not a Mutex because mutexes have thread - // affinity which does not work with async situations - // - // it is important that managed code properly release the Semaphore before - // going down else it will maintain the lock - however note that when the - // whole process (w3wp.exe) goes down and all handles to the Semaphore have - // been closed, the Semaphore system object is destroyed - so in any case - // an iisreset should clean up everything - // - public class SystemLock + private readonly IDisposable? _releaser; + private readonly Task? _releaserTask; + private readonly SemaphoreSlim? _semaphore; + private readonly Semaphore? _semaphore2; + + public SystemLock() + : this(null) { - private readonly SemaphoreSlim? _semaphore; - private readonly Semaphore? _semaphore2; - private readonly IDisposable? _releaser; - private readonly Task? _releaserTask; + } - public SystemLock() - : this(null) - { } - - public SystemLock(string? name) + public SystemLock(string? name) + { + // WaitOne() waits until count > 0 then decrements count + // Release() increments count + // initial count: the initial count value + // maximum count: the max value of count, and then Release() throws + if (string.IsNullOrWhiteSpace(name)) { - // WaitOne() waits until count > 0 then decrements count - // Release() increments count - // initial count: the initial count value - // maximum count: the max value of count, and then Release() throws + // anonymous semaphore + // use one unique releaser, that will not release the semaphore when finalized + // because the semaphore is destroyed anyway if the app goes down + _semaphore = new SemaphoreSlim(1, 1); // create a local (to the app domain) semaphore + _releaser = new SemaphoreSlimReleaser(_semaphore); + _releaserTask = Task.FromResult(_releaser); + } + else + { + // named semaphore + // use dedicated releasers, that will release the semaphore when finalized + // because the semaphore is system-wide and we cannot leak counts + _semaphore2 = new Semaphore(1, 1, name); // create a system-wide named semaphore + } + } - if (string.IsNullOrWhiteSpace(name)) + public IDisposable? Lock() + { + if (_semaphore != null) + { + _semaphore.Wait(); + } + else + { + _semaphore2?.WaitOne(); + } + + return _releaser ?? CreateReleaser(); // anonymous vs named + } + + private IDisposable? CreateReleaser() => + + // for anonymous semaphore, use the unique releaser, else create a new one + _semaphore != null + ? _releaser // (IDisposable)new SemaphoreSlimReleaser(_semaphore) + : new NamedSemaphoreReleaser(_semaphore2); + + public IDisposable? Lock(int millisecondsTimeout) + { + var entered = _semaphore != null + ? _semaphore.Wait(millisecondsTimeout) + : _semaphore2?.WaitOne(millisecondsTimeout); + if (entered == false) + { + throw new TimeoutException("Failed to enter the lock within timeout."); + } + + return _releaser ?? CreateReleaser(); // anonymous vs named + } + + // note - before making those classes some structs, read + // about "impure methods" and mutating readonly structs... + private class NamedSemaphoreReleaser : CriticalFinalizerObject, IDisposable + { + private readonly Semaphore? _semaphore; + + // This code added to correctly implement the disposable pattern. + private bool _disposedValue; // To detect redundant calls + + internal NamedSemaphoreReleaser(Semaphore? semaphore) => _semaphore = semaphore; + + // we WANT to release the semaphore because it's a system object, ie a critical + // non-managed resource - and if it is not released then noone else can acquire + // the lock - so we inherit from CriticalFinalizerObject which means that the + // finalizer "should" run in all situations - there is always a chance that it + // does not run and the semaphore remains "acquired" but then chances are the + // whole process (w3wp.exe...) is going down, at which point the semaphore will + // be destroyed by Windows. + + // however, the semaphore is a managed object, and so when the finalizer runs it + // might have been finalized already, and then we get a, ObjectDisposedException + // in the finalizer - which is bad. + + // in order to prevent this we do two things + // - use a GCHandler to ensure the semaphore is still there when the finalizer + // runs, so we can actually release it + // - wrap the finalizer code in a try...catch to make sure it never throws + ~NamedSemaphoreReleaser() + { + try { - // anonymous semaphore - // use one unique releaser, that will not release the semaphore when finalized - // because the semaphore is destroyed anyway if the app goes down - - _semaphore = new SemaphoreSlim(1, 1); // create a local (to the app domain) semaphore - _releaser = new SemaphoreSlimReleaser(_semaphore); - _releaserTask = Task.FromResult(_releaser); + Dispose(false); } - else + catch { - // named semaphore - // use dedicated releasers, that will release the semaphore when finalized - // because the semaphore is system-wide and we cannot leak counts - - _semaphore2 = new Semaphore(1, 1, name); // create a system-wide named semaphore + // we do NOT want the finalizer to throw - never ever } } - private IDisposable? CreateReleaser() + public void Dispose() { - // for anonymous semaphore, use the unique releaser, else create a new one - return _semaphore != null - ? _releaser // (IDisposable)new SemaphoreSlimReleaser(_semaphore) - : new NamedSemaphoreReleaser(_semaphore2); + Dispose(true); + GC.SuppressFinalize(this); // finalize will not run } - public IDisposable? Lock() + private void Dispose(bool disposing) { - if (_semaphore != null) - _semaphore.Wait(); - else - _semaphore2?.WaitOne(); - return _releaser ?? CreateReleaser(); // anonymous vs named - } - - public IDisposable? Lock(int millisecondsTimeout) - { - var entered = _semaphore != null - ? _semaphore.Wait(millisecondsTimeout) - : _semaphore2?.WaitOne(millisecondsTimeout); - if (entered == false) - throw new TimeoutException("Failed to enter the lock within timeout."); - return _releaser ?? CreateReleaser(); // anonymous vs named - } - - // note - before making those classes some structs, read - // about "impure methods" and mutating readonly structs... - - private class NamedSemaphoreReleaser : CriticalFinalizerObject, IDisposable - { - private readonly Semaphore? _semaphore; - - internal NamedSemaphoreReleaser(Semaphore? semaphore) - { - _semaphore = semaphore; - } - - #region IDisposable Support - - // This code added to correctly implement the disposable pattern. - - private bool disposedValue = false; // To detect redundant calls - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); // finalize will not run - } - - private void Dispose(bool disposing) - { - if (!disposedValue) - { - try - { - _semaphore?.Release(); - } - finally - { - try - { - _semaphore?.Dispose(); - } - catch { } - } - disposedValue = true; - } - } - - // we WANT to release the semaphore because it's a system object, ie a critical - // non-managed resource - and if it is not released then noone else can acquire - // the lock - so we inherit from CriticalFinalizerObject which means that the - // finalizer "should" run in all situations - there is always a chance that it - // does not run and the semaphore remains "acquired" but then chances are the - // whole process (w3wp.exe...) is going down, at which point the semaphore will - // be destroyed by Windows. - - // however, the semaphore is a managed object, and so when the finalizer runs it - // might have been finalized already, and then we get a, ObjectDisposedException - // in the finalizer - which is bad. - - // in order to prevent this we do two things - // - use a GCHandler to ensure the semaphore is still there when the finalizer - // runs, so we can actually release it - // - wrap the finalizer code in a try...catch to make sure it never throws - - ~NamedSemaphoreReleaser() + if (!_disposedValue) { try { - Dispose(false); + _semaphore?.Release(); } - catch + finally { - // we do NOT want the finalizer to throw - never ever + try + { + _semaphore?.Dispose(); + } + catch + { + } } + + _disposedValue = true; } + } + } - #endregion + private class SemaphoreSlimReleaser : IDisposable + { + private readonly SemaphoreSlim _semaphore; + internal SemaphoreSlimReleaser(SemaphoreSlim semaphore) => _semaphore = semaphore; + + ~SemaphoreSlimReleaser() => Dispose(false); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } - private class SemaphoreSlimReleaser : IDisposable + private void Dispose(bool disposing) { - private readonly SemaphoreSlim _semaphore; - - internal SemaphoreSlimReleaser(SemaphoreSlim semaphore) + if (disposing) { - _semaphore = semaphore; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - // normal - _semaphore.Release(); - } - } - - ~SemaphoreSlimReleaser() - { - Dispose(false); + // normal + _semaphore.Release(); } } } diff --git a/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs index 7fd0ee5a85..bd41914010 100644 --- a/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs +++ b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs @@ -1,31 +1,27 @@ -using System; +namespace Umbraco.Cms.Core.Telemetry; -namespace Umbraco.Cms.Core.Telemetry +/// +/// Used to get and create the site identifier +/// +public interface ISiteIdentifierService { /// - /// Used to get and create the site identifier + /// Tries to get the site identifier /// - public interface ISiteIdentifierService - { + /// True if success. + bool TryGetSiteIdentifier(out Guid siteIdentifier); - /// - /// Tries to get the site identifier - /// - /// True if success. - bool TryGetSiteIdentifier(out Guid siteIdentifier); + /// + /// Creates the site identifier and writes it to config. + /// + /// asd. + /// True if success. + bool TryCreateSiteIdentifier(out Guid createdGuid); - /// - /// Creates the site identifier and writes it to config. - /// - /// asd. - /// True if success. - bool TryCreateSiteIdentifier(out Guid createdGuid); - - /// - /// Tries to get the site identifier or otherwise create it if it doesn't exist. - /// - /// The out parameter for the existing or create site identifier. - /// True if success. - bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier); - } + /// + /// Tries to get the site identifier or otherwise create it if it doesn't exist. + /// + /// The out parameter for the existing or create site identifier. + /// True if success. + bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier); } diff --git a/src/Umbraco.Core/Telemetry/ITelemetryService.cs b/src/Umbraco.Core/Telemetry/ITelemetryService.cs index bb832bfd7e..23b0d154a4 100644 --- a/src/Umbraco.Core/Telemetry/ITelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/ITelemetryService.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Core.Telemetry.Models; -namespace Umbraco.Cms.Core.Telemetry +namespace Umbraco.Cms.Core.Telemetry; + +/// +/// Service which gathers the data for telemetry reporting +/// +public interface ITelemetryService { /// - /// Service which gathers the data for telemetry reporting + /// Try and get the /// - public interface ITelemetryService - { - /// - /// Try and get the - /// - bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData); - } + bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData); } diff --git a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs index 53dc6d1a6e..53c07766e8 100644 --- a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs +++ b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs @@ -1,26 +1,25 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Telemetry.Models +namespace Umbraco.Cms.Core.Telemetry.Models; + +/// +/// Serializable class containing information about an installed package. +/// +[DataContract(Name = "packageTelemetry")] +public class PackageTelemetry { /// - /// Serializable class containing information about an installed package. + /// Gets or sets the name of the installed package. /// - [DataContract(Name = "packageTelemetry")] - public class PackageTelemetry - { - /// - /// Gets or sets the name of the installed package. - /// - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - /// - /// Gets or sets the version of the installed package. - /// - /// - /// This may be an empty string if no version is specified, or if package telemetry has been restricted. - /// - [DataMember(Name = "version")] - public string? Version { get; set; } - } + /// + /// Gets or sets the version of the installed package. + /// + /// + /// This may be an empty string if no version is specified, or if package telemetry has been restricted. + /// + [DataMember(Name = "version")] + public string? Version { get; set; } } diff --git a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs index ea6ff63f91..31bab02f1c 100644 --- a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs +++ b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs @@ -1,38 +1,35 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Telemetry.Models +namespace Umbraco.Cms.Core.Telemetry.Models; + +/// +/// Serializable class containing telemetry information. +/// +[DataContract] +public class TelemetryReportData { /// - /// Serializable class containing telemetry information. + /// Gets or sets a random GUID to prevent an instance posting multiple times pr. day. /// - [DataContract] - public class TelemetryReportData - { - /// - /// Gets or sets a random GUID to prevent an instance posting multiple times pr. day. - /// - [DataMember(Name = "id")] - public Guid Id { get; set; } + [DataMember(Name = "id")] + public Guid Id { get; set; } - /// - /// Gets or sets the Umbraco CMS version. - /// - [DataMember(Name = "version")] - public string? Version { get; set; } + /// + /// Gets or sets the Umbraco CMS version. + /// + [DataMember(Name = "version")] + public string? Version { get; set; } - /// - /// Gets or sets an enumerable containing information about packages. - /// - /// - /// Contains only the name and version of the packages, unless no version is specified. - /// - [DataMember(Name = "packages")] - public IEnumerable? Packages { get; set; } + /// + /// Gets or sets an enumerable containing information about packages. + /// + /// + /// Contains only the name and version of the packages, unless no version is specified. + /// + [DataMember(Name = "packages")] + public IEnumerable? Packages { get; set; } - [DataMember(Name = "detailed")] - public IEnumerable? Detailed { get; set; } - } + [DataMember(Name = "detailed")] + public IEnumerable? Detailed { get; set; } } diff --git a/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs index b6e40665c1..a7b5882ecc 100644 --- a/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs +++ b/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs @@ -1,81 +1,79 @@ -using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Telemetry +namespace Umbraco.Cms.Core.Telemetry; + +/// +internal class SiteIdentifierService : ISiteIdentifierService { - /// - internal class SiteIdentifierService : ISiteIdentifierService + private readonly IConfigManipulator _configManipulator; + private readonly ILogger _logger; + private GlobalSettings _globalSettings; + + public SiteIdentifierService( + IOptionsMonitor optionsMonitor, + IConfigManipulator configManipulator, + ILogger logger) { - private GlobalSettings _globalSettings; - private readonly IConfigManipulator _configManipulator; - private readonly ILogger _logger; + _globalSettings = optionsMonitor.CurrentValue; + optionsMonitor.OnChange(globalSettings => _globalSettings = globalSettings); + _configManipulator = configManipulator; + _logger = logger; + } - public SiteIdentifierService( - IOptionsMonitor optionsMonitor, - IConfigManipulator configManipulator, - ILogger logger) + /// + public bool TryGetSiteIdentifier(out Guid siteIdentifier) + { + // Parse telemetry string as a GUID & verify its a GUID and not some random string + // since users may have messed with or decided to empty the app setting or put in something random + if (Guid.TryParse(_globalSettings.Id, out Guid parsedTelemetryId) is false + || parsedTelemetryId == Guid.Empty) { - _globalSettings = optionsMonitor.CurrentValue; - optionsMonitor.OnChange(globalSettings => _globalSettings = globalSettings); - _configManipulator = configManipulator; - _logger = logger; - } - - /// - public bool TryGetSiteIdentifier(out Guid siteIdentifier) - { - // Parse telemetry string as a GUID & verify its a GUID and not some random string - // since users may have messed with or decided to empty the app setting or put in something random - if (Guid.TryParse(_globalSettings.Id, out var parsedTelemetryId) is false - || parsedTelemetryId == Guid.Empty) - { - siteIdentifier = Guid.Empty; - return false; - } - - siteIdentifier = parsedTelemetryId; - return true; - } - - /// - public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier) - { - if (TryGetSiteIdentifier(out Guid existingId)) - { - siteIdentifier = existingId; - return true; - } - - if (TryCreateSiteIdentifier(out Guid createdId)) - { - siteIdentifier = createdId; - return true; - } - siteIdentifier = Guid.Empty; return false; } - /// - public bool TryCreateSiteIdentifier(out Guid createdGuid) + siteIdentifier = parsedTelemetryId; + return true; + } + + /// + public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier) + { + if (TryGetSiteIdentifier(out Guid existingId)) { - createdGuid = Guid.NewGuid(); - - try - { - _configManipulator.SetGlobalId(createdGuid.ToString()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Couldn't update config files with a telemetry site identifier"); - createdGuid = Guid.Empty; - return false; - } - + siteIdentifier = existingId; return true; } + + if (TryCreateSiteIdentifier(out Guid createdId)) + { + siteIdentifier = createdId; + return true; + } + + siteIdentifier = Guid.Empty; + return false; + } + + /// + public bool TryCreateSiteIdentifier(out Guid createdGuid) + { + createdGuid = Guid.NewGuid(); + + try + { + _configManipulator.SetGlobalId(createdGuid.ToString()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Couldn't update config files with a telemetry site identifier"); + createdGuid = Guid.Empty; + return false; + } + + return true; } } diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs index bcc6076d24..4ebf1ba0b9 100644 --- a/src/Umbraco.Core/Telemetry/TelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; @@ -10,88 +8,87 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Telemetry +namespace Umbraco.Cms.Core.Telemetry; + +/// +internal class TelemetryService : ITelemetryService { - /// - internal class TelemetryService : ITelemetryService + private readonly IManifestParser _manifestParser; + private readonly IMetricsConsentService _metricsConsentService; + private readonly ISiteIdentifierService _siteIdentifierService; + private readonly IUmbracoVersion _umbracoVersion; + private readonly IUsageInformationService _usageInformationService; + + /// + /// Initializes a new instance of the class. + /// + public TelemetryService( + IManifestParser manifestParser, + IUmbracoVersion umbracoVersion, + ISiteIdentifierService siteIdentifierService, + IUsageInformationService usageInformationService, + IMetricsConsentService metricsConsentService) { - private readonly IManifestParser _manifestParser; - private readonly IUmbracoVersion _umbracoVersion; - private readonly ISiteIdentifierService _siteIdentifierService; - private readonly IUsageInformationService _usageInformationService; - private readonly IMetricsConsentService _metricsConsentService; + _manifestParser = manifestParser; + _umbracoVersion = umbracoVersion; + _siteIdentifierService = siteIdentifierService; + _usageInformationService = usageInformationService; + _metricsConsentService = metricsConsentService; + } - /// - /// Initializes a new instance of the class. - /// - public TelemetryService( - IManifestParser manifestParser, - IUmbracoVersion umbracoVersion, - ISiteIdentifierService siteIdentifierService, - IUsageInformationService usageInformationService, - IMetricsConsentService metricsConsentService) + /// + public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) + { + if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false) { - _manifestParser = manifestParser; - _umbracoVersion = umbracoVersion; - _siteIdentifierService = siteIdentifierService; - _usageInformationService = usageInformationService; - _metricsConsentService = metricsConsentService; + telemetryReportData = null; + return false; } - /// - public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) + telemetryReportData = new TelemetryReportData { - if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false) - { - telemetryReportData = null; - return false; - } + Id = telemetryId, + Version = GetVersion(), + Packages = GetPackageTelemetry(), + Detailed = _usageInformationService.GetDetailed(), + }; + return true; + } - telemetryReportData = new TelemetryReportData - { - Id = telemetryId, - Version = GetVersion(), - Packages = GetPackageTelemetry(), - Detailed = _usageInformationService.GetDetailed(), - }; - return true; + private string? GetVersion() + { + if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + { + return null; } - private string? GetVersion() - { - if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) - { - return null; - } + return _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); + } - return _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); + private IEnumerable? GetPackageTelemetry() + { + if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + { + return null; } - private IEnumerable? GetPackageTelemetry() + List packages = new(); + IEnumerable manifests = _manifestParser.GetManifests(); + + foreach (PackageManifest manifest in manifests) { - if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + if (manifest.AllowPackageTelemetry is false) { - return null; + continue; } - List packages = new(); - IEnumerable manifests = _manifestParser.GetManifests(); - - foreach (PackageManifest manifest in manifests) + packages.Add(new PackageTelemetry { - if (manifest.AllowPackageTelemetry is false) - { - continue; - } - - packages.Add(new PackageTelemetry - { - Name = manifest.PackageName, - Version = manifest.Version ?? string.Empty, - }); - } - - return packages; + Name = manifest.PackageName, + Version = manifest.Version ?? string.Empty, + }); } + + return packages; } } diff --git a/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs index 46ac9fb6e7..aa0e9a09bf 100644 --- a/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs @@ -1,95 +1,96 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Umbraco.Cms.Core.Routing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +public sealed class HtmlImageSourceParser { + private static readonly Regex ResolveImgPattern = new( + @"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - public sealed class HtmlImageSourceParser + private static readonly Regex DataUdiAttributeRegex = new( + @"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + private readonly IPublishedUrlProvider? _publishedUrlProvider; + + private Func? _getMediaUrl; + + public HtmlImageSourceParser(Func getMediaUrl) => _getMediaUrl = getMediaUrl; + + public HtmlImageSourceParser(IPublishedUrlProvider publishedUrlProvider) => + _publishedUrlProvider = publishedUrlProvider; + + /// + /// Parses out media UDIs from an html string based on 'data-udi' html attributes + /// + /// + /// + public IEnumerable FindUdisFromDataAttributes(string text) { - public HtmlImageSourceParser(Func getMediaUrl) + MatchCollection matches = DataUdiAttributeRegex.Matches(text); + if (matches.Count == 0) { - this._getMediaUrl = getMediaUrl; + yield break; } - private readonly IPublishedUrlProvider? _publishedUrlProvider; - - public HtmlImageSourceParser(IPublishedUrlProvider publishedUrlProvider) + foreach (Match match in matches) { - _publishedUrlProvider = publishedUrlProvider; - } - - private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - - private Func? _getMediaUrl; - - /// - /// Parses out media UDIs from an html string based on 'data-udi' html attributes - /// - /// - /// - public IEnumerable FindUdisFromDataAttributes(string text) - { - var matches = DataUdiAttributeRegex.Matches(text); - if (matches.Count == 0) - yield break; - - foreach (Match match in matches) + if (match.Groups.Count == 2 && UdiParser.TryParse(match.Groups[1].Value, out Udi? udi)) { - if (match.Groups.Count == 2 && UdiParser.TryParse(match.Groups[1].Value, out var udi)) - yield return udi; + yield return udi; } } + } - /// - /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. - /// - /// - /// - /// Umbraco image tags are identified by their data-udi attributes - public string EnsureImageSources(string text) + /// + /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. + /// + /// + /// + /// Umbraco image tags are identified by their data-udi attributes + public string EnsureImageSources(string text) + { + if (_getMediaUrl == null) { - if(_getMediaUrl == null) - _getMediaUrl = (guid) => _publishedUrlProvider?.GetMediaUrl(guid); - - return ResolveImgPattern.Replace(text, match => - { - // match groups: - // - 1 = from the beginning of the image tag until src attribute value begins - // - 2 = the src attribute value excluding the querystring (if present) - // - 3 = anything after group 2 and before the data-udi attribute value begins - // - 4 = the data-udi attribute value - // - 5 = anything after group 4 until the image tag is closed - var udi = match.Groups[4].Value; - if (udi.IsNullOrWhiteSpace() ||UdiParser.TryParse(udi, out var guidUdi) == false) - { - return match.Value; - } - var mediaUrl = _getMediaUrl(guidUdi.Guid); - if (mediaUrl == null) - { - // image does not exist - we could choose to remove the image entirely here (return empty string), - // but that would leave the editors completely in the dark as to why the image doesn't show - return match.Value; - } - - return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; - }); + _getMediaUrl = guid => _publishedUrlProvider?.GetMediaUrl(guid); } - /// - /// Removes media URLs from <img> tags where a data-udi attribute is present - /// - /// - /// - public string RemoveImageSources(string text) - // see comment in ResolveMediaFromTextString for group reference - => ResolveImgPattern.Replace(text, "$1$3$4$5"); + return ResolveImgPattern.Replace(text, match => + { + // match groups: + // - 1 = from the beginning of the image tag until src attribute value begins + // - 2 = the src attribute value excluding the querystring (if present) + // - 3 = anything after group 2 and before the data-udi attribute value begins + // - 4 = the data-udi attribute value + // - 5 = anything after group 4 until the image tag is closed + var udi = match.Groups[4].Value; + if (udi.IsNullOrWhiteSpace() || UdiParser.TryParse(udi, out GuidUdi? guidUdi) == false) + { + return match.Value; + } + + var mediaUrl = _getMediaUrl(guidUdi.Guid); + if (mediaUrl == null) + { + // image does not exist - we could choose to remove the image entirely here (return empty string), + // but that would leave the editors completely in the dark as to why the image doesn't show + return match.Value; + } + + return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; + }); } + + /// + /// Removes media URLs from <img> tags where a data-udi attribute is present + /// + /// + /// + public string RemoveImageSources(string text) + + // see comment in ResolveMediaFromTextString for group reference + => ResolveImgPattern.Replace(text, "$1$3$4$5"); } diff --git a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs index 4317f05cc9..1030705051 100644 --- a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs +++ b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs @@ -1,127 +1,135 @@ -using System; -using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +/// +/// Utility class used to parse internal links +/// +public sealed class HtmlLocalLinkParser { - /// - /// Utility class used to parse internal links - /// - public sealed class HtmlLocalLinkParser + internal static readonly Regex LocalLinkPattern = new( + @"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private readonly IPublishedUrlProvider _publishedUrlProvider; + + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public HtmlLocalLinkParser( + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedUrlProvider publishedUrlProvider) { + _umbracoContextAccessor = umbracoContextAccessor; + _publishedUrlProvider = publishedUrlProvider; + } - internal static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedUrlProvider _publishedUrlProvider; - - public HtmlLocalLinkParser(IUmbracoContextAccessor umbracoContextAccessor, IPublishedUrlProvider publishedUrlProvider) + public IEnumerable FindUdisFromLocalLinks(string text) + { + foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(text)) { - _umbracoContextAccessor = umbracoContextAccessor; - _publishedUrlProvider = publishedUrlProvider; - } - - public IEnumerable FindUdisFromLocalLinks(string text) - { - foreach ((int? intId, GuidUdi? udi, string tagValue) in FindLocalLinkIds(text)) + if (udi is not null) { - if (udi is not null) - yield return udi; // In v8, we only care abuot UDIs + yield return udi; // In v8, we only care abuot UDIs } } + } - /// - /// Parses the string looking for the {localLink} syntax and updates them to their correct links. - /// - /// - /// - /// - public string EnsureInternalLinks(string text, bool preview) + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string EnsureInternalLinks(string text, bool preview) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) - { - throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); - } - - if (!preview) - { - return EnsureInternalLinks(text); - } - - using (umbracoContext!.ForcedPreview(preview)) // force for URL provider - { - return EnsureInternalLinks(text); - } + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); } - /// - /// Parses the string looking for the {localLink} syntax and updates them to their correct links. - /// - /// - /// - /// - public string EnsureInternalLinks(string text) + if (!preview) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out _)) - { - throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); - } + return EnsureInternalLinks(text); + } - foreach((int? intId, GuidUdi? udi, string tagValue) in FindLocalLinkIds(text)) + using (umbracoContext.ForcedPreview(preview)) // force for URL provider + { + return EnsureInternalLinks(text); + } + } + + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string EnsureInternalLinks(string text) + { + if (!_umbracoContextAccessor.TryGetUmbracoContext(out _)) + { + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); + } + + foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(text)) + { + if (udi is not null) { - if (udi is not null) + var newLink = "#"; + if (udi?.EntityType == Constants.UdiEntityType.Document) { - var newLink = "#"; - if (udi?.EntityType == Constants.UdiEntityType.Document) - newLink = _publishedUrlProvider.GetUrl(udi.Guid); - else if (udi?.EntityType == Constants.UdiEntityType.Media) - newLink = _publishedUrlProvider.GetMediaUrl(udi.Guid); - - if (newLink == null) - newLink = "#"; - - text = text.Replace(tagValue, "href=\"" + newLink); + newLink = _publishedUrlProvider.GetUrl(udi.Guid); } - else if (intId.HasValue) + else if (udi?.EntityType == Constants.UdiEntityType.Media) { - var newLink = _publishedUrlProvider.GetUrl(intId.Value); - text = text.Replace(tagValue, "href=\"" + newLink); + newLink = _publishedUrlProvider.GetMediaUrl(udi.Guid); } - } - return text; + if (newLink == null) + { + newLink = "#"; + } + + text = text.Replace(tagValue, "href=\"" + newLink); + } + else if (intId.HasValue) + { + var newLink = _publishedUrlProvider.GetUrl(intId.Value); + text = text.Replace(tagValue, "href=\"" + newLink); + } } - private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLocalLinkIds(string text) + return text; + } + + private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLocalLinkIds(string text) + { + // Parse internal links + MatchCollection tags = LocalLinkPattern.Matches(text); + foreach (Match tag in tags) { - // Parse internal links - var tags = LocalLinkPattern.Matches(text); - foreach (Match tag in tags) + if (tag.Groups.Count > 0) { - if (tag.Groups.Count > 0) + var id = tag.Groups[1].Value; // .Remove(tag.Groups[1].Value.Length - 1, 1); + + // The id could be an int or a UDI + if (UdiParser.TryParse(id, out Udi? udi)) { - var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); - - //The id could be an int or a UDI - if (UdiParser.TryParse(id, out var udi)) + var guidUdi = udi as GuidUdi; + if (guidUdi is not null) { - var guidUdi = udi as GuidUdi; - if (guidUdi is not null) - yield return (null, guidUdi, tag.Value); - } - - if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) - { - yield return (intId, null, tag.Value); + yield return (null, guidUdi, tag.Value); } } - } + if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) + { + yield return (intId, null, tag.Value); + } + } } } } diff --git a/src/Umbraco.Core/Templates/HtmlUrlParser.cs b/src/Umbraco.Core/Templates/HtmlUrlParser.cs index 39c82f00ab..f4a817485d 100644 --- a/src/Umbraco.Core/Templates/HtmlUrlParser.cs +++ b/src/Umbraco.Core/Templates/HtmlUrlParser.cs @@ -5,66 +5,77 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +public sealed class HtmlUrlParser { - public sealed class HtmlUrlParser + private static readonly Regex ResolveUrlPattern = new( + "(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private readonly IIOHelper _ioHelper; + private readonly ILogger _logger; + private readonly IProfilingLogger _profilingLogger; + private ContentSettings _contentSettings; + + public HtmlUrlParser(IOptionsMonitor contentSettings, ILogger logger, IProfilingLogger profilingLogger, IIOHelper ioHelper) { - private ContentSettings _contentSettings; - private readonly ILogger _logger; - private readonly IIOHelper _ioHelper; - private readonly IProfilingLogger _profilingLogger; + _contentSettings = contentSettings.CurrentValue; + _logger = logger; + _ioHelper = ioHelper; + _profilingLogger = profilingLogger; - private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + contentSettings.OnChange(x => _contentSettings = x); + } - public HtmlUrlParser(IOptionsMonitor contentSettings, ILogger logger, IProfilingLogger profilingLogger, IIOHelper ioHelper) + /// + /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl + /// to replace the tilde with the application path. + /// + /// + /// + /// + /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. + /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for + /// non-Virtual-Directory installs. + /// + public string EnsureUrls(string text) + { + if (_contentSettings.ResolveUrlsFromTextString == false) { - _contentSettings = contentSettings.CurrentValue; - _logger = logger; - _ioHelper = ioHelper; - _profilingLogger = profilingLogger; - - contentSettings.OnChange(x => _contentSettings = x); - } - - /// - /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. - /// - /// - /// - /// - /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. - /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. - /// - public string EnsureUrls(string text) - { - if (_contentSettings.ResolveUrlsFromTextString == false) - return text; - - using (var timer = _profilingLogger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) - { - // find all relative URLs (ie. URLs that contain ~) - var tags = ResolveUrlPattern.Matches(text); - _logger.LogDebug("After regex: {Duration} matched: {TagsCount}", timer?.Stopwatch.ElapsedMilliseconds, tags.Count); - foreach (Match tag in tags) - { - var url = ""; - if (tag.Groups[1].Success) - url = tag.Groups[1].Value; - - // The richtext editor inserts a slash in front of the URL. That's why we need this little fix - // if (url.StartsWith("/")) - // text = text.Replace(url, ResolveUrl(url.Substring(1))); - // else - if (string.IsNullOrEmpty(url) == false) - { - var resolvedUrl = (url.Substring(0, 1) == "/") ? _ioHelper.ResolveUrl(url.Substring(1)) : _ioHelper.ResolveUrl(url); - text = text.Replace(url, resolvedUrl); - } - } - } - return text; } + + using (DisposableTimer? timer = _profilingLogger.DebugDuration( + typeof(IOHelper), + "ResolveUrlsFromTextString starting", + "ResolveUrlsFromTextString complete")) + { + // find all relative URLs (ie. URLs that contain ~) + MatchCollection tags = ResolveUrlPattern.Matches(text); + _logger.LogDebug("After regex: {Duration} matched: {TagsCount}", timer?.Stopwatch.ElapsedMilliseconds, tags.Count); + foreach (Match tag in tags) + { + var url = string.Empty; + if (tag.Groups[1].Success) + { + url = tag.Groups[1].Value; + } + + // The richtext editor inserts a slash in front of the URL. That's why we need this little fix + // if (url.StartsWith("/")) + // text = text.Replace(url, ResolveUrl(url.Substring(1))); + // else + if (string.IsNullOrEmpty(url) == false) + { + var resolvedUrl = url[..1] == "/" + ? _ioHelper.ResolveUrl(url[1..]) + : _ioHelper.ResolveUrl(url); + text = text.Replace(url, resolvedUrl); + } + } + } + + return text; } } diff --git a/src/Umbraco.Core/Templates/ITemplateRenderer.cs b/src/Umbraco.Core/Templates/ITemplateRenderer.cs index f6e6435a8a..17d16168ec 100644 --- a/src/Umbraco.Core/Templates/ITemplateRenderer.cs +++ b/src/Umbraco.Core/Templates/ITemplateRenderer.cs @@ -1,13 +1,9 @@ -using System.IO; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.Templates; -namespace Umbraco.Cms.Core.Templates +/// +/// This is used purely for the RenderTemplate functionality in Umbraco +/// +public interface ITemplateRenderer { - /// - /// This is used purely for the RenderTemplate functionality in Umbraco - /// - public interface ITemplateRenderer - { - Task RenderAsync(int pageId, int? altTemplateId, StringWriter writer); - } + Task RenderAsync(int pageId, int? altTemplateId, StringWriter writer); } diff --git a/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs b/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs index 1239f22877..b94d575b9f 100644 --- a/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs +++ b/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs @@ -1,57 +1,53 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +/// +/// Methods used to render umbraco components as HTML in templates +/// +public interface IUmbracoComponentRenderer { /// - /// Methods used to render umbraco components as HTML in templates + /// Renders the template for the specified pageId and an optional altTemplateId /// - public interface IUmbracoComponentRenderer - { - /// - /// Renders the template for the specified pageId and an optional altTemplateId - /// - /// The content id - /// If not specified, will use the template assigned to the node - Task RenderTemplateAsync(int contentId, int? altTemplateId = null); + /// The content id + /// If not specified, will use the template assigned to the node + Task RenderTemplateAsync(int contentId, int? altTemplateId = null); - /// - /// Renders the macro with the specified alias. - /// - /// The content id - /// The alias. - Task RenderMacroAsync(int contentId, string alias); + /// + /// Renders the macro with the specified alias. + /// + /// The content id + /// The alias. + Task RenderMacroAsync(int contentId, string alias); - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - /// The content id - /// The alias. - /// The parameters. - Task RenderMacroAsync(int contentId, string alias, object parameters); + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// The content id + /// The alias. + /// The parameters. + Task RenderMacroAsync(int contentId, string alias, object parameters); - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - /// The content id - /// The alias. - /// The parameters. - Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters); + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// The content id + /// The alias. + /// The parameters. + Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters); - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - /// An IPublishedContent to use for the context for the macro rendering - /// The alias. - /// The parameters. - /// A raw HTML string of the macro output - /// - /// Currently only used when the node is unpublished and unable to get the contentId item from the - /// content cache as its unpublished. This deals with taking in a preview/draft version of the content node - /// - Task RenderMacroForContent(IPublishedContent content, string alias, IDictionary? parameters); - - } + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// An IPublishedContent to use for the context for the macro rendering + /// The alias. + /// The parameters. + /// A raw HTML string of the macro output + /// + /// Currently only used when the node is unpublished and unable to get the contentId item from the + /// content cache as its unpublished. This deals with taking in a preview/draft version of the content node + /// + Task RenderMacroForContent(IPublishedContent content, string alias, IDictionary? parameters); } diff --git a/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs b/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs index 407f85ad60..e419bd5be3 100644 --- a/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs @@ -1,112 +1,108 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; -using System.Threading.Tasks; using Umbraco.Cms.Core.Macros; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Templates +namespace Umbraco.Cms.Core.Templates; + +/// +/// Methods used to render umbraco components as HTML in templates +/// +/// +/// Used by UmbracoHelper +/// +public class UmbracoComponentRenderer : IUmbracoComponentRenderer { + private readonly IMacroRenderer _macroRenderer; + private readonly ITemplateRenderer _templateRenderer; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; /// - /// Methods used to render umbraco components as HTML in templates + /// Initializes a new instance of the class. /// - /// - /// Used by UmbracoHelper - /// - public class UmbracoComponentRenderer : IUmbracoComponentRenderer + public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer) { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IMacroRenderer _macroRenderer; - private readonly ITemplateRenderer _templateRenderer; + _umbracoContextAccessor = umbracoContextAccessor; + _macroRenderer = macroRenderer; + _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); + } - /// - /// Initializes a new instance of the class. - /// - public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer) + /// + public async Task RenderTemplateAsync(int contentId, int? altTemplateId = null) + { + using (var sw = new StringWriter()) { - _umbracoContextAccessor = umbracoContextAccessor; - _macroRenderer = macroRenderer; - _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); - } - - /// - public async Task RenderTemplateAsync(int contentId, int? altTemplateId = null) - { - using (var sw = new StringWriter()) + try { - try - { - await _templateRenderer.RenderAsync(contentId, altTemplateId, sw); - } - catch (Exception ex) - { - sw.Write("", contentId, ex); - } - - return new HtmlEncodedString(sw.ToString()); + await _templateRenderer.RenderAsync(contentId, altTemplateId, sw); } - } - - /// - public async Task RenderMacroAsync(int contentId, string alias) => await RenderMacroAsync(contentId, alias, new { }); - - /// - public async Task RenderMacroAsync(int contentId, string alias, object parameters) => await RenderMacroAsync(contentId, alias, parameters.ToDictionary()); - - /// - public async Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters) - { - if (contentId == default) + catch (Exception ex) { - throw new ArgumentException("Invalid content id " + contentId); - } - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var content = umbracoContext.Content?.GetById(contentId); - - if (content == null) - { - throw new InvalidOperationException("Cannot render a macro, no content found by id " + contentId); + sw.Write("", contentId, ex); } - return await RenderMacroAsync(content, alias, parameters); - } - - /// - public async Task RenderMacroForContent(IPublishedContent content, string alias, IDictionary? parameters) - { - if(content == null) - { - throw new InvalidOperationException("Cannot render a macro, IPublishedContent is null"); - } - - return await RenderMacroAsync(content, alias, parameters); - } - - /// - /// Renders the macro with the specified alias, passing in the specified parameters. - /// - private async Task RenderMacroAsync(IPublishedContent content, string alias, IDictionary? parameters) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - // TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method looks for a lower case match. the whole macro concept needs to be rewritten. - // NOTE: the value could have HTML encoded values, so we need to deal with that - var macroProps = parameters?.ToDictionary( - x => x.Key.ToLowerInvariant(), - i => (i.Value is string) ? WebUtility.HtmlDecode(i.Value.ToString()) : i.Value); - - var html = (await _macroRenderer.RenderAsync(alias, content, macroProps)).Text; - - return new HtmlEncodedString(html!); + return new HtmlEncodedString(sw.ToString()); } } + + /// + public async Task RenderMacroAsync(int contentId, string alias) => + await RenderMacroAsync(contentId, alias, new { }); + + /// + public async Task RenderMacroAsync(int contentId, string alias, object parameters) => + await RenderMacroAsync(contentId, alias, parameters.ToDictionary()); + + /// + public async Task RenderMacroAsync(int contentId, string alias, IDictionary? parameters) + { + if (contentId == default) + { + throw new ArgumentException("Invalid content id " + contentId); + } + + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContent? content = umbracoContext.Content?.GetById(contentId); + + if (content == null) + { + throw new InvalidOperationException("Cannot render a macro, no content found by id " + contentId); + } + + return await RenderMacroAsync(content, alias, parameters); + } + + /// + public async Task RenderMacroForContent(IPublishedContent content, string alias, IDictionary? parameters) + { + if (content == null) + { + throw new InvalidOperationException("Cannot render a macro, IPublishedContent is null"); + } + + return await RenderMacroAsync(content, alias, parameters); + } + + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + private async Task RenderMacroAsync(IPublishedContent content, string alias, IDictionary? parameters) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + // TODO: We are doing at ToLower here because for some insane reason the UpdateMacroModel method looks for a lower case match. the whole macro concept needs to be rewritten. + // NOTE: the value could have HTML encoded values, so we need to deal with that + var macroProps = parameters?.ToDictionary( + x => x.Key.ToLowerInvariant(), + i => i.Value is string ? WebUtility.HtmlDecode(i.Value.ToString()) : i.Value); + + var html = (await _macroRenderer.RenderAsync(alias, content, macroProps)).Text; + + return new HtmlEncodedString(html!); + } } diff --git a/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs b/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs index 3fba765f83..d1d8384502 100644 --- a/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs +++ b/src/Umbraco.Core/Tour/BackOfficeTourFilter.cs @@ -1,63 +1,65 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; -namespace Umbraco.Cms.Core.Tour +namespace Umbraco.Cms.Core.Tour; + +/// +/// Represents a back-office tour filter. +/// +public class BackOfficeTourFilter { /// - /// Represents a back-office tour filter. + /// Initializes a new instance of the class. /// - public class BackOfficeTourFilter + /// Value to filter out tours by a plugin, can be null + /// Value to filter out a tour file, can be null + /// Value to filter out a tour alias, can be null + /// + /// Depending on what is null will depend on how the filter is applied. + /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check + /// tour alias is not NULL and then match it, + /// if any steps is NULL then the filters upstream are applied. + /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from + /// the plugin "hello" but not from other plugins if the same file name exists. + /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the + /// plugin or file name + /// + public BackOfficeTourFilter(Regex? pluginName, Regex? tourFileName, Regex? tourAlias) { - /// - /// Initializes a new instance of the class. - /// - /// Value to filter out tours by a plugin, can be null - /// Value to filter out a tour file, can be null - /// Value to filter out a tour alias, can be null - /// - /// Depending on what is null will depend on how the filter is applied. - /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check tour alias is not NULL and then match it, - /// if any steps is NULL then the filters upstream are applied. - /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from the plugin "hello" but not from other plugins if the same file name exists. - /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the plugin or file name - /// - public BackOfficeTourFilter(Regex? pluginName, Regex? tourFileName, Regex? tourAlias) - { - PluginName = pluginName; - TourFileName = tourFileName; - TourAlias = tourAlias; - } - - /// - /// Gets the plugin name filtering regex. - /// - public Regex? PluginName { get; } - - /// - /// Gets the tour filename filtering regex. - /// - public Regex? TourFileName { get; } - - /// - /// Gets the tour alias filtering regex. - /// - public Regex? TourAlias { get; } - - /// - /// Creates a filter to filter on the plugin name. - /// - public static BackOfficeTourFilter FilterPlugin(Regex pluginName) - => new BackOfficeTourFilter(pluginName, null, null); - - /// - /// Creates a filter to filter on the tour filename. - /// - public static BackOfficeTourFilter FilterFile(Regex tourFileName) - => new BackOfficeTourFilter(null, tourFileName, null); - - /// - /// Creates a filter to filter on the tour alias. - /// - public static BackOfficeTourFilter FilterAlias(Regex tourAlias) - => new BackOfficeTourFilter(null, null, tourAlias); + PluginName = pluginName; + TourFileName = tourFileName; + TourAlias = tourAlias; } + + /// + /// Gets the plugin name filtering regex. + /// + public Regex? PluginName { get; } + + /// + /// Gets the tour filename filtering regex. + /// + public Regex? TourFileName { get; } + + /// + /// Gets the tour alias filtering regex. + /// + public Regex? TourAlias { get; } + + /// + /// Creates a filter to filter on the plugin name. + /// + public static BackOfficeTourFilter FilterPlugin(Regex pluginName) + => new(pluginName, null, null); + + /// + /// Creates a filter to filter on the tour filename. + /// + public static BackOfficeTourFilter FilterFile(Regex tourFileName) + => new(null, tourFileName, null); + + /// + /// Creates a filter to filter on the tour alias. + /// + public static BackOfficeTourFilter FilterAlias(Regex tourAlias) + => new(null, null, tourAlias); } diff --git a/src/Umbraco.Core/Tour/TourFilterCollection.cs b/src/Umbraco.Core/Tour/TourFilterCollection.cs index 2864abbced..44905f9127 100644 --- a/src/Umbraco.Core/Tour/TourFilterCollection.cs +++ b/src/Umbraco.Core/Tour/TourFilterCollection.cs @@ -1,16 +1,14 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Tour +namespace Umbraco.Cms.Core.Tour; + +/// +/// Represents a collection of items. +/// +public class TourFilterCollection : BuilderCollectionBase { - /// - /// Represents a collection of items. - /// - public class TourFilterCollection : BuilderCollectionBase + public TourFilterCollection(Func> items) + : base(items) { - public TourFilterCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs b/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs index 61f10cc96d..b39bcede46 100644 --- a/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs +++ b/src/Umbraco.Core/Tour/TourFilterCollectionBuilder.cs @@ -1,73 +1,57 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Tour +namespace Umbraco.Cms.Core.Tour; + +/// +/// Builds a collection of items. +/// +public class TourFilterCollectionBuilder : CollectionBuilderBase { + private readonly HashSet _instances = new(); + /// - /// Builds a collection of items. + /// Adds a filter instance. /// - public class TourFilterCollectionBuilder : CollectionBuilderBase + public void AddFilter(BackOfficeTourFilter filter) => _instances.Add(filter); + + /// + protected override IEnumerable CreateItems(IServiceProvider factory) => + base.CreateItems(factory).Concat(_instances); + + /// + /// Removes a filter instance. + /// + public void RemoveFilter(BackOfficeTourFilter filter) => _instances.Remove(filter); + + /// + /// Removes all filter instances. + /// + public void RemoveAllFilters() => _instances.Clear(); + + /// + /// Removes filters matching a condition. + /// + public void RemoveFilter(Func predicate) => + _instances.RemoveWhere(new Predicate(predicate)); + + /// + /// Creates and adds a filter instance filtering by plugin name. + /// + public void AddFilterByPlugin(string pluginName) { - private readonly HashSet _instances = new HashSet(); + pluginName = pluginName.EnsureStartsWith("^").EnsureEndsWith("$"); + _instances.Add(BackOfficeTourFilter.FilterPlugin(new Regex(pluginName, RegexOptions.IgnoreCase))); + } - /// - protected override IEnumerable CreateItems(IServiceProvider factory) - { - return base.CreateItems(factory).Concat(_instances); - } - - /// - /// Adds a filter instance. - /// - public void AddFilter(BackOfficeTourFilter filter) - { - _instances.Add(filter); - } - - /// - /// Removes a filter instance. - /// - public void RemoveFilter(BackOfficeTourFilter filter) - { - _instances.Remove(filter); - } - - /// - /// Removes all filter instances. - /// - public void RemoveAllFilters() - { - _instances.Clear(); - } - - /// - /// Removes filters matching a condition. - /// - public void RemoveFilter(Func predicate) - { - _instances.RemoveWhere(new Predicate(predicate)); - } - - /// - /// Creates and adds a filter instance filtering by plugin name. - /// - public void AddFilterByPlugin(string pluginName) - { - pluginName = pluginName.EnsureStartsWith("^").EnsureEndsWith("$"); - _instances.Add(BackOfficeTourFilter.FilterPlugin(new Regex(pluginName, RegexOptions.IgnoreCase))); - } - - /// - /// Creates and adds a filter instance filtering by tour filename. - /// - public void AddFilterByFile(string filename) - { - filename = filename.EnsureStartsWith("^").EnsureEndsWith("$"); - _instances.Add(BackOfficeTourFilter.FilterFile(new Regex(filename, RegexOptions.IgnoreCase))); - } + /// + /// Creates and adds a filter instance filtering by tour filename. + /// + public void AddFilterByFile(string filename) + { + filename = filename.EnsureStartsWith("^").EnsureEndsWith("$"); + _instances.Add(BackOfficeTourFilter.FilterFile(new Regex(filename, RegexOptions.IgnoreCase))); } } diff --git a/src/Umbraco.Core/Trees/ActionUrlMethod.cs b/src/Umbraco.Core/Trees/ActionUrlMethod.cs index fcf455c6ad..c2be2cea54 100644 --- a/src/Umbraco.Core/Trees/ActionUrlMethod.cs +++ b/src/Umbraco.Core/Trees/ActionUrlMethod.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +/// +/// Specifies the action to take for a menu item when a URL is specified +/// +public enum ActionUrlMethod { - /// - /// Specifies the action to take for a menu item when a URL is specified - /// - public enum ActionUrlMethod - { - Dialog, - BlankWindow - } + Dialog, + BlankWindow, } diff --git a/src/Umbraco.Core/Trees/CoreTreeAttribute.cs b/src/Umbraco.Core/Trees/CoreTreeAttribute.cs index eedad5b600..b1c29ccb63 100644 --- a/src/Umbraco.Core/Trees/CoreTreeAttribute.cs +++ b/src/Umbraco.Core/Trees/CoreTreeAttribute.cs @@ -1,14 +1,12 @@ -using System; +namespace Umbraco.Cms.Core.Trees; -namespace Umbraco.Cms.Core.Trees +/// +/// Indicates that a tree is a core tree and should not be treated as a plugin tree. +/// +/// +/// This ensures that umbraco will look in the umbraco folders for views for this tree. +/// +[AttributeUsage(AttributeTargets.Class)] +public class CoreTreeAttribute : Attribute { - /// - /// Indicates that a tree is a core tree and should not be treated as a plugin tree. - /// - /// - /// This ensures that umbraco will look in the umbraco folders for views for this tree. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class CoreTreeAttribute : Attribute - { } } diff --git a/src/Umbraco.Core/Trees/IMenuItemCollectionFactory.cs b/src/Umbraco.Core/Trees/IMenuItemCollectionFactory.cs index fca82ca18b..bba1bfc2dc 100644 --- a/src/Umbraco.Core/Trees/IMenuItemCollectionFactory.cs +++ b/src/Umbraco.Core/Trees/IMenuItemCollectionFactory.cs @@ -1,15 +1,13 @@ -namespace Umbraco.Cms.Core.Trees -{ +namespace Umbraco.Cms.Core.Trees; +/// +/// Represents a factory to create . +/// +public interface IMenuItemCollectionFactory +{ /// - /// Represents a factory to create . + /// Creates an empty . /// - public interface IMenuItemCollectionFactory - { - /// - /// Creates an empty . - /// - /// An empty . - MenuItemCollection Create(); - } + /// An empty . + MenuItemCollection Create(); } diff --git a/src/Umbraco.Core/Trees/ISearchableTree.cs b/src/Umbraco.Core/Trees/ISearchableTree.cs index dd61ba0cdb..42883d0f87 100644 --- a/src/Umbraco.Core/Trees/ISearchableTree.cs +++ b/src/Umbraco.Core/Trees/ISearchableTree.cs @@ -1,28 +1,25 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Core.Trees -{ - public interface ISearchableTree : IDiscoverable - { - /// - /// The alias of the tree that the belongs to - /// - string TreeAlias { get; } +namespace Umbraco.Cms.Core.Trees; - /// - /// Searches for results based on the entity type - /// - /// - /// - /// - /// - /// - /// A starting point for the search, generally a node id, but for members this is a member type alias - /// - /// - Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null); - } +public interface ISearchableTree : IDiscoverable +{ + /// + /// The alias of the tree that the belongs to + /// + string TreeAlias { get; } + + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// + Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null); } diff --git a/src/Umbraco.Core/Trees/ITree.cs b/src/Umbraco.Core/Trees/ITree.cs index 106b3eef37..efb3cfab97 100644 --- a/src/Umbraco.Core/Trees/ITree.cs +++ b/src/Umbraco.Core/Trees/ITree.cs @@ -1,44 +1,43 @@ -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +// TODO: we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used +// leave as internal for now, maybe we'll use in the future, means we could pass around ITree +// TODO: We should make this a thing, a tree should just be an interface *not* a controller +public interface ITree { - // TODO: we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used - // leave as internal for now, maybe we'll use in the future, means we could pass around ITree - // TODO: We should make this a thing, a tree should just be an interface *not* a controller - public interface ITree - { - /// - /// Gets or sets the sort order. - /// - /// The sort order. - int SortOrder { get; } + /// + /// Gets or sets the sort order. + /// + /// The sort order. + int SortOrder { get; } - /// - /// Gets the section alias. - /// - string SectionAlias { get; } + /// + /// Gets the section alias. + /// + string SectionAlias { get; } - /// - /// Gets the tree group. - /// - string? TreeGroup { get; } + /// + /// Gets the tree group. + /// + string? TreeGroup { get; } - /// - /// Gets the tree alias. - /// - string TreeAlias { get; } + /// + /// Gets the tree alias. + /// + string TreeAlias { get; } - /// - /// Gets or sets the tree title (fallback if the tree alias isn't localized) - /// - string? TreeTitle { get; } + /// + /// Gets or sets the tree title (fallback if the tree alias isn't localized) + /// + string? TreeTitle { get; } - /// - /// Gets the tree use. - /// - TreeUse TreeUse { get; } + /// + /// Gets the tree use. + /// + TreeUse TreeUse { get; } - /// - /// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app) - /// - bool IsSingleNodeTree { get; } - } + /// + /// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app) + /// + bool IsSingleNodeTree { get; } } diff --git a/src/Umbraco.Core/Trees/MenuItemCollection.cs b/src/Umbraco.Core/Trees/MenuItemCollection.cs index 66bdba55d4..aaace2cbd3 100644 --- a/src/Umbraco.Core/Trees/MenuItemCollection.cs +++ b/src/Umbraco.Core/Trees/MenuItemCollection.cs @@ -1,44 +1,33 @@ -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models.Trees; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +/// +/// A menu item collection for a given tree node +/// +[DataContract(Name = "menuItems", Namespace = "")] +public class MenuItemCollection { + public MenuItemCollection(ActionCollection actionCollection) => Items = new MenuItemList(actionCollection); + + public MenuItemCollection(ActionCollection actionCollection, IEnumerable items) => + Items = new MenuItemList(actionCollection, items); + /// - /// A menu item collection for a given tree node + /// Sets the default menu item alias to be shown when the menu is launched - this is optional and if not set then the + /// menu will just be shown normally. /// - [DataContract(Name = "menuItems", Namespace = "")] - public class MenuItemCollection - { - private readonly MenuItemList _menuItems; + [DataMember(Name = "defaultAlias")] + public string? DefaultMenuAlias { get; set; } - public MenuItemCollection(ActionCollection actionCollection) - { - _menuItems = new MenuItemList(actionCollection); - } - - public MenuItemCollection(ActionCollection actionCollection, IEnumerable items) - { - _menuItems = new MenuItemList(actionCollection, items); - } - - /// - /// Sets the default menu item alias to be shown when the menu is launched - this is optional and if not set then the menu will just be shown normally. - /// - [DataMember(Name = "defaultAlias")] - public string? DefaultMenuAlias { get; set; } - - /// - /// The list of menu items - /// - /// - /// We require this so the json serialization works correctly - /// - [DataMember(Name = "menuItems")] - public MenuItemList Items - { - get { return _menuItems; } - } - } + /// + /// The list of menu items + /// + /// + /// We require this so the json serialization works correctly + /// + [DataMember(Name = "menuItems")] + public MenuItemList Items { get; } } diff --git a/src/Umbraco.Core/Trees/MenuItemCollectionFactory.cs b/src/Umbraco.Core/Trees/MenuItemCollectionFactory.cs index 112b8b6240..da24b0d933 100644 --- a/src/Umbraco.Core/Trees/MenuItemCollectionFactory.cs +++ b/src/Umbraco.Core/Trees/MenuItemCollectionFactory.cs @@ -1,20 +1,12 @@ using Umbraco.Cms.Core.Actions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +public class MenuItemCollectionFactory : IMenuItemCollectionFactory { - public class MenuItemCollectionFactory: IMenuItemCollectionFactory - { - private readonly ActionCollection _actionCollection; + private readonly ActionCollection _actionCollection; - public MenuItemCollectionFactory(ActionCollection actionCollection) - { - _actionCollection = actionCollection; - } + public MenuItemCollectionFactory(ActionCollection actionCollection) => _actionCollection = actionCollection; - public MenuItemCollection Create() - { - return new MenuItemCollection(_actionCollection); - } - - } + public MenuItemCollection Create() => new MenuItemCollection(_actionCollection); } diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index b3fe420602..a4cb1899e3 100644 --- a/src/Umbraco.Core/Trees/MenuItemList.cs +++ b/src/Umbraco.Core/Trees/MenuItemList.cs @@ -1,72 +1,68 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Threading; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models.Trees; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +/// +/// A custom menu list +/// +/// +/// NOTE: We need a sub collection to the MenuItemCollection object due to how json serialization works. +/// +public class MenuItemList : List { + private readonly ActionCollection _actionCollection; + + public MenuItemList(ActionCollection actionCollection) => _actionCollection = actionCollection; + + public MenuItemList(ActionCollection actionCollection, IEnumerable items) + : base(items) => + _actionCollection = actionCollection; + /// - /// A custom menu list + /// Adds a menu item with a dictionary which is merged to the AdditionalData bag /// - /// - /// NOTE: We need a sub collection to the MenuItemCollection object due to how json serialization works. - /// - public class MenuItemList : List + /// + /// + /// The used to localize the action name based on its alias + /// Whether or not this action opens a dialog + public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) + where T : IAction { - private readonly ActionCollection _actionCollection; - - public MenuItemList(ActionCollection actionCollection) + MenuItem? item = CreateMenuItem(textService, hasSeparator, opensDialog); + if (item != null) { - _actionCollection = actionCollection; + Add(item); + return item; } - public MenuItemList(ActionCollection actionCollection, IEnumerable items) - : base(items) - { - _actionCollection = actionCollection; - } + return null; + } - /// - /// Adds a menu item with a dictionary which is merged to the AdditionalData bag - /// - /// - /// - /// The used to localize the action name based on its alias - /// Whether or not this action opens a dialog - public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) - where T : IAction + private MenuItem? CreateMenuItem(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) + where T : IAction + { + T? item = _actionCollection.GetAction(); + if (item == null) { - var item = CreateMenuItem(textService, hasSeparator, opensDialog); - if (item != null) - { - Add(item); - return item; - } return null; } - private MenuItem? CreateMenuItem(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) - where T : IAction + IDictionary values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); + values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); + + var menuItem = new MenuItem(item, textService.Localize("actions", item.Alias)) { - var item = _actionCollection.GetAction(); - if (item == null) return null; + SeparatorBefore = hasSeparator, + OpensDialog = opensDialog, + TextDescription = textDescription, + }; - var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); - values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); - - var menuItem = new MenuItem(item, textService.Localize($"actions", item.Alias)) - { - SeparatorBefore = hasSeparator, - OpensDialog = opensDialog, - TextDescription = textDescription, - }; - - return menuItem; - } + return menuItem; } } diff --git a/src/Umbraco.Core/Trees/SearchableApplicationTree.cs b/src/Umbraco.Core/Trees/SearchableApplicationTree.cs index 33104cb8c7..44b0a896ac 100644 --- a/src/Umbraco.Core/Trees/SearchableApplicationTree.cs +++ b/src/Umbraco.Core/Trees/SearchableApplicationTree.cs @@ -1,22 +1,26 @@ -namespace Umbraco.Cms.Core.Trees -{ - public class SearchableApplicationTree - { - public SearchableApplicationTree(string appAlias, string treeAlias, int sortOrder, string formatterService, string formatterMethod, ISearchableTree searchableTree) - { - AppAlias = appAlias; - TreeAlias = treeAlias; - SortOrder = sortOrder; - FormatterService = formatterService; - FormatterMethod = formatterMethod; - SearchableTree = searchableTree; - } +namespace Umbraco.Cms.Core.Trees; - public string AppAlias { get; } - public string TreeAlias { get; } - public int SortOrder { get; } - public string FormatterService { get; } - public string FormatterMethod { get; } - public ISearchableTree SearchableTree { get; } +public class SearchableApplicationTree +{ + public SearchableApplicationTree(string appAlias, string treeAlias, int sortOrder, string formatterService, string formatterMethod, ISearchableTree searchableTree) + { + AppAlias = appAlias; + TreeAlias = treeAlias; + SortOrder = sortOrder; + FormatterService = formatterService; + FormatterMethod = formatterMethod; + SearchableTree = searchableTree; } + + public string AppAlias { get; } + + public string TreeAlias { get; } + + public int SortOrder { get; } + + public string FormatterService { get; } + + public string FormatterMethod { get; } + + public ISearchableTree SearchableTree { get; } } diff --git a/src/Umbraco.Core/Trees/SearchableTreeAttribute.cs b/src/Umbraco.Core/Trees/SearchableTreeAttribute.cs index ca5cfef02a..f3a92fe82f 100644 --- a/src/Umbraco.Core/Trees/SearchableTreeAttribute.cs +++ b/src/Umbraco.Core/Trees/SearchableTreeAttribute.cs @@ -1,53 +1,64 @@ -using System; +namespace Umbraco.Cms.Core.Trees; -namespace Umbraco.Cms.Core.Trees +[AttributeUsage(AttributeTargets.Class)] +public sealed class SearchableTreeAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class)] - public sealed class SearchableTreeAttribute : Attribute + public const int DefaultSortOrder = 1000; + + /// + /// This constructor will assume that the method name equals `format(searchResult, appAlias, treeAlias)`. + /// + /// Name of the service. + public SearchableTreeAttribute(string serviceName) + : this(serviceName, string.Empty) { - public const int DefaultSortOrder = 1000; - - public string ServiceName { get; } - - public string MethodName { get; } - - public int SortOrder { get; } - - /// - /// This constructor will assume that the method name equals `format(searchResult, appAlias, treeAlias)`. - /// - /// Name of the service. - public SearchableTreeAttribute(string serviceName) - : this(serviceName, string.Empty) - { } - - /// - /// This constructor defines both the Angular service and method name to use. - /// - /// Name of the service. - /// Name of the method. - public SearchableTreeAttribute(string serviceName, string methodName) - : this(serviceName, methodName, DefaultSortOrder) - { } - - /// - /// This constructor defines both the Angular service and method name to use and explicitly defines a sort order for the results - /// - /// Name of the service. - /// Name of the method. - /// The sort order. - /// serviceName - /// or - /// methodName - /// Value can't be empty or consist only of white-space characters. - serviceName - public SearchableTreeAttribute(string serviceName, string methodName, int sortOrder) - { - if (serviceName == null) throw new ArgumentNullException(nameof(serviceName)); - if (string.IsNullOrWhiteSpace(serviceName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(serviceName)); - - ServiceName = serviceName; - MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); - SortOrder = sortOrder; - } } + + /// + /// This constructor defines both the Angular service and method name to use. + /// + /// Name of the service. + /// Name of the method. + public SearchableTreeAttribute(string serviceName, string methodName) + : this(serviceName, methodName, DefaultSortOrder) + { + } + + /// + /// This constructor defines both the Angular service and method name to use and explicitly defines a sort order for + /// the results + /// + /// Name of the service. + /// Name of the method. + /// The sort order. + /// + /// serviceName + /// or + /// methodName + /// + /// Value can't be empty or consist only of white-space characters. - serviceName + public SearchableTreeAttribute(string serviceName, string methodName, int sortOrder) + { + if (serviceName == null) + { + throw new ArgumentNullException(nameof(serviceName)); + } + + if (string.IsNullOrWhiteSpace(serviceName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(serviceName)); + } + + ServiceName = serviceName; + MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); + SortOrder = sortOrder; + } + + public string ServiceName { get; } + + public string MethodName { get; } + + public int SortOrder { get; } } diff --git a/src/Umbraco.Core/Trees/SearchableTreeCollection.cs b/src/Umbraco.Core/Trees/SearchableTreeCollection.cs index ff42b5e8c3..fdf2c8124b 100644 --- a/src/Umbraco.Core/Trees/SearchableTreeCollection.cs +++ b/src/Umbraco.Core/Trees/SearchableTreeCollection.cs @@ -1,50 +1,45 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +public class SearchableTreeCollection : BuilderCollectionBase { - public class SearchableTreeCollection : BuilderCollectionBase + private readonly Dictionary _dictionary; + + public SearchableTreeCollection(Func> items, ITreeService treeService) + : base(items) => + _dictionary = CreateDictionary(treeService); + + public IReadOnlyDictionary SearchableApplicationTrees => _dictionary; + + public SearchableApplicationTree this[string key] => _dictionary[key]; + + private Dictionary CreateDictionary(ITreeService treeService) { - private readonly Dictionary _dictionary; - - public SearchableTreeCollection(Func> items, ITreeService treeService) - : base(items) + Tree[] appTrees = treeService.GetAll() + .OrderBy(x => x.SortOrder) + .ToArray(); + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + ISearchableTree[] searchableTrees = this.ToArray(); + foreach (Tree appTree in appTrees) { - _dictionary = CreateDictionary(treeService); - } - - private Dictionary CreateDictionary(ITreeService treeService) - { - var appTrees = treeService.GetAll() - .OrderBy(x => x.SortOrder) - .ToArray(); - var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - var searchableTrees = this.ToArray(); - foreach (var appTree in appTrees) + ISearchableTree? found = searchableTrees.FirstOrDefault(x => x.TreeAlias.InvariantEquals(appTree.TreeAlias)); + if (found != null) { - var found = searchableTrees.FirstOrDefault(x => x.TreeAlias.InvariantEquals(appTree.TreeAlias)); - if (found != null) - { - var searchableTreeAttribute = found.GetType().GetCustomAttribute(false); - dictionary[found.TreeAlias] = new SearchableApplicationTree( - appTree.SectionAlias, - appTree.TreeAlias, - searchableTreeAttribute?.SortOrder ?? SearchableTreeAttribute.DefaultSortOrder, - searchableTreeAttribute?.ServiceName ?? string.Empty, - searchableTreeAttribute?.MethodName ?? string.Empty, - found - ); - } + SearchableTreeAttribute? searchableTreeAttribute = + found.GetType().GetCustomAttribute(false); + dictionary[found.TreeAlias] = new SearchableApplicationTree( + appTree.SectionAlias, + appTree.TreeAlias, + searchableTreeAttribute?.SortOrder ?? SearchableTreeAttribute.DefaultSortOrder, + searchableTreeAttribute?.ServiceName ?? string.Empty, + searchableTreeAttribute?.MethodName ?? string.Empty, + found); } - return dictionary; } - public IReadOnlyDictionary SearchableApplicationTrees => _dictionary; - - public SearchableApplicationTree this[string key] => _dictionary[key]; + return dictionary; } } diff --git a/src/Umbraco.Core/Trees/SearchableTreeCollectionBuilder.cs b/src/Umbraco.Core/Trees/SearchableTreeCollectionBuilder.cs index dca2839558..372866ba68 100644 --- a/src/Umbraco.Core/Trees/SearchableTreeCollectionBuilder.cs +++ b/src/Umbraco.Core/Trees/SearchableTreeCollectionBuilder.cs @@ -1,13 +1,13 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Trees -{ - public class SearchableTreeCollectionBuilder : LazyCollectionBuilderBase - { - protected override SearchableTreeCollectionBuilder This => this; +namespace Umbraco.Cms.Core.Trees; - //per request because generally an instance of ISearchableTree is a controller - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; - } +public class SearchableTreeCollectionBuilder : LazyCollectionBuilderBase +{ + protected override SearchableTreeCollectionBuilder This => this; + + // per request because generally an instance of ISearchableTree is a controller + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; } diff --git a/src/Umbraco.Core/Trees/Tree.cs b/src/Umbraco.Core/Trees/Tree.cs index f229dd8019..47ee0b234b 100644 --- a/src/Umbraco.Core/Trees/Tree.cs +++ b/src/Umbraco.Core/Trees/Tree.cs @@ -1,74 +1,74 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Diagnostics; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +[DebuggerDisplay("Tree - {SectionAlias}/{TreeAlias}")] +public class Tree : ITree { - [DebuggerDisplay("Tree - {SectionAlias}/{TreeAlias}")] - public class Tree : ITree + public Tree(int sortOrder, string applicationAlias, string? group, string alias, string? title, TreeUse use, Type treeControllerType, bool isSingleNodeTree) { - public Tree(int sortOrder, string applicationAlias, string? group, string alias, string? title, TreeUse use, Type treeControllerType, bool isSingleNodeTree) + SortOrder = sortOrder; + SectionAlias = applicationAlias ?? throw new ArgumentNullException(nameof(applicationAlias)); + TreeGroup = group; + TreeAlias = alias ?? throw new ArgumentNullException(nameof(alias)); + TreeTitle = title; + TreeUse = use; + TreeControllerType = treeControllerType ?? throw new ArgumentNullException(nameof(treeControllerType)); + IsSingleNodeTree = isSingleNodeTree; + } + + /// + /// Gets the tree controller type. + /// + public Type TreeControllerType { get; } + + /// + public int SortOrder { get; set; } + + /// + public string SectionAlias { get; set; } + + /// + public string? TreeGroup { get; } + + /// + public string TreeAlias { get; } + + /// + public string? TreeTitle { get; set; } + + /// + public TreeUse TreeUse { get; set; } + + /// + public bool IsSingleNodeTree { get; } + + public static string? GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) + { + var label = $"[{tree.TreeAlias}]"; + + // try to look up a the localized tree header matching the tree alias + var localizedLabel = textService.Localize("treeHeader", tree.TreeAlias); + + // if the localizedLabel returns [alias] then return the title if it's defined + if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) { - SortOrder = sortOrder; - SectionAlias = applicationAlias ?? throw new ArgumentNullException(nameof(applicationAlias)); - TreeGroup = group; - TreeAlias = alias ?? throw new ArgumentNullException(nameof(alias)); - TreeTitle = title; - TreeUse = use; - TreeControllerType = treeControllerType ?? throw new ArgumentNullException(nameof(treeControllerType)); - IsSingleNodeTree = isSingleNodeTree; + if (string.IsNullOrEmpty(tree.TreeTitle) == false) + { + label = tree.TreeTitle; + } + } + else + { + // the localizedLabel translated into something that's not just [alias], so use the translation + label = localizedLabel; } - /// - public int SortOrder { get; set; } - - /// - public string SectionAlias { get; set; } - - /// - public string? TreeGroup { get; } - - /// - public string TreeAlias { get; } - - /// - public string? TreeTitle { get; set; } - - /// - public TreeUse TreeUse { get; set; } - - /// - public bool IsSingleNodeTree { get; } - - /// - /// Gets the tree controller type. - /// - public Type TreeControllerType { get; } - - public static string? GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) - { - var label = $"[{tree.TreeAlias}]"; - - // try to look up a the localized tree header matching the tree alias - var localizedLabel = textService.Localize("treeHeader", tree.TreeAlias); - - // if the localizedLabel returns [alias] then return the title if it's defined - if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) - { - if (string.IsNullOrEmpty(tree.TreeTitle) == false) - label = tree.TreeTitle; - } - else - { - // the localizedLabel translated into something that's not just [alias], so use the translation - label = localizedLabel; - } - - return label; - } + return label; } } diff --git a/src/Umbraco.Core/Trees/TreeCollection.cs b/src/Umbraco.Core/Trees/TreeCollection.cs index 59fa99819c..fa6283753a 100644 --- a/src/Umbraco.Core/Trees/TreeCollection.cs +++ b/src/Umbraco.Core/Trees/TreeCollection.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Trees -{ - /// - /// Represents the collection of section trees. - /// - public class TreeCollection : BuilderCollectionBase - { +namespace Umbraco.Cms.Core.Trees; - public TreeCollection(Func> items) : base(items) - { - } +/// +/// Represents the collection of section trees. +/// +public class TreeCollection : BuilderCollectionBase +{ + public TreeCollection(Func> items) + : base(items) + { } } diff --git a/src/Umbraco.Core/Trees/TreeNode.cs b/src/Umbraco.Core/Trees/TreeNode.cs index 3c166c9fdd..dde66bd3a3 100644 --- a/src/Umbraco.Core/Trees/TreeNode.cs +++ b/src/Umbraco.Core/Trees/TreeNode.cs @@ -1,128 +1,130 @@ -using System; -using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +/// +/// Represents a model in the tree +/// +/// +/// TreeNode is sealed to prevent developers from adding additional json data to the response +/// +[DataContract(Name = "node", Namespace = "")] +public class TreeNode : EntityBasic { /// - /// Represents a model in the tree + /// Internal constructor, to create a tree node use the CreateTreeNode methods of the TreeApiController. + /// + /// + /// The parent id for the current node + /// + /// + public TreeNode(string nodeId, string? parentId, string? getChildNodesUrl, string? menuUrl) + { + if (nodeId == null) + { + throw new ArgumentNullException(nameof(nodeId)); + } + + if (string.IsNullOrWhiteSpace(nodeId)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(nodeId)); + } + + Id = nodeId; + ParentId = parentId; + ChildNodesUrl = getChildNodesUrl; + MenuUrl = menuUrl; + CssClasses = new List(); + + // default + Icon = "icon-folder-close"; + Path = "-1"; + } + + [DataMember(Name = "parentId", IsRequired = true)] + public new object? ParentId { get; set; } + + /// + /// A flag to set whether or not this node has children + /// + [DataMember(Name = "hasChildren")] + public bool HasChildren { get; set; } + + /// + /// The tree nodetype which refers to the type of node rendered in the tree + /// + [DataMember(Name = "nodeType")] + public string? NodeType { get; set; } + + /// + /// Optional: The Route path for the editor for this node /// /// - /// TreeNode is sealed to prevent developers from adding additional json data to the response + /// If this is not set, then the route path will be automatically determined by: {section}/edit/{id} /// - [DataContract(Name = "node", Namespace = "")] - public class TreeNode : EntityBasic + [DataMember(Name = "routePath")] + public string? RoutePath { get; set; } + + /// + /// The JSON URL to load the nodes children + /// + [DataMember(Name = "childNodesUrl")] + public string? ChildNodesUrl { get; set; } + + /// + /// The JSON URL to load the menu from + /// + [DataMember(Name = "menuUrl")] + public string? MenuUrl { get; set; } + + /// + /// Returns true if the icon represents a CSS class instead of a file path + /// + [DataMember(Name = "iconIsClass")] + public bool IconIsClass { - /// - /// Internal constructor, to create a tree node use the CreateTreeNode methods of the TreeApiController. - /// - /// - /// The parent id for the current node - /// - /// - public TreeNode(string nodeId, string? parentId, string? getChildNodesUrl, string? menuUrl) + get { - if (nodeId == null) throw new ArgumentNullException(nameof(nodeId)); - if (string.IsNullOrWhiteSpace(nodeId)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(nodeId)); - - Id = nodeId; - ParentId = parentId; - ChildNodesUrl = getChildNodesUrl; - MenuUrl = menuUrl; - CssClasses = new List(); - //default - Icon = "icon-folder-close"; - Path = "-1"; - } - - [DataMember(Name = "parentId", IsRequired = true)] - public new object? ParentId { get; set; } - - /// - /// A flag to set whether or not this node has children - /// - [DataMember(Name = "hasChildren")] - public bool HasChildren { get; set; } - - /// - /// The tree nodetype which refers to the type of node rendered in the tree - /// - [DataMember(Name = "nodeType")] - public string? NodeType { get; set; } - - /// - /// Optional: The Route path for the editor for this node - /// - /// - /// If this is not set, then the route path will be automatically determined by: {section}/edit/{id} - /// - [DataMember(Name = "routePath")] - public string? RoutePath { get; set; } - - /// - /// The JSON URL to load the nodes children - /// - [DataMember(Name = "childNodesUrl")] - public string? ChildNodesUrl { get; set; } - - /// - /// The JSON URL to load the menu from - /// - [DataMember(Name = "menuUrl")] - public string? MenuUrl { get; set; } - - /// - /// Returns true if the icon represents a CSS class instead of a file path - /// - [DataMember(Name = "iconIsClass")] - public bool IconIsClass - { - get + if (Icon.IsNullOrWhiteSpace()) { - if (Icon.IsNullOrWhiteSpace()) - { - return true; - } - - if (Icon!.StartsWith("..")) - return false; - - - //if it starts with a '.' or doesn't contain a '.' at all then it is a class - return Icon.StartsWith(".") || Icon.Contains(".") == false; + return true; } - } - /// - /// Returns the icon file path if the icon is not a class, otherwise returns an empty string - /// - [DataMember(Name = "iconFilePath")] - public string IconFilePath - { - get + if (Icon!.StartsWith("..")) { - return string.Empty; - - //TODO Figure out how to do this, without the model has to know a bout services and config. - // - // if (IconIsClass) - // return string.Empty; - // - // //absolute path with or without tilde - // if (Icon.StartsWith("~") || Icon.StartsWith("/")) - // return IOHelper.ResolveUrl("~" + Icon.TrimStart(Constants.CharArrays.Tilde)); - // - // //legacy icon path - // return string.Format("{0}images/umbraco/{1}", Current.Configs.Global().Path.EnsureEndsWith("/"), Icon); + return false; } - } - /// - /// A list of additional/custom css classes to assign to the node - /// - [DataMember(Name = "cssClasses")] - public IList CssClasses { get; private set; } + // if it starts with a '.' or doesn't contain a '.' at all then it is a class + return Icon.StartsWith(".") || Icon.Contains(".") == false; + } } + + /// + /// Returns the icon file path if the icon is not a class, otherwise returns an empty string + /// + [DataMember(Name = "iconFilePath")] + public string IconFilePath => string.Empty; + + // TODO Figure out how to do this, without the model has to know a bout services and config. + // + // if (IconIsClass) + // return string.Empty; + // + // //absolute path with or without tilde + // if (Icon.StartsWith("~") || Icon.StartsWith("/")) + // return IOHelper.ResolveUrl("~" + Icon.TrimStart(Constants.CharArrays.Tilde)); + // + // //legacy icon path + // return string.Format("{0}images/umbraco/{1}", Current.Configs.Global().Path.EnsureEndsWith("/"), Icon); + + /// + /// A list of additional/custom css classes to assign to the node + /// + [DataMember(Name = "cssClasses")] + public IList CssClasses { get; private set; } } diff --git a/src/Umbraco.Core/Trees/TreeNodeCollection.cs b/src/Umbraco.Core/Trees/TreeNodeCollection.cs index 545b6881aa..b76fcc41ce 100644 --- a/src/Umbraco.Core/Trees/TreeNodeCollection.cs +++ b/src/Umbraco.Core/Trees/TreeNodeCollection.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Core.Trees +namespace Umbraco.Cms.Core.Trees; + +[CollectionDataContract(Name = "nodes", Namespace = "")] +public sealed class TreeNodeCollection : List { - [CollectionDataContract(Name = "nodes", Namespace = "")] - public sealed class TreeNodeCollection : List + public TreeNodeCollection() { - public static TreeNodeCollection Empty => new TreeNodeCollection(); - - public TreeNodeCollection() - { - } - - public TreeNodeCollection(IEnumerable nodes) - : base(nodes) - { - } } + + public TreeNodeCollection(IEnumerable nodes) + : base(nodes) + { + } + + public static TreeNodeCollection Empty => new(); } diff --git a/src/Umbraco.Core/Trees/TreeNodeExtensions.cs b/src/Umbraco.Core/Trees/TreeNodeExtensions.cs index 9e887f68ec..7fdc8ef480 100644 --- a/src/Umbraco.Core/Trees/TreeNodeExtensions.cs +++ b/src/Umbraco.Core/Trees/TreeNodeExtensions.cs @@ -1,82 +1,79 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Trees; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class TreeNodeExtensions { - public static class TreeNodeExtensions + internal const string LegacyJsCallbackKey = "jsClickCallback"; + + /// + /// Sets the node style to show that it is a container type + /// + /// + public static void SetContainerStyle(this TreeNode treeNode) { - internal const string LegacyJsCallbackKey = "jsClickCallback"; - - /// - /// Legacy tree node's assign a JS method callback for when an item is clicked, this method facilitates that. - /// - /// - /// - internal static void AssignLegacyJsCallback(this TreeNode treeNode, string jsCallback) + if (treeNode.CssClasses.Contains("is-container") == false) { - treeNode.AdditionalData[LegacyJsCallbackKey] = jsCallback; + treeNode.CssClasses.Add("is-container"); } + } - /// - /// Sets the node style to show that it is a container type - /// - /// - public static void SetContainerStyle(this TreeNode treeNode) + /// + /// Legacy tree node's assign a JS method callback for when an item is clicked, this method facilitates that. + /// + /// + /// + internal static void AssignLegacyJsCallback(this TreeNode treeNode, string jsCallback) => + treeNode.AdditionalData[LegacyJsCallbackKey] = jsCallback; + + /// + /// Sets the node style to show that it is currently protected publicly + /// + /// + public static void SetProtectedStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("protected") == false) { - if (treeNode.CssClasses.Contains("is-container") == false) - { - treeNode.CssClasses.Add("is-container"); - } + treeNode.CssClasses.Add("protected"); } + } - /// - /// Sets the node style to show that it is currently protected publicly - /// - /// - public static void SetProtectedStyle(this TreeNode treeNode) + /// + /// Sets the node style to show that it is currently locked / non-deletable + /// + /// + public static void SetLockedStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("locked") == false) { - if (treeNode.CssClasses.Contains("protected") == false) - { - treeNode.CssClasses.Add("protected"); - } + treeNode.CssClasses.Add("locked"); } + } - /// - /// Sets the node style to show that it is currently locked / non-deletable - /// - /// - public static void SetLockedStyle(this TreeNode treeNode) + /// + /// Sets the node style to show that it is has unpublished versions (but is currently published) + /// + /// + public static void SetHasPendingVersionStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("has-unpublished-version") == false) { - if (treeNode.CssClasses.Contains("locked") == false) - { - treeNode.CssClasses.Add("locked"); - } + treeNode.CssClasses.Add("has-unpublished-version"); } + } - /// - /// Sets the node style to show that it is has unpublished versions (but is currently published) - /// - /// - public static void SetHasPendingVersionStyle(this TreeNode treeNode) + /// + /// Sets the node style to show that it is not published + /// + /// + public static void SetNotPublishedStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("not-published") == false) { - if (treeNode.CssClasses.Contains("has-unpublished-version") == false) - { - treeNode.CssClasses.Add("has-unpublished-version"); - } - } - - /// - /// Sets the node style to show that it is not published - /// - /// - public static void SetNotPublishedStyle(this TreeNode treeNode) - { - if (treeNode.CssClasses.Contains("not-published") == false) - { - treeNode.CssClasses.Add("not-published"); - } + treeNode.CssClasses.Add("not-published"); } } } diff --git a/src/Umbraco.Core/Trees/TreeUse.cs b/src/Umbraco.Core/Trees/TreeUse.cs index 55be24d54d..ff06bc1dea 100644 --- a/src/Umbraco.Core/Trees/TreeUse.cs +++ b/src/Umbraco.Core/Trees/TreeUse.cs @@ -1,26 +1,23 @@ -using System; +namespace Umbraco.Cms.Core.Trees; -namespace Umbraco.Cms.Core.Trees +/// +/// Defines tree uses. +/// +[Flags] +public enum TreeUse { /// - /// Defines tree uses. + /// The tree is not used. /// - [Flags] - public enum TreeUse - { - /// - /// The tree is not used. - /// - None = 0, + None = 0, - /// - /// The tree is used as a main (section) tree. - /// - Main = 1, + /// + /// The tree is used as a main (section) tree. + /// + Main = 1, - /// - /// The tree is used as a dialog. - /// - Dialog = 2, - } + /// + /// The tree is used as a dialog. + /// + Dialog = 2, } diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index 2e141e2e66..bc6c1ab6ac 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -1,171 +1,186 @@ -using System; using System.ComponentModel; -using System.Linq; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents an entity identifier. +/// +/// An Udi can be fully qualified or "closed" eg umb://document/{guid} or "open" eg umb://document. +[TypeConverter(typeof(UdiTypeConverter))] +public abstract class Udi : IComparable { + /// + /// Initializes a new instance of the Udi class. + /// + /// The entity type part of the identifier. + /// The string value of the identifier. + protected Udi(string entityType, string stringValue) + { + EntityType = entityType; + UriValue = new Uri(stringValue); + } /// - /// Represents an entity identifier. + /// Initializes a new instance of the Udi class. /// - /// An Udi can be fully qualified or "closed" eg umb://document/{guid} or "open" eg umb://document. - [TypeConverter(typeof(UdiTypeConverter))] - public abstract class Udi : IComparable + /// The uri value of the identifier. + protected Udi(Uri uriValue) { - public Uri UriValue { get; } - - /// - /// Initializes a new instance of the Udi class. - /// - /// The entity type part of the identifier. - /// The string value of the identifier. - protected Udi(string entityType, string stringValue) - { - EntityType = entityType; - UriValue = new Uri(stringValue); - } - - /// - /// Initializes a new instance of the Udi class. - /// - /// The uri value of the identifier. - protected Udi(Uri uriValue) - { - EntityType = uriValue.Host; - UriValue = uriValue; - } - - - - /// - /// Gets the entity type part of the identifier. - /// - public string EntityType { get; private set; } - - public int CompareTo(Udi? other) - { - return string.Compare(UriValue.ToString(), other?.UriValue.ToString(), StringComparison.OrdinalIgnoreCase); - } - - public override string ToString() - { - // UriValue is created in the ctor and is never null - // use AbsoluteUri here and not ToString else it's not encoded! - return UriValue.AbsoluteUri; - } - - - - /// - /// Creates a root Udi for an entity type. - /// - /// The entity type. - /// The root Udi for the entity type. - public static Udi Create(string entityType) - { - return UdiParser.GetRootUdi(entityType); - } - - /// - /// Creates a string Udi. - /// - /// The entity type. - /// The identifier. - /// The string Udi for the entity type and identifier. - public static Udi Create(string entityType, string id) - { - if (UdiParser.UdiTypes.TryGetValue(entityType, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); - - if (string.IsNullOrWhiteSpace(id)) - throw new ArgumentException("Value cannot be null or whitespace.", "id"); - if (udiType != UdiType.StringUdi) - throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have string udis.", entityType)); - - return new StringUdi(entityType, id); - } - - /// - /// Creates a Guid Udi. - /// - /// The entity type. - /// The identifier. - /// The Guid Udi for the entity type and identifier. - public static Udi Create(string? entityType, Guid id) - { - if (entityType is null || UdiParser.UdiTypes.TryGetValue(entityType, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); - - if (udiType != UdiType.GuidUdi) - throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have guid udis.", entityType)); - if (id == default(Guid)) - throw new ArgumentException("Cannot be an empty guid.", "id"); - - return new GuidUdi(entityType, id); - } - - public static Udi Create(Uri uri) - { - // if it's a know type go fast and use ctors - // else fallback to parsing the string (and guess the type) - - if (UdiParser.UdiTypes.TryGetValue(uri.Host, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); - - if (udiType == UdiType.GuidUdi) - return new GuidUdi(uri); - if (udiType == UdiType.StringUdi) - return new StringUdi(uri); - - throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); - } - - public void EnsureType(params string[] validTypes) - { - if (validTypes.Contains(EntityType) == false) - throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); - } - - /// - /// Gets a value indicating whether this Udi is a root Udi. - /// - /// A root Udi points to the "root of all things" for a given entity type, e.g. the content tree root. - public abstract bool IsRoot { get; } - - /// - /// Ensures that this Udi is not a root Udi. - /// - /// This Udi. - /// When this Udi is a Root Udi. - public Udi EnsureNotRoot() - { - if (IsRoot) throw new Exception("Root Udi."); - return this; - } - - public override bool Equals(object? obj) - { - var other = obj as Udi; - return other is not null && GetType() == other.GetType() && UriValue == other.UriValue; - } - - public override int GetHashCode() - { - return UriValue.GetHashCode(); - } - - public static bool operator ==(Udi? udi1, Udi? udi2) - { - if (ReferenceEquals(udi1, udi2)) return true; - if ((object?)udi1 == null || (object?)udi2 == null) return false; - return udi1.Equals(udi2); - } - - public static bool operator !=(Udi? udi1, Udi? udi2) - { - return (udi1 == udi2) == false; - } - - + EntityType = uriValue.Host; + UriValue = uriValue; } + + public Uri UriValue { get; } + + /// + /// Gets the entity type part of the identifier. + /// + public string EntityType { get; } + + /// + /// Gets a value indicating whether this Udi is a root Udi. + /// + /// A root Udi points to the "root of all things" for a given entity type, e.g. the content tree root. + public abstract bool IsRoot { get; } + + public static bool operator ==(Udi? udi1, Udi? udi2) + { + if (ReferenceEquals(udi1, udi2)) + { + return true; + } + + if (udi1 is null || udi2 is null) + { + return false; + } + + return udi1.Equals(udi2); + } + + /// + /// Creates a root Udi for an entity type. + /// + /// The entity type. + /// The root Udi for the entity type. + public static Udi Create(string entityType) => UdiParser.GetRootUdi(entityType); + + public int CompareTo(Udi? other) => string.Compare(UriValue.ToString(), other?.UriValue.ToString(), StringComparison.OrdinalIgnoreCase); + + public override string ToString() => + + // UriValue is created in the ctor and is never null + // use AbsoluteUri here and not ToString else it's not encoded! + UriValue.AbsoluteUri; + + /// + /// Creates a string Udi. + /// + /// The entity type. + /// The identifier. + /// The string Udi for the entity type and identifier. + public static Udi Create(string entityType, string id) + { + if (UdiParser.UdiTypes.TryGetValue(entityType, out UdiType udiType) == false) + { + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); + } + + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("Value cannot be null or whitespace.", "id"); + } + + if (udiType != UdiType.StringUdi) + { + throw new InvalidOperationException(string.Format( + "Entity type \"{0}\" does not have string udis.", + entityType)); + } + + return new StringUdi(entityType, id); + } + + /// + /// Creates a Guid Udi. + /// + /// The entity type. + /// The identifier. + /// The Guid Udi for the entity type and identifier. + public static Udi Create(string? entityType, Guid id) + { + if (entityType is null || UdiParser.UdiTypes.TryGetValue(entityType, out UdiType udiType) == false) + { + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); + } + + if (udiType != UdiType.GuidUdi) + { + throw new InvalidOperationException(string.Format( + "Entity type \"{0}\" does not have guid udis.", + entityType)); + } + + if (id == default) + { + throw new ArgumentException("Cannot be an empty guid.", "id"); + } + + return new GuidUdi(entityType, id); + } + + public static Udi Create(Uri uri) + { + // if it's a know type go fast and use ctors + // else fallback to parsing the string (and guess the type) + if (UdiParser.UdiTypes.TryGetValue(uri.Host, out UdiType udiType) == false) + { + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); + } + + if (udiType == UdiType.GuidUdi) + { + return new GuidUdi(uri); + } + + if (udiType == UdiType.StringUdi) + { + return new StringUdi(uri); + } + + throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); + } + + public void EnsureType(params string[] validTypes) + { + if (validTypes.Contains(EntityType) == false) + { + throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); + } + } + + /// + /// Ensures that this Udi is not a root Udi. + /// + /// This Udi. + /// When this Udi is a Root Udi. + public Udi EnsureNotRoot() + { + if (IsRoot) + { + throw new Exception("Root Udi."); + } + + return this; + } + + public override bool Equals(object? obj) + { + var other = obj as Udi; + return other is not null && GetType() == other.GetType() && UriValue == other.UriValue; + } + + public override int GetHashCode() => UriValue.GetHashCode(); + + public static bool operator !=(Udi? udi1, Udi? udi2) => udi1 == udi2 == false; } diff --git a/src/Umbraco.Core/UdiDefinitionAttribute.cs b/src/Umbraco.Core/UdiDefinitionAttribute.cs index 9139ef4188..fe96909f78 100644 --- a/src/Umbraco.Core/UdiDefinitionAttribute.cs +++ b/src/Umbraco.Core/UdiDefinitionAttribute.cs @@ -1,20 +1,25 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public sealed class UdiDefinitionAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class UdiDefinitionAttribute : Attribute + public UdiDefinitionAttribute(string entityType, UdiType udiType) { - public UdiDefinitionAttribute(string entityType, UdiType udiType) + if (string.IsNullOrWhiteSpace(entityType)) { - if (string.IsNullOrWhiteSpace(entityType)) throw new ArgumentNullException("entityType"); - if (udiType != UdiType.GuidUdi && udiType != UdiType.StringUdi) throw new ArgumentException("Invalid value.", "udiType"); - EntityType = entityType; - UdiType = udiType; + throw new ArgumentNullException("entityType"); } - public string EntityType { get; private set; } + if (udiType != UdiType.GuidUdi && udiType != UdiType.StringUdi) + { + throw new ArgumentException("Invalid value.", "udiType"); + } - public UdiType UdiType { get; private set; } + EntityType = entityType; + UdiType = udiType; } + + public string EntityType { get; } + + public UdiType UdiType { get; } } diff --git a/src/Umbraco.Core/UdiEntityTypeHelper.cs b/src/Umbraco.Core/UdiEntityTypeHelper.cs index 781c084785..f0e8774cf8 100644 --- a/src/Umbraco.Core/UdiEntityTypeHelper.cs +++ b/src/Umbraco.Core/UdiEntityTypeHelper.cs @@ -1,102 +1,98 @@ -using System; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class UdiEntityTypeHelper { - public static class UdiEntityTypeHelper + public static string FromUmbracoObjectType(UmbracoObjectTypes umbracoObjectType) { - - - public static string FromUmbracoObjectType(UmbracoObjectTypes umbracoObjectType) + switch (umbracoObjectType) { - switch (umbracoObjectType) - { - case UmbracoObjectTypes.Document: - return Constants.UdiEntityType.Document; - case UmbracoObjectTypes.DocumentBlueprint: - return Constants.UdiEntityType.DocumentBlueprint; - case UmbracoObjectTypes.Media: - return Constants.UdiEntityType.Media; - case UmbracoObjectTypes.Member: - return Constants.UdiEntityType.Member; - case UmbracoObjectTypes.Template: - return Constants.UdiEntityType.Template; - case UmbracoObjectTypes.DocumentType: - return Constants.UdiEntityType.DocumentType; - case UmbracoObjectTypes.DocumentTypeContainer: - return Constants.UdiEntityType.DocumentTypeContainer; - case UmbracoObjectTypes.MediaType: - return Constants.UdiEntityType.MediaType; - case UmbracoObjectTypes.MediaTypeContainer: - return Constants.UdiEntityType.MediaTypeContainer; - case UmbracoObjectTypes.DataType: - return Constants.UdiEntityType.DataType; - case UmbracoObjectTypes.DataTypeContainer: - return Constants.UdiEntityType.DataTypeContainer; - case UmbracoObjectTypes.MemberType: - return Constants.UdiEntityType.MemberType; - case UmbracoObjectTypes.MemberGroup: - return Constants.UdiEntityType.MemberGroup; - case UmbracoObjectTypes.RelationType: - return Constants.UdiEntityType.RelationType; - case UmbracoObjectTypes.FormsForm: - return Constants.UdiEntityType.FormsForm; - case UmbracoObjectTypes.FormsPreValue: - return Constants.UdiEntityType.FormsPreValue; - case UmbracoObjectTypes.FormsDataSource: - return Constants.UdiEntityType.FormsDataSource; - case UmbracoObjectTypes.Language: - return Constants.UdiEntityType.Language; - } - - throw new NotSupportedException( - $"UmbracoObjectType \"{umbracoObjectType}\" does not have a matching EntityType."); + case UmbracoObjectTypes.Document: + return Constants.UdiEntityType.Document; + case UmbracoObjectTypes.DocumentBlueprint: + return Constants.UdiEntityType.DocumentBlueprint; + case UmbracoObjectTypes.Media: + return Constants.UdiEntityType.Media; + case UmbracoObjectTypes.Member: + return Constants.UdiEntityType.Member; + case UmbracoObjectTypes.Template: + return Constants.UdiEntityType.Template; + case UmbracoObjectTypes.DocumentType: + return Constants.UdiEntityType.DocumentType; + case UmbracoObjectTypes.DocumentTypeContainer: + return Constants.UdiEntityType.DocumentTypeContainer; + case UmbracoObjectTypes.MediaType: + return Constants.UdiEntityType.MediaType; + case UmbracoObjectTypes.MediaTypeContainer: + return Constants.UdiEntityType.MediaTypeContainer; + case UmbracoObjectTypes.DataType: + return Constants.UdiEntityType.DataType; + case UmbracoObjectTypes.DataTypeContainer: + return Constants.UdiEntityType.DataTypeContainer; + case UmbracoObjectTypes.MemberType: + return Constants.UdiEntityType.MemberType; + case UmbracoObjectTypes.MemberGroup: + return Constants.UdiEntityType.MemberGroup; + case UmbracoObjectTypes.RelationType: + return Constants.UdiEntityType.RelationType; + case UmbracoObjectTypes.FormsForm: + return Constants.UdiEntityType.FormsForm; + case UmbracoObjectTypes.FormsPreValue: + return Constants.UdiEntityType.FormsPreValue; + case UmbracoObjectTypes.FormsDataSource: + return Constants.UdiEntityType.FormsDataSource; + case UmbracoObjectTypes.Language: + return Constants.UdiEntityType.Language; } - public static UmbracoObjectTypes ToUmbracoObjectType(string entityType) - { - switch (entityType) - { - case Constants.UdiEntityType.Document: - return UmbracoObjectTypes.Document; - case Constants.UdiEntityType.DocumentBlueprint: - return UmbracoObjectTypes.DocumentBlueprint; - case Constants.UdiEntityType.Media: - return UmbracoObjectTypes.Media; - case Constants.UdiEntityType.Member: - return UmbracoObjectTypes.Member; - case Constants.UdiEntityType.Template: - return UmbracoObjectTypes.Template; - case Constants.UdiEntityType.DocumentType: - return UmbracoObjectTypes.DocumentType; - case Constants.UdiEntityType.DocumentTypeContainer: - return UmbracoObjectTypes.DocumentTypeContainer; - case Constants.UdiEntityType.MediaType: - return UmbracoObjectTypes.MediaType; - case Constants.UdiEntityType.MediaTypeContainer: - return UmbracoObjectTypes.MediaTypeContainer; - case Constants.UdiEntityType.DataType: - return UmbracoObjectTypes.DataType; - case Constants.UdiEntityType.DataTypeContainer: - return UmbracoObjectTypes.DataTypeContainer; - case Constants.UdiEntityType.MemberType: - return UmbracoObjectTypes.MemberType; - case Constants.UdiEntityType.MemberGroup: - return UmbracoObjectTypes.MemberGroup; - case Constants.UdiEntityType.RelationType: - return UmbracoObjectTypes.RelationType; - case Constants.UdiEntityType.FormsForm: - return UmbracoObjectTypes.FormsForm; - case Constants.UdiEntityType.FormsPreValue: - return UmbracoObjectTypes.FormsPreValue; - case Constants.UdiEntityType.FormsDataSource: - return UmbracoObjectTypes.FormsDataSource; - case Constants.UdiEntityType.Language: - return UmbracoObjectTypes.Language; - } + throw new NotSupportedException( + $"UmbracoObjectType \"{umbracoObjectType}\" does not have a matching EntityType."); + } - throw new NotSupportedException( - $"EntityType \"{entityType}\" does not have a matching UmbracoObjectType."); + public static UmbracoObjectTypes ToUmbracoObjectType(string entityType) + { + switch (entityType) + { + case Constants.UdiEntityType.Document: + return UmbracoObjectTypes.Document; + case Constants.UdiEntityType.DocumentBlueprint: + return UmbracoObjectTypes.DocumentBlueprint; + case Constants.UdiEntityType.Media: + return UmbracoObjectTypes.Media; + case Constants.UdiEntityType.Member: + return UmbracoObjectTypes.Member; + case Constants.UdiEntityType.Template: + return UmbracoObjectTypes.Template; + case Constants.UdiEntityType.DocumentType: + return UmbracoObjectTypes.DocumentType; + case Constants.UdiEntityType.DocumentTypeContainer: + return UmbracoObjectTypes.DocumentTypeContainer; + case Constants.UdiEntityType.MediaType: + return UmbracoObjectTypes.MediaType; + case Constants.UdiEntityType.MediaTypeContainer: + return UmbracoObjectTypes.MediaTypeContainer; + case Constants.UdiEntityType.DataType: + return UmbracoObjectTypes.DataType; + case Constants.UdiEntityType.DataTypeContainer: + return UmbracoObjectTypes.DataTypeContainer; + case Constants.UdiEntityType.MemberType: + return UmbracoObjectTypes.MemberType; + case Constants.UdiEntityType.MemberGroup: + return UmbracoObjectTypes.MemberGroup; + case Constants.UdiEntityType.RelationType: + return UmbracoObjectTypes.RelationType; + case Constants.UdiEntityType.FormsForm: + return UmbracoObjectTypes.FormsForm; + case Constants.UdiEntityType.FormsPreValue: + return UmbracoObjectTypes.FormsPreValue; + case Constants.UdiEntityType.FormsDataSource: + return UmbracoObjectTypes.FormsDataSource; + case Constants.UdiEntityType.Language: + return UmbracoObjectTypes.Language; } + + throw new NotSupportedException( + $"EntityType \"{entityType}\" does not have a matching UmbracoObjectType."); } } diff --git a/src/Umbraco.Core/UdiParser.cs b/src/Umbraco.Core/UdiParser.cs index 907880db13..30448e1b45 100644 --- a/src/Umbraco.Core/UdiParser.cs +++ b/src/Umbraco.Core/UdiParser.cs @@ -1,222 +1,235 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public sealed class UdiParser { - public sealed class UdiParser + private static readonly ConcurrentDictionary RootUdis = new(); + + static UdiParser() => + + // initialize with known (built-in) Udi types + // we will add scanned types later on + UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); + + internal static ConcurrentDictionary UdiTypes { get; private set; } + + /// + /// Internal API for tests to resets all udi types back to only the known udi types. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static void ResetUdiTypes() => UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); + + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// An Udi instance that contains the value that was parsed. + public static Udi Parse(string s) { - private static readonly ConcurrentDictionary RootUdis = new ConcurrentDictionary(); - internal static ConcurrentDictionary UdiTypes { get; private set; } + ParseInternal(s, false, false, out Udi? udi); + return udi!; + } - static UdiParser() + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// A value indicating whether to only deal with known types. + /// An Udi instance that contains the value that was parsed. + /// + /// + /// If is true, and the string could not be parsed because + /// the entity type was not known, the method succeeds but sets udito an + /// value. + /// + /// + /// If is true, assemblies are not scanned for types, + /// and therefore only builtin types may be known. Unless scanning already took place. + /// + /// + public static Udi Parse(string s, bool knownTypes) + { + ParseInternal(s, false, knownTypes, out Udi? udi); + return udi!; + } + + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// An Udi instance that contains the value that was parsed. + /// A boolean value indicating whether the string could be parsed. + public static bool TryParse(string s, [MaybeNullWhen(false)] out Udi udi) => ParseInternal(s, true, false, out udi); + + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// An Udi instance that contains the value that was parsed. + /// A boolean value indicating whether the string could be parsed. + public static bool TryParse(string? s, [MaybeNullWhen(false)] out T udi) + where T : Udi? + { + var result = ParseInternal(s, true, false, out Udi? parsed); + if (result && parsed is T) { - // initialize with known (built-in) Udi types - // we will add scanned types later on - UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); + udi = (T)parsed; + return true; } - /// - /// Internal API for tests to resets all udi types back to only the known udi types. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static void ResetUdiTypes() - { - UdiTypes = new ConcurrentDictionary(GetKnownUdiTypes()); - } + udi = null; + return false; + } - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// An Udi instance that contains the value that was parsed. - public static Udi Parse(string s) - { - ParseInternal(s, false, false, out var udi); - return udi!; - } + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// A value indicating whether to only deal with known types. + /// An Udi instance that contains the value that was parsed. + /// A boolean value indicating whether the string could be parsed. + /// + /// + /// If is true, and the string could not be parsed because + /// the entity type was not known, the method returns false but still sets udi + /// to an value. + /// + /// + /// If is true, assemblies are not scanned for types, + /// and therefore only builtin types may be known. Unless scanning already took place. + /// + /// + public static bool TryParse(string? s, bool knownTypes, [MaybeNullWhen(false)] out Udi udi) => + ParseInternal(s, true, knownTypes, out udi); - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// A value indicating whether to only deal with known types. - /// An Udi instance that contains the value that was parsed. - /// - /// If is true, and the string could not be parsed because - /// the entity type was not known, the method succeeds but sets udito an - /// value. - /// If is true, assemblies are not scanned for types, - /// and therefore only builtin types may be known. Unless scanning already took place. - /// - public static Udi Parse(string s, bool knownTypes) - { - ParseInternal(s, false, knownTypes, out var udi); - return udi!; - } + /// + /// Registers a custom entity type. + /// + /// + /// + public static void RegisterUdiType(string entityType, UdiType udiType) => UdiTypes.TryAdd(entityType, udiType); - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// An Udi instance that contains the value that was parsed. - /// A boolean value indicating whether the string could be parsed. - public static bool TryParse(string s, [MaybeNullWhen(returnValue: false)] out Udi udi) + internal static Udi GetRootUdi(string entityType) => + RootUdis.GetOrAdd(entityType, x => { - return ParseInternal(s, true, false, out udi); - } - - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// An Udi instance that contains the value that was parsed. - /// A boolean value indicating whether the string could be parsed. - public static bool TryParse(string? s, [MaybeNullWhen(returnValue: false)] out T udi) - where T : Udi? - { - var result = ParseInternal(s, true, false, out var parsed); - if (result && parsed is T) + if (UdiTypes.TryGetValue(x, out UdiType udiType) == false) { - udi = (T)parsed; + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); + } + + return udiType == UdiType.StringUdi + ? new StringUdi(entityType, string.Empty) + : new GuidUdi(entityType, Guid.Empty); + }); + + private static bool ParseInternal(string? s, bool tryParse, bool knownTypes, [MaybeNullWhen(false)] out Udi udi) + { + udi = null; + if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false + || Uri.TryCreate(s, UriKind.Absolute, out Uri? uri) == false) + { + if (tryParse) + { + return false; + } + + throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); + } + + var entityType = uri.Host; + if (UdiTypes.TryGetValue(entityType, out UdiType udiType) == false) + { + if (knownTypes) + { + // not knowing the type is not an error + // just return the unknown type udi + udi = UnknownTypeUdi.Instance; + return false; + } + + if (tryParse) + { + return false; + } + + throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType)); + } + + var path = uri.AbsolutePath.TrimStart('/'); + + if (udiType == UdiType.GuidUdi) + { + if (path == string.Empty) + { + udi = GetRootUdi(uri.Host); return true; } - udi = null; - return false; - } - - /// - /// Converts the string representation of an entity identifier into the equivalent Udi instance. - /// - /// The string to convert. - /// A value indicating whether to only deal with known types. - /// An Udi instance that contains the value that was parsed. - /// A boolean value indicating whether the string could be parsed. - /// - /// If is true, and the string could not be parsed because - /// the entity type was not known, the method returns false but still sets udi - /// to an value. - /// If is true, assemblies are not scanned for types, - /// and therefore only builtin types may be known. Unless scanning already took place. - /// - public static bool TryParse(string? s, bool knownTypes, [MaybeNullWhen(returnValue: false)] out Udi udi) - { - return ParseInternal(s, true, knownTypes, out udi); - } - - private static bool ParseInternal(string? s, bool tryParse, bool knownTypes,[MaybeNullWhen(returnValue: false)] out Udi udi) - { - udi = null; - if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false - || Uri.TryCreate(s, UriKind.Absolute, out var uri) == false) + if (Guid.TryParse(path, out Guid guid) == false) { - if (tryParse) return false; + if (tryParse) + { + return false; + } + throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); } - var entityType = uri.Host; - if (UdiTypes.TryGetValue(entityType, out var udiType) == false) - { - if (knownTypes) - { - // not knowing the type is not an error - // just return the unknown type udi - udi = UnknownTypeUdi.Instance; - return false; - } - if (tryParse) return false; - throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType)); - } - - var path = uri.AbsolutePath.TrimStart('/'); - - if (udiType == UdiType.GuidUdi) - { - if (path == string.Empty) - { - udi = GetRootUdi(uri.Host); - return true; - } - if (Guid.TryParse(path, out var guid) == false) - { - if (tryParse) return false; - throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); - } - udi = new GuidUdi(uri.Host, guid); - return true; - } - - if (udiType == UdiType.StringUdi) - { - udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, Uri.UnescapeDataString(path)); - return true; - } - - if (tryParse) return false; - throw new InvalidOperationException(string.Format("Invalid udi type \"{0}\".", udiType)); + udi = new GuidUdi(uri.Host, guid); + return true; } - internal static Udi GetRootUdi(string entityType) + if (udiType == UdiType.StringUdi) { - return RootUdis.GetOrAdd(entityType, x => - { - if (UdiTypes.TryGetValue(x, out var udiType) == false) - throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); - return udiType == UdiType.StringUdi - ? (Udi)new StringUdi(entityType, string.Empty) - : new GuidUdi(entityType, Guid.Empty); - }); + udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, Uri.UnescapeDataString(path)); + return true; } + if (tryParse) + { + return false; + } - - /// - /// Registers a custom entity type. - /// - /// - /// - public static void RegisterUdiType(string entityType, UdiType udiType) => UdiTypes.TryAdd(entityType, udiType); - - public static Dictionary GetKnownUdiTypes() => - new Dictionary - { - { Constants.UdiEntityType.Unknown, UdiType.Unknown }, - - { Constants.UdiEntityType.AnyGuid, UdiType.GuidUdi }, - { Constants.UdiEntityType.Element, UdiType.GuidUdi }, - { Constants.UdiEntityType.Document, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi }, - { Constants.UdiEntityType.Media, UdiType.GuidUdi }, - { Constants.UdiEntityType.Member, UdiType.GuidUdi }, - { Constants.UdiEntityType.DictionaryItem, UdiType.GuidUdi }, - { Constants.UdiEntityType.Macro, UdiType.GuidUdi }, - { Constants.UdiEntityType.Template, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentType, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentTypeBluePrints, UdiType.GuidUdi }, - { Constants.UdiEntityType.MediaType, UdiType.GuidUdi }, - { Constants.UdiEntityType.MediaTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.DataType, UdiType.GuidUdi }, - { Constants.UdiEntityType.DataTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.MemberType, UdiType.GuidUdi }, - { Constants.UdiEntityType.MemberGroup, UdiType.GuidUdi }, - { Constants.UdiEntityType.RelationType, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsForm, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsPreValue, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsDataSource, UdiType.GuidUdi }, - - { Constants.UdiEntityType.AnyString, UdiType.StringUdi }, - { Constants.UdiEntityType.Language, UdiType.StringUdi }, - { Constants.UdiEntityType.MacroScript, UdiType.StringUdi }, - { Constants.UdiEntityType.MediaFile, UdiType.StringUdi }, - { Constants.UdiEntityType.TemplateFile, UdiType.StringUdi }, - { Constants.UdiEntityType.Script, UdiType.StringUdi }, - { Constants.UdiEntityType.PartialView, UdiType.StringUdi }, - { Constants.UdiEntityType.PartialViewMacro, UdiType.StringUdi }, - { Constants.UdiEntityType.Stylesheet, UdiType.StringUdi } - }; + throw new InvalidOperationException(string.Format("Invalid udi type \"{0}\".", udiType)); } + + public static Dictionary GetKnownUdiTypes() => + new() + { + { Constants.UdiEntityType.Unknown, UdiType.Unknown }, + { Constants.UdiEntityType.AnyGuid, UdiType.GuidUdi }, + { Constants.UdiEntityType.Element, UdiType.GuidUdi }, + { Constants.UdiEntityType.Document, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi }, + { Constants.UdiEntityType.Media, UdiType.GuidUdi }, + { Constants.UdiEntityType.Member, UdiType.GuidUdi }, + { Constants.UdiEntityType.DictionaryItem, UdiType.GuidUdi }, + { Constants.UdiEntityType.Macro, UdiType.GuidUdi }, + { Constants.UdiEntityType.Template, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentType, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentTypeContainer, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentTypeBluePrints, UdiType.GuidUdi }, + { Constants.UdiEntityType.MediaType, UdiType.GuidUdi }, + { Constants.UdiEntityType.MediaTypeContainer, UdiType.GuidUdi }, + { Constants.UdiEntityType.DataType, UdiType.GuidUdi }, + { Constants.UdiEntityType.DataTypeContainer, UdiType.GuidUdi }, + { Constants.UdiEntityType.MemberType, UdiType.GuidUdi }, + { Constants.UdiEntityType.MemberGroup, UdiType.GuidUdi }, + { Constants.UdiEntityType.RelationType, UdiType.GuidUdi }, + { Constants.UdiEntityType.FormsForm, UdiType.GuidUdi }, + { Constants.UdiEntityType.FormsPreValue, UdiType.GuidUdi }, + { Constants.UdiEntityType.FormsDataSource, UdiType.GuidUdi }, + { Constants.UdiEntityType.AnyString, UdiType.StringUdi }, + { Constants.UdiEntityType.Language, UdiType.StringUdi }, + { Constants.UdiEntityType.MacroScript, UdiType.StringUdi }, + { Constants.UdiEntityType.MediaFile, UdiType.StringUdi }, + { Constants.UdiEntityType.TemplateFile, UdiType.StringUdi }, + { Constants.UdiEntityType.Script, UdiType.StringUdi }, + { Constants.UdiEntityType.PartialView, UdiType.StringUdi }, + { Constants.UdiEntityType.PartialViewMacro, UdiType.StringUdi }, + { Constants.UdiEntityType.Stylesheet, UdiType.StringUdi }, + }; } diff --git a/src/Umbraco.Core/UdiParserServiceConnectors.cs b/src/Umbraco.Core/UdiParserServiceConnectors.cs index 320cc9a901..4c307435de 100644 --- a/src/Umbraco.Core/UdiParserServiceConnectors.cs +++ b/src/Umbraco.Core/UdiParserServiceConnectors.cs @@ -1,82 +1,99 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Deploy; using Umbraco.Extensions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class UdiParserServiceConnectors { - public static class UdiParserServiceConnectors + private static readonly object ScanLocker = new(); + + // notes - see U4-10409 + // if this class is used during application pre-start it cannot scans the assemblies, + // this is addressed by lazily-scanning, with the following caveats: + // - parsing a root udi still requires a scan and therefore still breaks + // - parsing an invalid udi ("umb://should-be-guid/") corrupts KnowUdiTypes + private static volatile bool _scanned; + + /// + /// Scan for deploy in assemblies for known UDI types. + /// + /// + public static void ScanDeployServiceConnectorsForUdiTypes(TypeLoader typeLoader) { - // notes - see U4-10409 - // if this class is used during application pre-start it cannot scans the assemblies, - // this is addressed by lazily-scanning, with the following caveats: - // - parsing a root udi still requires a scan and therefore still breaks - // - parsing an invalid udi ("umb://should-be-guid/") corrupts KnowUdiTypes - - private static volatile bool _scanned; - private static readonly object ScanLocker = new object(); - - /// - /// Scan for deploy in assemblies for known UDI types. - /// - /// - public static void ScanDeployServiceConnectorsForUdiTypes(TypeLoader typeLoader) + if (typeLoader is null) { - if (typeLoader is null) - throw new ArgumentNullException(nameof(typeLoader)); - - if (_scanned) return; - - lock (ScanLocker) - { - // Scan for unknown UDI types - // there is no way we can get the "registered" service connectors, as registration - // happens in Deploy, not in Core, and the Udi class belongs to Core - therefore, we - // just pick every service connectors - just making sure that not two of them - // would register the same entity type, with different udi types (would not make - // much sense anyways) - var connectors = typeLoader.GetTypes(); - var result = new Dictionary(); - foreach (var connector in connectors) - { - var attrs = connector.GetCustomAttributes(false); - foreach (var attr in attrs) - { - if (result.TryGetValue(attr.EntityType, out var udiType) && udiType != attr.UdiType) - throw new Exception(string.Format("Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", attr.EntityType)); - result[attr.EntityType] = attr.UdiType; - } - } - - // merge these into the known list - foreach (var item in result) - UdiParser.RegisterUdiType(item.Key, item.Value); - - _scanned = true; - } + throw new ArgumentNullException(nameof(typeLoader)); } - /// - /// Registers a single to add it's UDI type. - /// - /// - public static void RegisterServiceConnector() - where T: IServiceConnector + if (_scanned) { + return; + } + + lock (ScanLocker) + { + // Scan for unknown UDI types + // there is no way we can get the "registered" service connectors, as registration + // happens in Deploy, not in Core, and the Udi class belongs to Core - therefore, we + // just pick every service connectors - just making sure that not two of them + // would register the same entity type, with different udi types (would not make + // much sense anyways) + IEnumerable connectors = typeLoader.GetTypes(); var result = new Dictionary(); - var connector = typeof(T); - var attrs = connector.GetCustomAttributes(false); - foreach (var attr in attrs) + foreach (Type connector in connectors) { - if (result.TryGetValue(attr.EntityType, out var udiType) && udiType != attr.UdiType) - throw new Exception(string.Format("Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", attr.EntityType)); - result[attr.EntityType] = attr.UdiType; + IEnumerable + attrs = connector.GetCustomAttributes(false); + foreach (UdiDefinitionAttribute attr in attrs) + { + if (result.TryGetValue(attr.EntityType, out UdiType udiType) && udiType != attr.UdiType) + { + throw new Exception(string.Format( + "Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", + attr.EntityType)); + } + + result[attr.EntityType] = attr.UdiType; + } } // merge these into the known list - foreach (var item in result) + foreach (KeyValuePair item in result) + { UdiParser.RegisterUdiType(item.Key, item.Value); + } + + _scanned = true; + } + } + + /// + /// Registers a single to add it's UDI type. + /// + /// + public static void RegisterServiceConnector() + where T : IServiceConnector + { + var result = new Dictionary(); + Type connector = typeof(T); + IEnumerable attrs = connector.GetCustomAttributes(false); + foreach (UdiDefinitionAttribute attr in attrs) + { + if (result.TryGetValue(attr.EntityType, out UdiType udiType) && udiType != attr.UdiType) + { + throw new Exception(string.Format( + "Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", + attr.EntityType)); + } + + result[attr.EntityType] = attr.UdiType; + } + + // merge these into the known list + foreach (KeyValuePair item in result) + { + UdiParser.RegisterUdiType(item.Key, item.Value); } } } diff --git a/src/Umbraco.Core/UdiRange.cs b/src/Umbraco.Core/UdiRange.cs index ca5b07bf36..5d98664a3e 100644 --- a/src/Umbraco.Core/UdiRange.cs +++ b/src/Umbraco.Core/UdiRange.cs @@ -1,103 +1,96 @@ -using System; +namespace Umbraco.Cms.Core; -namespace Umbraco.Cms.Core +/// +/// Represents a range. +/// +/// +/// +/// A Udi range is composed of a which represents the base of the range, +/// plus a selector that can be "." (the Udi), ".*" (the Udi and its children), ".**" (the udi and +/// its descendants, "*" (the children of the Udi), and "**" (the descendants of the Udi). +/// +/// The Udi here can be a closed entity, or an open entity. +/// +public class UdiRange { + private readonly Uri _uriValue; + /// - /// Represents a range. + /// Initializes a new instance of the class with a and an optional + /// selector. /// - /// - /// A Udi range is composed of a which represents the base of the range, - /// plus a selector that can be "." (the Udi), ".*" (the Udi and its children), ".**" (the udi and - /// its descendants, "*" (the children of the Udi), and "**" (the descendants of the Udi). - /// The Udi here can be a closed entity, or an open entity. - public class UdiRange + /// A . + /// An optional selector. + public UdiRange(Udi udi, string selector = Constants.DeploySelector.This) { - private readonly Uri _uriValue; - - /// - /// Initializes a new instance of the class with a and an optional selector. - /// - /// A . - /// An optional selector. - public UdiRange(Udi udi, string selector = Constants.DeploySelector.This) + Udi = udi; + switch (selector) { - Udi = udi; - switch (selector) - { - case Constants.DeploySelector.This: - Selector = selector; - _uriValue = udi.UriValue; - break; - case Constants.DeploySelector.ChildrenOfThis: - case Constants.DeploySelector.DescendantsOfThis: - case Constants.DeploySelector.ThisAndChildren: - case Constants.DeploySelector.ThisAndDescendants: - Selector = selector; - _uriValue = new Uri(Udi + "?" + selector); - break; - default: - throw new ArgumentException(string.Format("Invalid selector \"{0}\".", selector)); - } - } - - /// - /// Gets the for this range. - /// - public Udi Udi { get; private set; } - - /// - /// Gets or sets the selector for this range. - /// - public string Selector { get; private set; } - - /// - /// Gets the entity type of the for this range. - /// - public string EntityType - { - get { return Udi.EntityType; } - } - - public static UdiRange Parse(string s) - { - Uri? uri; - - if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false - || Uri.TryCreate(s, UriKind.Absolute, out uri) == false) - { - //if (tryParse) return false; - throw new FormatException(string.Format("String \"{0}\" is not a valid udi range.", s)); - } - - var udiUri = uri.Query == string.Empty ? uri : new UriBuilder(uri) { Query = string.Empty }.Uri; - return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart(Constants.CharArrays.QuestionMark)); - } - - public override string ToString() - { - return _uriValue.ToString(); - } - - public override bool Equals(object? obj) - { - return obj is UdiRange other && GetType() == other.GetType() && _uriValue == other._uriValue; - } - - public override int GetHashCode() - { - return _uriValue.GetHashCode(); - } - - public static bool operator ==(UdiRange range1, UdiRange range2) - { - if (ReferenceEquals(range1, range2)) return true; - if ((object)range1 == null || (object)range2 == null) return false; - return range1.Equals(range2); - } - - public static bool operator !=(UdiRange range1, UdiRange range2) - { - return !(range1 == range2); + case Constants.DeploySelector.This: + Selector = selector; + _uriValue = udi.UriValue; + break; + case Constants.DeploySelector.ChildrenOfThis: + case Constants.DeploySelector.DescendantsOfThis: + case Constants.DeploySelector.ThisAndChildren: + case Constants.DeploySelector.ThisAndDescendants: + Selector = selector; + _uriValue = new Uri(Udi + "?" + selector); + break; + default: + throw new ArgumentException(string.Format("Invalid selector \"{0}\".", selector)); } } + + /// + /// Gets the for this range. + /// + public Udi Udi { get; } + + /// + /// Gets or sets the selector for this range. + /// + public string Selector { get; } + + /// + /// Gets the entity type of the for this range. + /// + public string EntityType => Udi.EntityType; + + public static bool operator ==(UdiRange? range1, UdiRange? range2) + { + if (ReferenceEquals(range1, range2)) + { + return true; + } + + if (range1 is null || range2 is null) + { + return false; + } + + return range1.Equals(range2); + } + + public static bool operator !=(UdiRange range1, UdiRange range2) => !(range1 == range2); + + public static UdiRange Parse(string s) + { + if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false + || Uri.TryCreate(s, UriKind.Absolute, out Uri? uri) == false) + { + // if (tryParse) return false; + throw new FormatException(string.Format("String \"{0}\" is not a valid udi range.", s)); + } + + Uri udiUri = uri.Query == string.Empty ? uri : new UriBuilder(uri) { Query = string.Empty }.Uri; + return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart(Constants.CharArrays.QuestionMark)); + } + + public override string ToString() => _uriValue.ToString(); + + public override bool Equals(object? obj) => + obj is UdiRange other && GetType() == other.GetType() && _uriValue == other._uriValue; + + public override int GetHashCode() => _uriValue.GetHashCode(); } diff --git a/src/Umbraco.Core/UdiType.cs b/src/Umbraco.Core/UdiType.cs index 572c36de95..e5ebd2f7ce 100644 --- a/src/Umbraco.Core/UdiType.cs +++ b/src/Umbraco.Core/UdiType.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Defines Udi types. +/// +public enum UdiType { - /// - /// Defines Udi types. - /// - public enum UdiType - { - Unknown, - GuidUdi, - StringUdi - } + Unknown, + GuidUdi, + StringUdi, } diff --git a/src/Umbraco.Core/UdiTypeConverter.cs b/src/Umbraco.Core/UdiTypeConverter.cs index c443b1817b..2a52a1e093 100644 --- a/src/Umbraco.Core/UdiTypeConverter.cs +++ b/src/Umbraco.Core/UdiTypeConverter.cs @@ -1,37 +1,36 @@ -using System; using System.ComponentModel; using System.Globalization; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// A custom type converter for UDI +/// +/// +/// Primarily this is used so that WebApi can auto-bind a string parameter to a UDI instance +/// +internal class UdiTypeConverter : TypeConverter { - /// - /// A custom type converter for UDI - /// - /// - /// Primarily this is used so that WebApi can auto-bind a string parameter to a UDI instance - /// - internal class UdiTypeConverter : TypeConverter + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + if (sourceType == typeof(string)) { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); + return true; } - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + return base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is string) { - if (value is string) + if (UdiParser.TryParse((string)value, out Udi? udi)) { - Udi? udi; - if (UdiParser.TryParse((string)value, out udi)) - { - return udi; - } + return udi; } - return base.ConvertFrom(context, culture, value); } + + return base.ConvertFrom(context, culture, value); } } diff --git a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs index 66ad608881..afd6183b54 100644 --- a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs +++ b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public class UmbracoApiControllerTypeCollection : BuilderCollectionBase { - public class UmbracoApiControllerTypeCollection : BuilderCollectionBase + public UmbracoApiControllerTypeCollection(Func> items) + : base(items) { - public UmbracoApiControllerTypeCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/UmbracoContextReference.cs b/src/Umbraco.Core/UmbracoContextReference.cs index 89959c3b32..d17012e0f9 100644 --- a/src/Umbraco.Core/UmbracoContextReference.cs +++ b/src/Umbraco.Core/UmbracoContextReference.cs @@ -1,61 +1,61 @@ -using System; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Represents a reference to an instance. +/// +/// +/// +/// A reference points to an and it may own it (when it +/// is a root reference) or just reference it. A reference must be disposed after it has +/// been used. Disposing does nothing if the reference is not a root reference. Otherwise, +/// it disposes the and clears the +/// . +/// +/// +public class UmbracoContextReference : IDisposable { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private bool _disposedValue; + /// - /// Represents a reference to an instance. + /// Initializes a new instance of the class. /// - /// - /// A reference points to an and it may own it (when it - /// is a root reference) or just reference it. A reference must be disposed after it has - /// been used. Disposing does nothing if the reference is not a root reference. Otherwise, - /// it disposes the and clears the - /// . - /// - public class UmbracoContextReference : IDisposable + public UmbracoContextReference(IUmbracoContext umbracoContext, bool isRoot, IUmbracoContextAccessor umbracoContextAccessor) { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private bool _disposedValue; + IsRoot = isRoot; - /// - /// Initializes a new instance of the class. - /// - public UmbracoContextReference(IUmbracoContext umbracoContext, bool isRoot, IUmbracoContextAccessor umbracoContextAccessor) + UmbracoContext = umbracoContext; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Gets the . + /// + public IUmbracoContext UmbracoContext { get; } + + /// + /// Gets a value indicating whether the reference is a root reference. + /// + public bool IsRoot { get; } + + public void Dispose() => Dispose(true); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - IsRoot = isRoot; - - UmbracoContext = umbracoContext; - _umbracoContextAccessor = umbracoContextAccessor; - } - - /// - /// Gets the . - /// - public IUmbracoContext UmbracoContext { get; } - - /// - /// Gets a value indicating whether the reference is a root reference. - /// - public bool IsRoot { get; } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) + if (disposing) { - if (disposing) + if (IsRoot) { - if (IsRoot) - { - UmbracoContext.Dispose(); - _umbracoContextAccessor.Clear(); - } + UmbracoContext.Dispose(); + _umbracoContextAccessor.Clear(); } - - _disposedValue = true; } - } - public void Dispose() => Dispose(disposing: true); + _disposedValue = true; + } } } diff --git a/src/Umbraco.Core/UnknownTypeUdi.cs b/src/Umbraco.Core/UnknownTypeUdi.cs index 4131eae053..3c38418f0e 100644 --- a/src/Umbraco.Core/UnknownTypeUdi.cs +++ b/src/Umbraco.Core/UnknownTypeUdi.cs @@ -1,16 +1,13 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public class UnknownTypeUdi : Udi { - public class UnknownTypeUdi : Udi + public static readonly UnknownTypeUdi Instance = new(); + + private UnknownTypeUdi() + : base("unknown", "umb://unknown/") { - private UnknownTypeUdi() - : base("unknown", "umb://unknown/") - { } - - public static readonly UnknownTypeUdi Instance = new UnknownTypeUdi(); - - public override bool IsRoot - { - get { return false; } - } } + + public override bool IsRoot => false; } diff --git a/src/Umbraco.Core/UpgradeResult.cs b/src/Umbraco.Core/UpgradeResult.cs index 25431a5983..7f27e503fe 100644 --- a/src/Umbraco.Core/UpgradeResult.cs +++ b/src/Umbraco.Core/UpgradeResult.cs @@ -1,16 +1,17 @@ -namespace Umbraco.Cms.Core -{ - public class UpgradeResult - { - public string UpgradeType { get; } - public string Comment { get; } - public string UpgradeUrl { get; } +namespace Umbraco.Cms.Core; - public UpgradeResult(string upgradeType, string comment, string upgradeUrl) - { - UpgradeType = upgradeType; - Comment = comment; - UpgradeUrl = upgradeUrl; - } +public class UpgradeResult +{ + public UpgradeResult(string upgradeType, string comment, string upgradeUrl) + { + UpgradeType = upgradeType; + Comment = comment; + UpgradeUrl = upgradeUrl; } + + public string UpgradeType { get; } + + public string Comment { get; } + + public string UpgradeUrl { get; } } diff --git a/src/Umbraco.Core/UriUtilityCore.cs b/src/Umbraco.Core/UriUtilityCore.cs index 68b6234a0f..3599a7c16a 100644 --- a/src/Umbraco.Core/UriUtilityCore.cs +++ b/src/Umbraco.Core/UriUtilityCore.cs @@ -1,59 +1,51 @@ -using System; -using Umbraco.Extensions; +using Umbraco.Extensions; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +public static class UriUtilityCore { - public static class UriUtilityCore + #region Uri string utilities + + public static bool HasScheme(string uri) => uri.IndexOf("://") > 0; + + public static string StartWithScheme(string uri) => StartWithScheme(uri, null); + + public static string StartWithScheme(string uri, string? scheme) => + HasScheme(uri) ? uri : string.Format("{0}://{1}", scheme ?? Uri.UriSchemeHttp, uri); + + public static string EndPathWithSlash(string uri) { + var pos1 = Math.Max(0, uri.IndexOf('?')); + var pos2 = Math.Max(0, uri.IndexOf('#')); + var pos = Math.Min(pos1, pos2); - #region Uri string utilities + var path = pos > 0 ? uri.Substring(0, pos) : uri; + path = path.EnsureEndsWith('/'); - public static bool HasScheme(string uri) + if (pos > 0) { - return uri.IndexOf("://") > 0; + path += uri.Substring(pos); } - public static string StartWithScheme(string uri) - { - return StartWithScheme(uri, null); - } - - public static string StartWithScheme(string uri, string? scheme) - { - return HasScheme(uri) ? uri : String.Format("{0}://{1}", scheme ?? Uri.UriSchemeHttp, uri); - } - - public static string EndPathWithSlash(string uri) - { - var pos1 = Math.Max(0, uri.IndexOf('?')); - var pos2 = Math.Max(0, uri.IndexOf('#')); - var pos = Math.Min(pos1, pos2); - - var path = pos > 0 ? uri.Substring(0, pos) : uri; - path = path.EnsureEndsWith('/'); - - if (pos > 0) - path += uri.Substring(pos); - - return path; - } - - public static string TrimPathEndSlash(string uri) - { - var pos1 = Math.Max(0, uri.IndexOf('?')); - var pos2 = Math.Max(0, uri.IndexOf('#')); - var pos = Math.Min(pos1, pos2); - - var path = pos > 0 ? uri.Substring(0, pos) : uri; - path = path.TrimEnd(Constants.CharArrays.ForwardSlash); - - if (pos > 0) - path += uri.Substring(pos); - - return path; - } - - #endregion - + return path; } + + public static string TrimPathEndSlash(string uri) + { + var pos1 = Math.Max(0, uri.IndexOf('?')); + var pos2 = Math.Max(0, uri.IndexOf('#')); + var pos = Math.Min(pos1, pos2); + + var path = pos > 0 ? uri[..pos] : uri; + path = path.TrimEnd(Constants.CharArrays.ForwardSlash); + + if (pos > 0) + { + path += uri.Substring(pos); + } + + return path; + } + + #endregion } diff --git a/src/Umbraco.Core/Web/CookieManagerExtensions.cs b/src/Umbraco.Core/Web/CookieManagerExtensions.cs index 75014000bb..2e399ac8c1 100644 --- a/src/Umbraco.Core/Web/CookieManagerExtensions.cs +++ b/src/Umbraco.Core/Web/CookieManagerExtensions.cs @@ -1,18 +1,13 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core; using Umbraco.Cms.Core.Web; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class CookieManagerExtensions { - public static class CookieManagerExtensions - { - public static string? GetPreviewCookieValue(this ICookieManager cookieManager) - { - return cookieManager.GetCookieValue(Constants.Web.PreviewCookieName); - } - - } - + public static string? GetPreviewCookieValue(this ICookieManager cookieManager) => + cookieManager.GetCookieValue(Constants.Web.PreviewCookieName); } diff --git a/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs b/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs index 94710429f0..509a746b30 100644 --- a/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs +++ b/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs @@ -1,39 +1,39 @@ using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +/// +/// Implements a hybrid . +/// +public class HybridUmbracoContextAccessor : HybridAccessorBase, IUmbracoContextAccessor { /// - /// Implements a hybrid . + /// Initializes a new instance of the class. /// - public class HybridUmbracoContextAccessor : HybridAccessorBase, IUmbracoContextAccessor + public HybridUmbracoContextAccessor(IRequestCache requestCache) + : base(requestCache) { - /// - /// Initializes a new instance of the class. - /// - public HybridUmbracoContextAccessor(IRequestCache requestCache) - : base(requestCache) - { } - - /// - /// Tries to get the object. - /// - public bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext) - { - umbracoContext = Value; - - return umbracoContext is not null; - } - - /// - /// Clears the current object. - /// - public void Clear() => Value = null; - - /// - /// Sets the object. - /// - /// - public void Set(IUmbracoContext umbracoContext) => Value = umbracoContext; } + + /// + /// Tries to get the object. + /// + public bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext) + { + umbracoContext = Value; + + return umbracoContext is not null; + } + + /// + /// Clears the current object. + /// + public void Clear() => Value = null; + + /// + /// Sets the object. + /// + /// + public void Set(IUmbracoContext umbracoContext) => Value = umbracoContext; } diff --git a/src/Umbraco.Core/Web/ICookieManager.cs b/src/Umbraco.Core/Web/ICookieManager.cs index 730b78a705..2815675f5d 100644 --- a/src/Umbraco.Core/Web/ICookieManager.cs +++ b/src/Umbraco.Core/Web/ICookieManager.cs @@ -1,12 +1,12 @@ -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +public interface ICookieManager { + void ExpireCookie(string cookieName); - public interface ICookieManager - { - void ExpireCookie(string cookieName); - string? GetCookieValue(string cookieName); - void SetCookieValue(string cookieName, string value); - bool HasCookie(string cookieName); - } + string? GetCookieValue(string cookieName); + void SetCookieValue(string cookieName, string value); + + bool HasCookie(string cookieName); } diff --git a/src/Umbraco.Core/Web/IRequestAccessor.cs b/src/Umbraco.Core/Web/IRequestAccessor.cs index 9fb4e99d5c..a72ec5bc72 100644 --- a/src/Umbraco.Core/Web/IRequestAccessor.cs +++ b/src/Umbraco.Core/Web/IRequestAccessor.cs @@ -1,22 +1,19 @@ -using System; +namespace Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Core.Web +public interface IRequestAccessor { - public interface IRequestAccessor - { - /// - /// Returns the request/form/querystring value for the given name - /// - string GetRequestValue(string name); + /// + /// Returns the request/form/querystring value for the given name + /// + string GetRequestValue(string name); - /// - /// Returns the query string value for the given name - /// - string GetQueryStringValue(string name); + /// + /// Returns the query string value for the given name + /// + string GetQueryStringValue(string name); - /// - /// Returns the current request uri - /// - Uri? GetRequestUrl(); - } + /// + /// Returns the current request uri + /// + Uri? GetRequestUrl(); } diff --git a/src/Umbraco.Core/Web/ISessionManager.cs b/src/Umbraco.Core/Web/ISessionManager.cs index 3ba691e222..a37bebcfa7 100644 --- a/src/Umbraco.Core/Web/ISessionManager.cs +++ b/src/Umbraco.Core/Web/ISessionManager.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +public interface ISessionManager { - public interface ISessionManager - { - string? GetSessionValue(string key); + string? GetSessionValue(string key); - void SetSessionValue(string key, string value); + void SetSessionValue(string key, string value); - void ClearSessionValue(string key); - } + void ClearSessionValue(string key); } diff --git a/src/Umbraco.Core/Web/IUmbracoContext.cs b/src/Umbraco.Core/Web/IUmbracoContext.cs index 7cfa3876c0..17ffc515a2 100644 --- a/src/Umbraco.Core/Web/IUmbracoContext.cs +++ b/src/Umbraco.Core/Web/IUmbracoContext.cs @@ -1,74 +1,72 @@ -using System; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +public interface IUmbracoContext : IDisposable { - public interface IUmbracoContext : IDisposable - { - /// - /// Gets the DateTime this instance was created. - /// - /// - /// Used internally for performance calculations, the ObjectCreated DateTime is set as soon as this - /// object is instantiated which in the web site is created during the BeginRequest phase. - /// We can then determine complete rendering time from that. - /// - DateTime ObjectCreated { get; } + /// + /// Gets the DateTime this instance was created. + /// + /// + /// Used internally for performance calculations, the ObjectCreated DateTime is set as soon as this + /// object is instantiated which in the web site is created during the BeginRequest phase. + /// We can then determine complete rendering time from that. + /// + DateTime ObjectCreated { get; } - /// - /// Gets the uri that is handled by ASP.NET after server-side rewriting took place. - /// - Uri OriginalRequestUrl { get; } + /// + /// Gets the uri that is handled by ASP.NET after server-side rewriting took place. + /// + Uri OriginalRequestUrl { get; } - /// - /// Gets the cleaned up url that is handled by Umbraco. - /// - /// That is, lowercase, no trailing slash after path, no .aspx... - Uri CleanedUmbracoUrl { get; } + /// + /// Gets the cleaned up url that is handled by Umbraco. + /// + /// That is, lowercase, no trailing slash after path, no .aspx... + Uri CleanedUmbracoUrl { get; } - /// - /// Gets the published snapshot. - /// - IPublishedSnapshot PublishedSnapshot { get; } + /// + /// Gets the published snapshot. + /// + IPublishedSnapshot PublishedSnapshot { get; } - /// - /// Gets the published content cache. - /// - IPublishedContentCache? Content { get; } + /// + /// Gets the published content cache. + /// + IPublishedContentCache? Content { get; } - /// - /// Gets the published media cache. - /// - IPublishedMediaCache? Media { get; } + /// + /// Gets the published media cache. + /// + IPublishedMediaCache? Media { get; } - /// - /// Gets the domains cache. - /// - IDomainCache? Domains { get; } + /// + /// Gets the domains cache. + /// + IDomainCache? Domains { get; } - /// - /// Gets or sets the PublishedRequest object - /// - //// TODO: Can we refactor this so it's not a settable thing required for routing? - //// The only nicer way would be to have a RouteRequest method directly on IUmbracoContext but that means adding another dep to the ctx/factory. - IPublishedRequest? PublishedRequest { get; set; } + /// + /// Gets or sets the PublishedRequest object + /// + //// TODO: Can we refactor this so it's not a settable thing required for routing? + //// The only nicer way would be to have a RouteRequest method directly on IUmbracoContext but that means adding another dep to the ctx/factory. + IPublishedRequest? PublishedRequest { get; set; } - /// - /// Gets a value indicating whether the request has debugging enabled - /// - /// true if this instance is debug; otherwise, false. - bool IsDebug { get; } + /// + /// Gets a value indicating whether the request has debugging enabled + /// + /// true if this instance is debug; otherwise, false. + bool IsDebug { get; } - /// - /// Gets a value indicating whether the current user is in a preview mode and browsing the site (ie. not in the admin UI) - /// - bool InPreviewMode { get; } + /// + /// Gets a value indicating whether the current user is in a preview mode and browsing the site (ie. not in the admin UI) + /// + bool InPreviewMode { get; } - /// - /// Forces the context into preview - /// - /// A instance to be disposed to exit the preview context - IDisposable ForcedPreview(bool preview); - } + /// + /// Forces the context into preview + /// + /// A instance to be disposed to exit the preview context + IDisposable ForcedPreview(bool preview); } diff --git a/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs b/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs index d8e6793f89..370412b281 100644 --- a/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs +++ b/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs @@ -1,16 +1,17 @@ using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +/// +/// Provides access to a TryGetUmbracoContext bool method that will return true if the "current" is not null. +/// Provides a Clear() method that will clear the current object. +/// Provides a Set() method that til set the current object. +/// +public interface IUmbracoContextAccessor { - /// - /// Provides access to a TryGetUmbracoContext bool method that will return true if the "current" is not null. - /// Provides a Clear() method that will clear the current object. - /// Provides a Set() method that til set the current object. - /// - public interface IUmbracoContextAccessor - { - bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext); - void Clear(); - void Set(IUmbracoContext umbracoContext); - } + bool TryGetUmbracoContext([MaybeNullWhen(false)] out IUmbracoContext umbracoContext); + + void Clear(); + + void Set(IUmbracoContext umbracoContext); } diff --git a/src/Umbraco.Core/Web/IUmbracoContextFactory.cs b/src/Umbraco.Core/Web/IUmbracoContextFactory.cs index 68ebcf8b2b..d8d5475841 100644 --- a/src/Umbraco.Core/Web/IUmbracoContextFactory.cs +++ b/src/Umbraco.Core/Web/IUmbracoContextFactory.cs @@ -1,30 +1,27 @@ - -namespace Umbraco.Cms.Core.Web +namespace Umbraco.Cms.Core.Web; + +/// +/// Creates and manages instances. +/// +public interface IUmbracoContextFactory { /// - /// Creates and manages instances. + /// Ensures that a current exists. /// - public interface IUmbracoContextFactory - { - /// - /// Ensures that a current exists. - /// - /// - /// If an is already registered in the - /// , returns a non-root reference to it. - /// Otherwise, create a new instance, registers it, and return a root reference - /// to it. - /// If is null, the factory tries to use - /// if it exists. Otherwise, it uses a dummy - /// . - /// - /// - /// using (var contextReference = contextFactory.EnsureUmbracoContext()) - /// { - /// var umbracoContext = contextReference.UmbracoContext; - /// // use umbracoContext... - /// } - /// - UmbracoContextReference EnsureUmbracoContext(); - } + /// + /// + /// If an is already registered in the + /// , returns a non-root reference to it. + /// Otherwise, create a new instance, registers it, and return a root reference + /// to it. + /// + /// + /// + /// using (var contextReference = contextFactory.EnsureUmbracoContext()) + /// { + /// var umbracoContext = contextReference.UmbracoContext; + /// // use umbracoContext... + /// } + /// + UmbracoContextReference EnsureUmbracoContext(); } diff --git a/src/Umbraco.Core/Web/Mvc/PluginControllerMetadata.cs b/src/Umbraco.Core/Web/Mvc/PluginControllerMetadata.cs index efc162a9a3..5f484c8fe0 100644 --- a/src/Umbraco.Core/Web/Mvc/PluginControllerMetadata.cs +++ b/src/Umbraco.Core/Web/Mvc/PluginControllerMetadata.cs @@ -1,21 +1,21 @@ -using System; +namespace Umbraco.Cms.Core.Web.Mvc; -namespace Umbraco.Cms.Core.Web.Mvc +/// +/// Represents some metadata about the controller +/// +public class PluginControllerMetadata { - /// - /// Represents some metadata about the controller - /// - public class PluginControllerMetadata - { - public Type ControllerType { get; set; } = null!; - public string? ControllerName { get; set; } - public string? ControllerNamespace { get; set; } - public string? AreaName { get; set; } + public Type ControllerType { get; set; } = null!; - /// - /// This is determined by another attribute [IsBackOffice] which slightly modifies the route path - /// allowing us to determine if it is indeed a back office request or not - /// - public bool IsBackOffice { get; set; } - } + public string? ControllerName { get; set; } + + public string? ControllerNamespace { get; set; } + + public string? AreaName { get; set; } + + /// + /// This is determined by another attribute [IsBackOffice] which slightly modifies the route path + /// allowing us to determine if it is indeed a back office request or not + /// + public bool IsBackOffice { get; set; } } diff --git a/src/Umbraco.Core/WebAssets/AssetFile.cs b/src/Umbraco.Core/WebAssets/AssetFile.cs index c10a423a99..a0ad298302 100644 --- a/src/Umbraco.Core/WebAssets/AssetFile.cs +++ b/src/Umbraco.Core/WebAssets/AssetFile.cs @@ -1,23 +1,20 @@ -using System.Diagnostics; +using System.Diagnostics; -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +/// +/// Represents a dependency file +/// +[DebuggerDisplay("Type: {DependencyType}, File: {FilePath}")] +public class AssetFile : IAssetFile { - /// - /// Represents a dependency file - /// - [DebuggerDisplay("Type: {DependencyType}, File: {FilePath}")] - public class AssetFile : IAssetFile - { - #region IAssetFile Members + public AssetFile(AssetType type) => DependencyType = type; - public string? FilePath { get; set; } - public AssetType DependencyType { get; } + #region IAssetFile Members - #endregion + public string? FilePath { get; set; } - public AssetFile(AssetType type) - { - DependencyType = type; - } - } + public AssetType DependencyType { get; } + + #endregion } diff --git a/src/Umbraco.Core/WebAssets/AssetType.cs b/src/Umbraco.Core/WebAssets/AssetType.cs index f40a592588..e04caa80a2 100644 --- a/src/Umbraco.Core/WebAssets/AssetType.cs +++ b/src/Umbraco.Core/WebAssets/AssetType.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +public enum AssetType { - public enum AssetType - { - Javascript, - Css - } + Javascript, + Css, } diff --git a/src/Umbraco.Core/WebAssets/BundlingOptions.cs b/src/Umbraco.Core/WebAssets/BundlingOptions.cs index 64b9e72e17..99236494f3 100644 --- a/src/Umbraco.Core/WebAssets/BundlingOptions.cs +++ b/src/Umbraco.Core/WebAssets/BundlingOptions.cs @@ -1,44 +1,46 @@ -using System; +namespace Umbraco.Cms.Core.WebAssets; -namespace Umbraco.Cms.Core.WebAssets +public struct BundlingOptions : IEquatable { - public struct BundlingOptions : IEquatable + public BundlingOptions(bool optimizeOutput = true, bool enabledCompositeFiles = true) { - public static BundlingOptions OptimizedAndComposite => new BundlingOptions(true, true); - public static BundlingOptions OptimizedNotComposite => new BundlingOptions(true, false); - public static BundlingOptions NotOptimizedNotComposite => new BundlingOptions(false, false); - public static BundlingOptions NotOptimizedAndComposite => new BundlingOptions(false, true); - - public BundlingOptions(bool optimizeOutput = true, bool enabledCompositeFiles = true) - { - OptimizeOutput = optimizeOutput; - EnabledCompositeFiles = enabledCompositeFiles; - } - - /// - /// If true, the files in the bundle will be minified - /// - public bool OptimizeOutput { get; } - - /// - /// If true, the files in the bundle will be combined, if false the files - /// will be served as individual files. - /// - public bool EnabledCompositeFiles { get; } - - public override bool Equals(object? obj) => obj is BundlingOptions options && Equals(options); - public bool Equals(BundlingOptions other) => OptimizeOutput == other.OptimizeOutput && EnabledCompositeFiles == other.EnabledCompositeFiles; - - public override int GetHashCode() - { - int hashCode = 2130304063; - hashCode = hashCode * -1521134295 + OptimizeOutput.GetHashCode(); - hashCode = hashCode * -1521134295 + EnabledCompositeFiles.GetHashCode(); - return hashCode; - } - - public static bool operator ==(BundlingOptions left, BundlingOptions right) => left.Equals(right); - - public static bool operator !=(BundlingOptions left, BundlingOptions right) => !(left == right); + OptimizeOutput = optimizeOutput; + EnabledCompositeFiles = enabledCompositeFiles; } + + public static BundlingOptions OptimizedAndComposite => new(true); + + public static BundlingOptions OptimizedNotComposite => new(true, false); + + public static BundlingOptions NotOptimizedNotComposite => new(false, false); + + public static BundlingOptions NotOptimizedAndComposite => new(false); + + /// + /// If true, the files in the bundle will be minified + /// + public bool OptimizeOutput { get; } + + /// + /// If true, the files in the bundle will be combined, if false the files + /// will be served as individual files. + /// + public bool EnabledCompositeFiles { get; } + + public static bool operator ==(BundlingOptions left, BundlingOptions right) => left.Equals(right); + + public override bool Equals(object? obj) => obj is BundlingOptions options && Equals(options); + + public bool Equals(BundlingOptions other) => OptimizeOutput == other.OptimizeOutput && + EnabledCompositeFiles == other.EnabledCompositeFiles; + + public override int GetHashCode() + { + var hashCode = 2130304063; + hashCode = (hashCode * -1521134295) + OptimizeOutput.GetHashCode(); + hashCode = (hashCode * -1521134295) + EnabledCompositeFiles.GetHashCode(); + return hashCode; + } + + public static bool operator !=(BundlingOptions left, BundlingOptions right) => !(left == right); } diff --git a/src/Umbraco.Core/WebAssets/CssFile.cs b/src/Umbraco.Core/WebAssets/CssFile.cs index 101ff22763..9ba30c83de 100644 --- a/src/Umbraco.Core/WebAssets/CssFile.cs +++ b/src/Umbraco.Core/WebAssets/CssFile.cs @@ -1,14 +1,11 @@ -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +/// +/// Represents a CSS asset file +/// +public class CssFile : AssetFile { - /// - /// Represents a CSS asset file - /// - public class CssFile : AssetFile - { - public CssFile(string filePath) - : base(AssetType.Css) - { - FilePath = filePath; - } - } + public CssFile(string filePath) + : base(AssetType.Css) => + FilePath = filePath; } diff --git a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs index 2595afe40e..523b186c9a 100644 --- a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs +++ b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +public class CustomBackOfficeAssetsCollection : BuilderCollectionBase { - public class CustomBackOfficeAssetsCollection : BuilderCollectionBase + public CustomBackOfficeAssetsCollection(Func> items) + : base(items) { - public CustomBackOfficeAssetsCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollectionBuilder.cs b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollectionBuilder.cs index df84bf013d..bdfebf128a 100644 --- a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollectionBuilder.cs +++ b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollectionBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +public class CustomBackOfficeAssetsCollectionBuilder : OrderedCollectionBuilderBase { - public class CustomBackOfficeAssetsCollectionBuilder : OrderedCollectionBuilderBase - { - protected override CustomBackOfficeAssetsCollectionBuilder This => this; - } + protected override CustomBackOfficeAssetsCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/WebAssets/IAssetFile.cs b/src/Umbraco.Core/WebAssets/IAssetFile.cs index dd66afe4a7..f3e5516f45 100644 --- a/src/Umbraco.Core/WebAssets/IAssetFile.cs +++ b/src/Umbraco.Core/WebAssets/IAssetFile.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +public interface IAssetFile { - public interface IAssetFile - { - string? FilePath { get; set; } - AssetType DependencyType { get; } - } + string? FilePath { get; set; } + + AssetType DependencyType { get; } } diff --git a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs index baf9549562..c6116e122f 100644 --- a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs +++ b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs @@ -1,105 +1,101 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +namespace Umbraco.Cms.Core.WebAssets; -namespace Umbraco.Cms.Core.WebAssets +/// +/// Used for bundling and minifying web assets at runtime +/// +public interface IRuntimeMinifier { /// - /// Used for bundling and minifying web assets at runtime + /// Returns the cache buster value /// - public interface IRuntimeMinifier - { - /// - /// Returns the cache buster value - /// - string CacheBuster { get; } + string CacheBuster { get; } - /// - /// Creates a css bundle - /// - /// - /// - /// - /// All files must be absolute paths, relative paths will throw - /// - /// - /// Thrown if any of the paths specified are not absolute - /// - void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths); + /// + /// Creates a css bundle + /// + /// + /// + /// + /// All files must be absolute paths, relative paths will throw + /// + /// + /// Thrown if any of the paths specified are not absolute + /// + void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths); - /// - /// Renders the html link tag for the bundle - /// - /// - /// - /// An html encoded string - /// - Task RenderCssHereAsync(string bundleName); + /// + /// Renders the html link tag for the bundle + /// + /// + /// + /// An html encoded string + /// + Task RenderCssHereAsync(string bundleName); - /// - /// Creates a JS bundle - /// - /// - /// - /// - /// - /// All files must be absolute paths, relative paths will throw - /// - /// - /// Thrown if any of the paths specified are not absolute - /// - void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths); + /// + /// Creates a JS bundle + /// + /// + /// + /// + /// + /// All files must be absolute paths, relative paths will throw + /// + /// + /// Thrown if any of the paths specified are not absolute + /// + void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[]? filePaths); - /// - /// Renders the html script tag for the bundle - /// - /// - /// - /// An html encoded string - /// - Task RenderJsHereAsync(string bundleName); + /// + /// Renders the html script tag for the bundle + /// + /// + /// + /// An html encoded string + /// + Task RenderJsHereAsync(string bundleName); - /// - /// Returns the asset paths for the JS bundle name - /// - /// - /// - /// If debug mode is enabled this will return all asset paths (not bundled), else it will return a bundle URL - /// - Task> GetJsAssetPathsAsync(string bundleName); + /// + /// Returns the asset paths for the JS bundle name + /// + /// + /// + /// If debug mode is enabled this will return all asset paths (not bundled), else it will return a bundle URL + /// + Task> GetJsAssetPathsAsync(string bundleName); - /// - /// Returns the asset paths for the css bundle name - /// - /// - /// - /// If debug mode is enabled this will return all asset paths (not bundled), else it will return a bundle URL - /// - Task> GetCssAssetPathsAsync(string bundleName); + /// + /// Returns the asset paths for the css bundle name + /// + /// + /// + /// If debug mode is enabled this will return all asset paths (not bundled), else it will return a bundle URL + /// + Task> GetCssAssetPathsAsync(string bundleName); - /// - /// Minify the file content, of a given type - /// - /// - /// - /// - Task MinifyAsync(string? fileContent, AssetType assetType); + /// + /// Minify the file content, of a given type + /// + /// + /// + /// + Task MinifyAsync(string? fileContent, AssetType assetType); - /// - /// Ensures that all runtime minifications are refreshed on next request. E.g. Clearing cache. - /// - /// - /// - /// No longer necessary, invalidation occurs automatically if any of the following occur. - /// - /// - /// Your sites assembly information version changes. - /// Umbraco.Cms.Core assembly information version changes. - /// RuntimeMinificationSettings Version string changes. - /// - /// for further details. - /// - [Obsolete("Invalidation is handled automatically. Scheduled for removal V11.")] - void Reset(); - } + /// + /// Ensures that all runtime minifications are refreshed on next request. E.g. Clearing cache. + /// + /// + /// + /// No longer necessary, invalidation occurs automatically if any of the following occur. + /// + /// + /// Your sites assembly information version changes. + /// Umbraco.Cms.Core assembly information version changes. + /// RuntimeMinificationSettings Version string changes. + /// + /// for further + /// details. + /// + [Obsolete("Invalidation is handled automatically. Scheduled for removal V11.")] + void Reset(); } diff --git a/src/Umbraco.Core/WebAssets/JavascriptFile.cs b/src/Umbraco.Core/WebAssets/JavascriptFile.cs index 2dccbf2a07..e7f4ea239f 100644 --- a/src/Umbraco.Core/WebAssets/JavascriptFile.cs +++ b/src/Umbraco.Core/WebAssets/JavascriptFile.cs @@ -1,14 +1,11 @@ -namespace Umbraco.Cms.Core.WebAssets +namespace Umbraco.Cms.Core.WebAssets; + +/// +/// Represents a JS asset file +/// +public class JavaScriptFile : AssetFile { - /// - /// Represents a JS asset file - /// - public class JavaScriptFile : AssetFile - { - public JavaScriptFile(string filePath) - : base(AssetType.Javascript) - { - FilePath = filePath; - } - } + public JavaScriptFile(string filePath) + : base(AssetType.Javascript) => + FilePath = filePath; } diff --git a/src/Umbraco.Core/Xml/DynamicContext.cs b/src/Umbraco.Core/Xml/DynamicContext.cs index 7547b7cc31..fd86866348 100644 --- a/src/Umbraco.Core/Xml/DynamicContext.cs +++ b/src/Umbraco.Core/Xml/DynamicContext.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Xml; +using System.Xml; using System.Xml.XPath; using System.Xml.Xsl; @@ -66,16 +64,25 @@ namespace Umbraco.Cms.Core.Xml object xml = table.Add(XmlNamespaces.Xml); object xmlns = table.Add(XmlNamespaces.XmlNs); - if (context == null) return; + if (context == null) + { + return; + } foreach (string prefix in context) { var uri = context.LookupNamespace(prefix); // Use fast object reference comparison to omit forbidden namespace declarations. if (Equals(uri, xml) || Equals(uri, xmlns)) + { continue; + } + if (uri == null) + { continue; + } + base.AddNamespace(prefix, uri); } } @@ -87,10 +94,8 @@ namespace Umbraco.Cms.Core.Xml /// /// Implementation equal to . /// - public override int CompareDocument(string baseUri, string nextbaseUri) - { - return String.Compare(baseUri, nextbaseUri, false, System.Globalization.CultureInfo.InvariantCulture); - } + public override int CompareDocument(string baseUri, string nextbaseUri) => + String.Compare(baseUri, nextbaseUri, false, System.Globalization.CultureInfo.InvariantCulture); /// /// Same as . @@ -187,7 +192,11 @@ namespace Umbraco.Cms.Core.Xml /// The is null. public void AddVariable(string name, object value) { - if (value == null) throw new ArgumentNullException("value"); + if (value == null) + { + throw new ArgumentNullException("value"); + } + _variables[name] = new DynamicVariable(name, value); } @@ -203,7 +212,7 @@ namespace Umbraco.Cms.Core.Xml { IXsltContextVariable var; _variables.TryGetValue(name, out var!); - return var!; + return var; } #endregion Variable Handling Code @@ -215,8 +224,8 @@ namespace Umbraco.Cms.Core.Xml /// internal class DynamicVariable : IXsltContextVariable { - readonly string _name; - readonly object _value; + private readonly string _name; + private readonly object _value; #region Public Members @@ -234,13 +243,21 @@ namespace Umbraco.Cms.Core.Xml _value = value; if (value is string) + { _type = XPathResultType.String; + } else if (value is bool) + { _type = XPathResultType.Boolean; + } else if (value is XPathNavigator) + { _type = XPathResultType.Navigator; + } else if (value is XPathNodeIterator) + { _type = XPathResultType.NodeSet; + } else { // Try to convert to double (native XPath numeric type) @@ -284,7 +301,7 @@ namespace Umbraco.Cms.Core.Xml get { return _type; } } - readonly XPathResultType _type; + private readonly XPathResultType _type; object IXsltContextVariable.Evaluate(XsltContext context) { diff --git a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs index 4285f9c97f..bb5c186ca6 100644 --- a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs +++ b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs @@ -1,122 +1,134 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Xml +namespace Umbraco.Cms.Core.Xml; + +/// +/// This is used to parse our customize Umbraco XPath expressions (i.e. that include special tokens like $site) into +/// a real XPath statement +/// +public class UmbracoXPathPathSyntaxParser { /// - /// This is used to parse our customize Umbraco XPath expressions (i.e. that include special tokens like $site) into - /// a real XPath statement + /// Parses custom umbraco xpath expression /// - public class UmbracoXPathPathSyntaxParser + /// The Xpath expression + /// + /// The current node id context of executing the query - null if there is no current node, in which case + /// some of the parameters like $current, $parent, $site will be disabled + /// + /// The callback to create the nodeId path, given a node Id + /// The callback to return whether a published node exists based on Id + /// + public static string ParseXPathQuery( + string xpathExpression, + int? nodeContextId, + Func?> getPath, + Func publishedContentExists) { - /// - /// Parses custom umbraco xpath expression - /// - /// The Xpath expression - /// - /// The current node id context of executing the query - null if there is no current node, in which case - /// some of the parameters like $current, $parent, $site will be disabled - /// - /// The callback to create the nodeId path, given a node Id - /// The callback to return whether a published node exists based on Id - /// - public static string ParseXPathQuery( - string xpathExpression, - int? nodeContextId, - Func?> getPath, - Func publishedContentExists) + // TODO: This should probably support some of the old syntax and token replacements, currently + // it does not, there is a ticket raised here about it: http://issues.umbraco.org/issue/U4-6364 + // previous tokens were: "$currentPage", "$ancestorOrSelf", "$parentPage" and I believe they were + // allowed 'inline', not just at the beginning... whether or not we want to support that is up + // for discussion. + if (xpathExpression == null) { + throw new ArgumentNullException(nameof(xpathExpression)); + } - // TODO: This should probably support some of the old syntax and token replacements, currently - // it does not, there is a ticket raised here about it: http://issues.umbraco.org/issue/U4-6364 - // previous tokens were: "$currentPage", "$ancestorOrSelf", "$parentPage" and I believe they were - // allowed 'inline', not just at the beginning... whether or not we want to support that is up - // for discussion. + if (string.IsNullOrWhiteSpace(xpathExpression)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(xpathExpression)); + } - if (xpathExpression == null) throw new ArgumentNullException(nameof(xpathExpression)); - if (string.IsNullOrWhiteSpace(xpathExpression)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(xpathExpression)); - if (getPath == null) throw new ArgumentNullException(nameof(getPath)); - if (publishedContentExists == null) throw new ArgumentNullException(nameof(publishedContentExists)); + if (getPath == null) + { + throw new ArgumentNullException(nameof(getPath)); + } - //no need to parse it - if (xpathExpression.StartsWith("$") == false) - return xpathExpression; - - //get nearest published item - Func?, int> getClosestPublishedAncestor = path => - { - if (path is not null) - { - foreach (var i in path) - { - int idAsInt; - if (int.TryParse(i, NumberStyles.Integer, CultureInfo.InvariantCulture, out idAsInt)) - { - var exists = publishedContentExists(int.Parse(i, CultureInfo.InvariantCulture)); - if (exists) - return idAsInt; - } - } - } - - return -1; - }; - - const string rootXpath = "id({0})"; - - //parseable items: - var vars = new Dictionary>(); - - //These parameters must have a node id context - if (nodeContextId.HasValue) - { - vars.Add("$current", q => - { - var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); - return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); - }); - - vars.Add("$parent", q => - { - //remove the first item in the array if its the current node - //this happens when current is published, but we are looking for its parent specifically - var path = getPath(nodeContextId.Value)?.ToArray(); - if (path?[0] == nodeContextId.ToString()) - { - path = path?.Skip(1).ToArray(); - } - - var closestPublishedAncestorId = getClosestPublishedAncestor(path); - return q.Replace("$parent", string.Format(rootXpath, closestPublishedAncestorId)); - }); - - - vars.Add("$site", q => - { - var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); - return q.Replace("$site", string.Format(rootXpath, closestPublishedAncestorId) + "/ancestor-or-self::*[@level = 1]"); - }); - } - - // TODO: This used to just replace $root with string.Empty BUT, that would never work - // the root is always "/root . Need to confirm with Per why this was string.Empty before! - vars.Add("$root", q => q.Replace("$root", "/root")); - - - foreach (var varible in vars) - { - if (xpathExpression.StartsWith(varible.Key)) - { - xpathExpression = varible.Value(xpathExpression); - break; - } - } + if (publishedContentExists == null) + { + throw new ArgumentNullException(nameof(publishedContentExists)); + } + // no need to parse it + if (xpathExpression.StartsWith("$") == false) + { return xpathExpression; } + // get nearest published item + Func?, int> getClosestPublishedAncestor = path => + { + if (path is not null) + { + foreach (var i in path) + { + if (int.TryParse(i, NumberStyles.Integer, CultureInfo.InvariantCulture, out int idAsInt)) + { + var exists = publishedContentExists(int.Parse(i, CultureInfo.InvariantCulture)); + if (exists) + { + return idAsInt; + } + } + } + } + + return -1; + }; + + const string rootXpath = "id({0})"; + + // parseable items: + var vars = new Dictionary>(); + + // These parameters must have a node id context + if (nodeContextId.HasValue) + { + vars.Add("$current", q => + { + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); + }); + + vars.Add("$parent", q => + { + // remove the first item in the array if its the current node + // this happens when current is published, but we are looking for its parent specifically + var path = getPath(nodeContextId.Value)?.ToArray(); + if (path?[0] == nodeContextId.ToString()) + { + path = path?.Skip(1).ToArray(); + } + + var closestPublishedAncestorId = getClosestPublishedAncestor(path); + return q.Replace("$parent", string.Format(rootXpath, closestPublishedAncestorId)); + }); + + vars.Add("$site", q => + { + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + return q.Replace( + "$site", + string.Format(rootXpath, closestPublishedAncestorId) + "/ancestor-or-self::*[@level = 1]"); + }); + } + + // TODO: This used to just replace $root with string.Empty BUT, that would never work + // the root is always "/root . Need to confirm with Per why this was string.Empty before! + vars.Add("$root", q => q.Replace("$root", "/root")); + + foreach (KeyValuePair> varible in vars) + { + if (xpathExpression.StartsWith(varible.Key)) + { + xpathExpression = varible.Value(xpathExpression); + break; + } + } + + return xpathExpression; } } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContent.cs b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs index c1a4e6c3e4..b9359b4fef 100644 --- a/src/Umbraco.Core/Xml/XPath/INavigableContent.cs +++ b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs @@ -1,59 +1,62 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Xml.XPath; -namespace Umbraco.Cms.Core.Xml.XPath +/// +/// Represents a content that can be navigated via XPath. +/// +public interface INavigableContent { /// - /// Represents a content that can be navigated via XPath. + /// Gets the unique identifier of the navigable content. /// - public interface INavigableContent - { - /// - /// Gets the unique identifier of the navigable content. - /// - /// The root node identifier should be -1. - int Id { get; } + /// The root node identifier should be -1. + int Id { get; } - /// - /// Gets the unique identifier of parent of the navigable content. - /// - /// The top-level content parent identifiers should be -1 ie the identifier - /// of the root node, whose parent identifier should in turn be -1. - int ParentId { get; } + /// + /// Gets the unique identifier of parent of the navigable content. + /// + /// + /// The top-level content parent identifiers should be -1 ie the identifier + /// of the root node, whose parent identifier should in turn be -1. + /// + int ParentId { get; } - /// - /// Gets the type of the navigable content. - /// - INavigableContentType Type { get; } + /// + /// Gets the type of the navigable content. + /// + INavigableContentType Type { get; } - /// - /// Gets the unique identifiers of the children of the navigable content. - /// - IList? ChildIds { get; } + /// + /// Gets the unique identifiers of the children of the navigable content. + /// + IList? ChildIds { get; } - /// - /// Gets the value of a field of the navigable content for XPath navigation use. - /// - /// The field index. - /// The value of the field for XPath navigation use. - /// - /// Fields are attributes or elements depending on their relative index value compared - /// to source.LastAttributeIndex. - /// For attributes, the value must be a string. - /// For elements, the value should an XPathNavigator instance if the field is xml - /// and has content (is not empty), null to indicate that the element is empty, or a string - /// which can be empty, whitespace... depending on what the data type wants to expose. - /// - object? Value(int index); + /// + /// Gets the value of a field of the navigable content for XPath navigation use. + /// + /// The field index. + /// The value of the field for XPath navigation use. + /// + /// + /// Fields are attributes or elements depending on their relative index value compared + /// to source.LastAttributeIndex. + /// + /// For attributes, the value must be a string. + /// + /// For elements, the value should an XPathNavigator instance if the field is xml + /// and has content (is not empty), null to indicate that the element is empty, or a string + /// which can be empty, whitespace... depending on what the data type wants to expose. + /// + /// + object? Value(int index); - // TODO: implement the following one + // TODO: implement the following one - ///// - ///// Gets the value of a field of the navigable content, for a specified language. - ///// - ///// The field index. - ///// The language key. - ///// The value of the field for the specified language. - ///// ... - //object Value(int index, string languageKey); - } + ///// + ///// Gets the value of a field of the navigable content, for a specified language. + ///// + ///// The field index. + ///// The language key. + ///// The value of the field for the specified language. + ///// ... + // object Value(int index, string languageKey); } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs index 2e214d5e9a..08a7c1a0f6 100644 --- a/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs +++ b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Core.Xml.XPath +namespace Umbraco.Cms.Core.Xml.XPath; + +/// +/// Represents the type of a content that can be navigated via XPath. +/// +public interface INavigableContentType { /// - /// Represents the type of a content that can be navigated via XPath. + /// Gets the name of the content type. /// - public interface INavigableContentType - { - /// - /// Gets the name of the content type. - /// - string? Name { get; } + string? Name { get; } - /// - /// Gets the field types of the content type. - /// - /// This includes the attributes and the properties. - INavigableFieldType[] FieldTypes { get; } - } + /// + /// Gets the field types of the content type. + /// + /// This includes the attributes and the properties. + INavigableFieldType[] FieldTypes { get; } } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs index 0b66cc0626..28fa46e84b 100644 --- a/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs +++ b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs @@ -1,23 +1,22 @@ -using System; +namespace Umbraco.Cms.Core.Xml.XPath; -namespace Umbraco.Cms.Core.Xml.XPath +/// +/// Represents the type of a field of a content that can be navigated via XPath. +/// +/// A field can be an attribute or a property. +public interface INavigableFieldType { /// - /// Represents the type of a field of a content that can be navigated via XPath. + /// Gets the name of the field type. /// - /// A field can be an attribute or a property. - public interface INavigableFieldType - { - /// - /// Gets the name of the field type. - /// - string Name { get; } + string Name { get; } - /// - /// Gets a method to convert the field value to a string. - /// - /// This is for built-in properties, ie attributes. User-defined properties have their - /// own way to convert their value for XPath. - Func? XmlStringConverter { get; } - } + /// + /// Gets a method to convert the field value to a string. + /// + /// + /// This is for built-in properties, ie attributes. User-defined properties have their + /// own way to convert their value for XPath. + /// + Func? XmlStringConverter { get; } } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableSource.cs b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs index 76b43b618c..1f8500725b 100644 --- a/src/Umbraco.Core/Xml/XPath/INavigableSource.cs +++ b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs @@ -1,29 +1,30 @@ -namespace Umbraco.Cms.Core.Xml.XPath +namespace Umbraco.Cms.Core.Xml.XPath; + +/// +/// Represents a source of content that can be navigated via XPath. +/// +public interface INavigableSource { /// - /// Represents a source of content that can be navigated via XPath. + /// Gets the index of the last attribute in the fields collections. /// - public interface INavigableSource - { - /// - /// Gets a content identified by its unique identifier. - /// - /// The unique identifier. - /// The content identified by the unique identifier, or null. - /// When id is -1 (root content) implementations should return null. - INavigableContent? Get(int id); + int LastAttributeIndex { get; } - /// - /// Gets the index of the last attribute in the fields collections. - /// - int LastAttributeIndex { get; } + /// + /// Gets the content at the root of the source. + /// + /// + /// That content should have unique identifier -1 and should not be gettable, + /// ie Get(-1) should return null. Its ParentId should be -1. It should provide + /// values for the attribute fields. + /// + INavigableContent Root { get; } - /// - /// Gets the content at the root of the source. - /// - /// That content should have unique identifier -1 and should not be gettable, - /// ie Get(-1) should return null. Its ParentId should be -1. It should provide - /// values for the attribute fields. - INavigableContent Root { get; } - } + /// + /// Gets a content identified by its unique identifier. + /// + /// The unique identifier. + /// The content identified by the unique identifier, or null. + /// When id is -1 (root content) implementations should return null. + INavigableContent? Get(int id); } diff --git a/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs index 2e2819066b..dd27e6124c 100644 --- a/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.Diagnostics; using System.Xml; using System.Xml.XPath; @@ -66,10 +63,10 @@ namespace Umbraco.Cms.Core.Xml.XPath #endif [Conditional("DEBUG")] - void DebugEnter(string name) + private void DebugEnter(string name) { #if DEBUG - Debug(""); + Debug(string.Empty); DebugState(":"); Debug(name); _tabs = Math.Min(Tabs.Length, _tabs + 2); @@ -77,7 +74,7 @@ namespace Umbraco.Cms.Core.Xml.XPath } [Conditional("DEBUG")] - void DebugCreate(MacroNavigator nav) + private void DebugCreate(MacroNavigator nav) { #if DEBUG Debug("Create: [MacroNavigator::{0}]", nav._uid); @@ -103,16 +100,19 @@ namespace Umbraco.Cms.Core.Xml.XPath } [Conditional("DEBUG")] - void DebugReturn(string format, params object[] args) + private void DebugReturn(string format, params object[] args) { #if DEBUG Debug("=> " + format, args); - if (_tabs > 0) _tabs -= 2; + if (_tabs > 0) + { + _tabs -= 2; + } #endif } [Conditional("DEBUG")] - void DebugState(string s = " =>") + private void DebugState(string s = " =>") { #if DEBUG string position; @@ -123,25 +123,28 @@ namespace Umbraco.Cms.Core.Xml.XPath position = "At macro."; break; case StatePosition.Parameter: - position = string.Format("At parameter '{0}'.", - _macro.Parameters[_state.ParameterIndex].Name); + position = string.Format("At parameter '{0}'.", _macro.Parameters[_state.ParameterIndex].Name); break; case StatePosition.ParameterAttribute: - position = string.Format("At parameter attribute '{0}/{1}'.", + position = string.Format( + "At parameter attribute '{0}/{1}'.", _macro.Parameters[_state.ParameterIndex].Name, _macro.Parameters[_state.ParameterIndex].Attributes?[_state.ParameterAttributeIndex].Key); break; case StatePosition.ParameterNavigator: - position = string.Format("In parameter '{0}{1}' navigator.", + position = string.Format( + "In parameter '{0}{1}' navigator.", _macro.Parameters[_state.ParameterIndex].Name, - _macro.Parameters[_state.ParameterIndex].WrapNavigatorInNodes ? "/nodes" : ""); + _macro.Parameters[_state.ParameterIndex].WrapNavigatorInNodes ? "/nodes" : string.Empty); break; case StatePosition.ParameterNodes: - position = string.Format("At parameter '{0}/nodes'.", + position = string.Format( + "At parameter '{0}/nodes'.", _macro.Parameters[_state.ParameterIndex].Name); break; case StatePosition.ParameterText: - position = string.Format("In parameter '{0}' text.", + position = string.Format( + "In parameter '{0}' text.", _macro.Parameters[_state.ParameterIndex].Name); break; case StatePosition.Root: @@ -156,7 +159,7 @@ namespace Umbraco.Cms.Core.Xml.XPath } #if DEBUG - void Debug(string format, params object[] args) + private void Debug(string format, params object[] args) { // remove comments to write @@ -192,7 +195,9 @@ namespace Umbraco.Cms.Core.Xml.XPath StringValue = value; } - public MacroParameter(string name, XPathNavigator navigator, + public MacroParameter( + string name, + XPathNavigator navigator, int maxNavigatorDepth = int.MaxValue, bool wrapNavigatorInNodes = false, IEnumerable>? attributes = null) @@ -202,10 +207,13 @@ namespace Umbraco.Cms.Core.Xml.XPath WrapNavigatorInNodes = wrapNavigatorInNodes; if (attributes != null) { - var a = attributes.ToArray(); + KeyValuePair[] a = attributes.ToArray(); if (a.Length > 0) + { Attributes = a; + } } + NavigatorValue = navigator; // should not be empty } @@ -248,8 +256,8 @@ namespace Umbraco.Cms.Core.Xml.XPath isEmpty = _macro.Parameters.Length == 0; break; case StatePosition.Parameter: - var parameter = _macro.Parameters[_state.ParameterIndex]; - var nav = parameter.NavigatorValue; + MacroParameter parameter = _macro.Parameters[_state.ParameterIndex]; + XPathNavigator? nav = parameter.NavigatorValue; if (parameter.WrapNavigatorInNodes || nav != null) { isEmpty = false; @@ -259,6 +267,7 @@ namespace Umbraco.Cms.Core.Xml.XPath var s = _macro.Parameters[_state.ParameterIndex].StringValue; isEmpty = s == null; } + break; case StatePosition.ParameterNavigator: isEmpty = _state.ParameterNavigator?.IsEmptyElement ?? true; @@ -410,7 +419,11 @@ namespace Umbraco.Cms.Core.Xml.XPath succ = true; DebugState(); } - else succ = false; + else + { + succ = false; + } + break; case StatePosition.ParameterAttribute: case StatePosition.ParameterNodes: @@ -452,8 +465,8 @@ namespace Umbraco.Cms.Core.Xml.XPath } break; case StatePosition.Parameter: - var parameter = _macro.Parameters[_state.ParameterIndex]; - var nav = parameter.NavigatorValue; + MacroParameter parameter = _macro.Parameters[_state.ParameterIndex]; + XPathNavigator? nav = parameter.NavigatorValue; if (parameter.WrapNavigatorInNodes) { _state.Position = StatePosition.ParameterNodes; @@ -479,8 +492,12 @@ namespace Umbraco.Cms.Core.Xml.XPath DebugState(); succ = true; } - else succ = false; + else + { + succ = false; + } } + break; case StatePosition.ParameterNavigator: if (_state.ParameterNavigatorDepth == _macro.Parameters[_state.ParameterIndex].MaxNavigatorDepth) @@ -507,7 +524,11 @@ namespace Umbraco.Cms.Core.Xml.XPath succ = true; DebugState(); } - else succ = false; + else + { + succ = false; + } + break; case StatePosition.ParameterAttribute: case StatePosition.ParameterText: @@ -692,7 +713,9 @@ namespace Umbraco.Cms.Core.Xml.XPath break; case StatePosition.ParameterAttribute: if (_state.ParameterAttributeIndex == _macro.Parameters[_state.ParameterIndex].Attributes?.Length - 1) + { succ = false; + } else { ++_state.ParameterAttributeIndex; @@ -914,7 +937,9 @@ namespace Umbraco.Cms.Core.Xml.XPath case StatePosition.ParameterNodes: nav = _macro.Parameters[_state.ParameterIndex].NavigatorValue; if (nav == null) + { value = string.Empty; + } else { nav = nav.Clone(); // never use the raw parameter's navigator @@ -945,16 +970,24 @@ namespace Umbraco.Cms.Core.Xml.XPath return false; } if (nav.NodeType != XPathNodeType.Element) + { return false; + } - var clone = nav.Clone(); + XPathNavigator clone = nav.Clone(); if (!clone.MoveToFirstAttribute()) + { return false; + } + do { if (clone.Name == "isDoc") + { return true; - } while (clone.MoveToNextAttribute()); + } + } + while (clone.MoveToNextAttribute()); return false; } @@ -971,7 +1004,7 @@ namespace Umbraco.Cms.Core.Xml.XPath ParameterText, ParameterNodes, ParameterNavigator - }; + } // gets the state // for unit tests only diff --git a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs index a575ee86f8..3529f55922 100644 --- a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs @@ -5,140 +5,141 @@ // but by default nothing is written, unless some lines are un-commented in Debug(...) below. // // Beware! Diagnostics are extremely verbose and can overflow logging pretty easily. - #if DEBUG // define to enable diagnostics code #undef DEBUGNAVIGATOR #endif -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Xml; using System.Xml.XPath; -namespace Umbraco.Cms.Core.Xml.XPath +namespace Umbraco.Cms.Core.Xml.XPath; + +/// +/// Provides a cursor model for navigating Umbraco data as if it were XML. +/// +public class NavigableNavigator : XPathNavigator { + // "The XmlNameTable stores atomized strings of any local name, namespace URI, + // and prefix used by the XPathNavigator. This means that when the same Name is + // returned multiple times (like "book"), the same String object is returned for + // that Name. This makes it possible to write efficient code that does object + // comparisons on these strings, instead of expensive string comparisons." + // + // "When an element or attribute name occurs multiple times in an XML document, + // it is stored only once in the NameTable. The names are stored as common + // language runtime (CLR) object types. This enables you to do object comparisons + // on these strings rather than a more expensive string comparison. These + // string objects are referred to as atomized strings." + // + // But... "Any instance members are not guaranteed to be thread safe." + // + // see http://msdn.microsoft.com/en-us/library/aa735772%28v=vs.71%29.aspx + // see http://www.hanselman.com/blog/XmlAndTheNametable.aspx + // see http://blogs.msdn.com/b/mfussell/archive/2004/04/30/123673.aspx + // + // "Additionally, all LocalName, NameSpaceUri and Prefix strings must be added to + // a NameTable, given by the NameTable property. When the LocalName, NamespaceURI, + // and Prefix properties are returned, the string returned should come from the + // NameTable. Comparisons between names are done by object comparisons rather + // than by string comparisons, which are significantly slower."" + // + // So what shall we do? Well, here we have no namespace, no prefix, and all + // local names come from cached instances of INavigableContentType or + // INavigableFieldType and are already unique. So... create a one nametable + // because we need one, and share it amongst all clones. + private readonly XmlNameTable _nameTable; + private readonly INavigableSource _source; + private readonly int _lastAttributeIndex; // last index of attributes in the fields collection + private readonly int _maxDepth; + + #region Constructor + + ///// + ///// Initializes a new instance of the class with a content source. + ///// + ///// The content source. + ///// The maximum depth. + // private NavigableNavigator(INavigableSource source, int maxDepth) + // { + // _source = source; + // _lastAttributeIndex = source.LastAttributeIndex; + // _maxDepth = maxDepth; + // } + /// - /// Provides a cursor model for navigating Umbraco data as if it were XML. + /// Initializes a new instance of the class with a content source, + /// and an optional root content. /// - public class NavigableNavigator : XPathNavigator + /// The content source. + /// The root content identifier. + /// The maximum depth. + /// When no root content is supplied then the root of the source is used. + public NavigableNavigator(INavigableSource source, int rootId = 0, int maxDepth = int.MaxValue) + + // : this(source, maxDepth) { - // "The XmlNameTable stores atomized strings of any local name, namespace URI, - // and prefix used by the XPathNavigator. This means that when the same Name is - // returned multiple times (like "book"), the same String object is returned for - // that Name. This makes it possible to write efficient code that does object - // comparisons on these strings, instead of expensive string comparisons." - // - // "When an element or attribute name occurs multiple times in an XML document, - // it is stored only once in the NameTable. The names are stored as common - // language runtime (CLR) object types. This enables you to do object comparisons - // on these strings rather than a more expensive string comparison. These - // string objects are referred to as atomized strings." - // - // But... "Any instance members are not guaranteed to be thread safe." - // - // see http://msdn.microsoft.com/en-us/library/aa735772%28v=vs.71%29.aspx - // see http://www.hanselman.com/blog/XmlAndTheNametable.aspx - // see http://blogs.msdn.com/b/mfussell/archive/2004/04/30/123673.aspx - // - // "Additionally, all LocalName, NameSpaceUri and Prefix strings must be added to - // a NameTable, given by the NameTable property. When the LocalName, NamespaceURI, - // and Prefix properties are returned, the string returned should come from the - // NameTable. Comparisons between names are done by object comparisons rather - // than by string comparisons, which are significantly slower."" - // - // So what shall we do? Well, here we have no namespace, no prefix, and all - // local names come from cached instances of INavigableContentType or - // INavigableFieldType and are already unique. So... create a one nametable - // because we need one, and share it amongst all clones. + _source = source; + _lastAttributeIndex = source.LastAttributeIndex; + _maxDepth = maxDepth; - private readonly XmlNameTable _nameTable; - private readonly INavigableSource _source; - private readonly int _lastAttributeIndex; // last index of attributes in the fields collection - private State _state; - private readonly int _maxDepth; - - #region Constructor - - ///// - ///// Initializes a new instance of the class with a content source. - ///// - ///// The content source. - ///// The maximum depth. - //private NavigableNavigator(INavigableSource source, int maxDepth) - //{ - // _source = source; - // _lastAttributeIndex = source.LastAttributeIndex; - // _maxDepth = maxDepth; - //} - - /// - /// Initializes a new instance of the class with a content source, - /// and an optional root content. - /// - /// The content source. - /// The root content identifier. - /// The maximum depth. - /// When no root content is supplied then the root of the source is used. - public NavigableNavigator(INavigableSource source, int rootId = 0, int maxDepth = int.MaxValue) - //: this(source, maxDepth) + _nameTable = new NameTable(); + _lastAttributeIndex = source.LastAttributeIndex; + INavigableContent? content = rootId <= 0 ? source.Root : source.Get(rootId); + if (content == null) { - _source = source; - _lastAttributeIndex = source.LastAttributeIndex; - _maxDepth = maxDepth; - - _nameTable = new NameTable(); - _lastAttributeIndex = source.LastAttributeIndex; - var content = rootId <= 0 ? source.Root : source.Get(rootId); - if (content == null) - throw new ArgumentException("Not the identifier of a content within the source.", nameof(rootId)); - _state = new State(content, null, null, 0, StatePosition.Root); - - _contents = new ConcurrentDictionary(); + throw new ArgumentException("Not the identifier of a content within the source.", nameof(rootId)); } - ///// - ///// Initializes a new instance of the class with a content source, a name table and a state. - ///// - ///// The content source. - ///// The name table. - ///// The state. - ///// The maximum depth. - ///// Privately used for cloning a navigator. - //private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state, int maxDepth) - // : this(source, rootId: 0, maxDepth: maxDepth) - //{ - // _nameTable = nameTable; - // _state = state; - //} + InternalState = new State(content, null, null, 0, StatePosition.Root); - /// - /// Initializes a new instance of the class as a clone. - /// - /// The cloned navigator. - /// The clone state. - /// The clone maximum depth. - /// Privately used for cloning a navigator. - private NavigableNavigator(NavigableNavigator orig, State? state = null, int maxDepth = -1) - : this(orig._source, rootId: 0, maxDepth: orig._maxDepth) + _contents = new ConcurrentDictionary(); + } + + ///// + ///// Initializes a new instance of the class with a content source, a name table and a state. + ///// + ///// The content source. + ///// The name table. + ///// The state. + ///// The maximum depth. + ///// Privately used for cloning a navigator. + // private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state, int maxDepth) + // : this(source, rootId: 0, maxDepth: maxDepth) + // { + // _nameTable = nameTable; + // _state = state; + // } + + /// + /// Initializes a new instance of the class as a clone. + /// + /// The cloned navigator. + /// The clone state. + /// The clone maximum depth. + /// Privately used for cloning a navigator. + private NavigableNavigator(NavigableNavigator orig, State? state = null, int maxDepth = -1) + : this(orig._source, 0, orig._maxDepth) + { + _nameTable = orig._nameTable; + + InternalState = state ?? orig.InternalState.Clone(); + if (state != null && maxDepth < 0) { - _nameTable = orig._nameTable; - - _state = state ?? orig._state.Clone(); - if (state != null && maxDepth < 0) - throw new ArgumentException("Both state and maxDepth are required."); - _maxDepth = maxDepth < 0 ? orig._maxDepth : maxDepth; - - _contents = orig._contents; + throw new ArgumentException("Both state and maxDepth are required."); } - #endregion + _maxDepth = maxDepth < 0 ? orig._maxDepth : maxDepth; - #region Diagnostics + _contents = orig._contents; + } + + #endregion + + #region Diagnostics #if DEBUGNAVIGATOR private const string Tabs = " "; @@ -155,60 +156,59 @@ namespace Umbraco.Cms.Core.Xml.XPath } #endif - // About conditional methods: marking a method with the [Conditional] attribute ensures - // that no calls to the method will be generated by the compiler. However, the method - // does exist. Wrapping the method body with #if/endif ensures that no IL is generated - // and so it's only an empty method. - - [Conditional("DEBUGNAVIGATOR")] - void DebugEnter(string name) - { + // About conditional methods: marking a method with the [Conditional] attribute ensures + // that no calls to the method will be generated by the compiler. However, the method + // does exist. Wrapping the method body with #if/endif ensures that no IL is generated + // and so it's only an empty method. + [Conditional("DEBUGNAVIGATOR")] + private void DebugEnter(string name) + { #if DEBUGNAVIGATOR Debug(""); DebugState(":"); Debug(name); _tabs = Math.Min(Tabs.Length, _tabs + 2); #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - void DebugCreate(NavigableNavigator nav) - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugCreate(NavigableNavigator nav) + { #if DEBUGNAVIGATOR Debug("Create: [NavigableNavigator::{0}]", nav._uid); #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - private void DebugReturn() - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugReturn() + { #if DEBUGNAVIGATOR // ReSharper disable IntroduceOptionalParameters.Local DebugReturn("(void)"); // ReSharper restore IntroduceOptionalParameters.Local #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - private void DebugReturn(bool value) - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugReturn(bool value) + { #if DEBUGNAVIGATOR DebugReturn(value ? "true" : "false"); #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - void DebugReturn(string format, params object[] args) - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugReturn(string format, params object[] args) + { #if DEBUGNAVIGATOR Debug("=> " + format, args); if (_tabs > 0) _tabs -= 2; #endif - } + } - [Conditional("DEBUGNAVIGATOR")] - void DebugState(string s = " =>") - { + [Conditional("DEBUGNAVIGATOR")] + private void DebugState(string s = " =>") + { #if DEBUGNAVIGATOR string position; @@ -245,7 +245,7 @@ namespace Umbraco.Cms.Core.Xml.XPath Debug("State{0} {1}", s, position); #endif - } + } #if DEBUGNAVIGATOR void Debug(string format, params object[] args) @@ -257,980 +257,1035 @@ namespace Umbraco.Cms.Core.Xml.XPath } #endif - #endregion + #endregion - #region Source management + #region Source management - private readonly ConcurrentDictionary _contents; + private readonly ConcurrentDictionary _contents; - private INavigableContent? SourceGet(int id) + private INavigableContent? SourceGet(int id) => + + // original version, would keep creating INavigableContent objects + // return _source.Get(id); + // improved version, uses a cache, shared with clones + _contents.GetOrAdd(id, x => _source.Get(x)); + + #endregion + + /// + /// Gets the underlying content object. + /// + public override object? UnderlyingObject => InternalState.Content; + + /// + /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. + /// + /// A new XPathNavigator positioned at the same node as this XPathNavigator. + public override XPathNavigator Clone() + { + DebugEnter("Clone"); + var nav = new NavigableNavigator(this); + DebugCreate(nav); + DebugReturn("[XPathNavigator]"); + return nav; + } + + /// + /// Creates a new XPathNavigator using the same source but positioned at a new root. + /// + /// A new XPathNavigator using the same source and positioned at a new root. + /// The new root can be above this navigator's root. + public XPathNavigator CloneWithNewRoot(string id, int maxDepth = int.MaxValue) + { + int i; + if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out i) == false) { - // original version, would keep creating INavigableContent objects - //return _source.Get(id); - - // improved version, uses a cache, shared with clones - return _contents.GetOrAdd(id, x => _source.Get(x)); + throw new ArgumentException("Not a valid identifier.", nameof(id)); } - #endregion + return CloneWithNewRoot(id); + } - /// - /// Gets the underlying content object. - /// - public override object? UnderlyingObject => _state.Content; + /// + /// Creates a new XPathNavigator using the same source but positioned at a new root. + /// + /// A new XPathNavigator using the same source and positioned at a new root. + /// The new root can be above this navigator's root. + public XPathNavigator? CloneWithNewRoot(int id, int maxDepth = int.MaxValue) + { + DebugEnter("CloneWithNewRoot"); - /// - /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. - /// - /// A new XPathNavigator positioned at the same node as this XPathNavigator. - public override XPathNavigator Clone() + State? state = null; + + if (id <= 0) { - DebugEnter("Clone"); - var nav = new NavigableNavigator(this); - DebugCreate(nav); + state = new State(_source.Root, null, null, 0, StatePosition.Root); + } + else + { + INavigableContent? content = SourceGet(id); + if (content != null) + { + state = new State(content, null, null, 0, StatePosition.Root); + } + } + + NavigableNavigator? clone = null; + + if (state != null) + { + clone = new NavigableNavigator(this, state, maxDepth); + DebugCreate(clone); DebugReturn("[XPathNavigator]"); - return nav; + } + else + { + DebugReturn("[null]"); } - /// - /// Creates a new XPathNavigator using the same source but positioned at a new root. - /// - /// A new XPathNavigator using the same source and positioned at a new root. - /// The new root can be above this navigator's root. - public XPathNavigator CloneWithNewRoot(string id, int maxDepth = int.MaxValue) + return clone; + } + + /// + /// Gets a value indicating whether the current node is an empty element without an end element tag. + /// + public override bool IsEmptyElement + { + get { - int i; - if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out i) == false) - throw new ArgumentException("Not a valid identifier.", nameof(id)); - return CloneWithNewRoot(id); - } + DebugEnter("IsEmptyElement"); + bool isEmpty; - /// - /// Creates a new XPathNavigator using the same source but positioned at a new root. - /// - /// A new XPathNavigator using the same source and positioned at a new root. - /// The new root can be above this navigator's root. - public XPathNavigator? CloneWithNewRoot(int id, int maxDepth = int.MaxValue) - { - DebugEnter("CloneWithNewRoot"); - - State? state = null; - - if (id <= 0) - { - state = new State(_source.Root, null, null, 0, StatePosition.Root); - } - else - { - var content = SourceGet(id); - if (content != null) - { - state = new State(content, null, null, 0, StatePosition.Root); - } - } - - NavigableNavigator? clone = null; - - if (state != null) - { - clone = new NavigableNavigator(this, state, maxDepth); - DebugCreate(clone); - DebugReturn("[XPathNavigator]"); - } - else - { - DebugReturn("[null]"); - } - - return clone; - } - - /// - /// Gets a value indicating whether the current node is an empty element without an end element tag. - /// - public override bool IsEmptyElement - { - get - { - DebugEnter("IsEmptyElement"); - bool isEmpty; - - switch (_state.Position) - { - case StatePosition.Element: - // must go through source because of preview/published ie there may be - // ids but corresponding to preview elements that we don't see here - var hasContentChild = _state.GetContentChildIds(_maxDepth).Any(x => SourceGet(x) != null); - isEmpty = (hasContentChild == false) // no content child - && _state.FieldsCount - 1 == _lastAttributeIndex; // no property element child - break; - case StatePosition.PropertyElement: - // value should be - // - an XPathNavigator over a non-empty XML fragment - // - a non-Xml-whitespace string - // - null - isEmpty = _state.Content?.Value(_state.FieldIndex) == null; - break; - case StatePosition.PropertyXml: - isEmpty = _state.XmlFragmentNavigator?.IsEmptyElement ?? true; - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - case StatePosition.Root: - throw new InvalidOperationException("Not an element."); - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(isEmpty); - return isEmpty; - } - } - - /// - /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator. - /// - /// The XPathNavigator to compare to this XPathNavigator. - /// true if the two XPathNavigator objects have the same position; otherwise, false. - public override bool IsSamePosition(XPathNavigator nav) - { - DebugEnter("IsSamePosition"); - bool isSame; - - switch (_state.Position) + switch (InternalState.Position) { + case StatePosition.Element: + // must go through source because of preview/published ie there may be + // ids but corresponding to preview elements that we don't see here + var hasContentChild = InternalState.GetContentChildIds(_maxDepth).Any(x => SourceGet(x) != null); + isEmpty = hasContentChild == false // no content child + && InternalState.FieldsCount - 1 == _lastAttributeIndex; // no property element child + break; + case StatePosition.PropertyElement: + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + isEmpty = InternalState.Content?.Value(InternalState.FieldIndex) == null; + break; case StatePosition.PropertyXml: - isSame = _state.XmlFragmentNavigator?.IsSamePosition(nav) ?? false; + isEmpty = InternalState.XmlFragmentNavigator?.IsEmptyElement ?? true; break; case StatePosition.Attribute: - case StatePosition.Element: - case StatePosition.PropertyElement: case StatePosition.PropertyText: case StatePosition.Root: - var other = nav as NavigableNavigator; - isSame = other != null && other._source == _source && _state.IsSamePosition(other._state); + throw new InvalidOperationException("Not an element."); + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isEmpty); + return isEmpty; + } + } + + /// + /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator. + /// + /// The XPathNavigator to compare to this XPathNavigator. + /// true if the two XPathNavigator objects have the same position; otherwise, false. + public override bool IsSamePosition(XPathNavigator nav) + { + DebugEnter("IsSamePosition"); + bool isSame; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + isSame = InternalState.XmlFragmentNavigator?.IsSamePosition(nav) ?? false; + break; + case StatePosition.Attribute: + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + var other = nav as NavigableNavigator; + isSame = other != null && other._source == _source && InternalState.IsSamePosition(other.InternalState); + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isSame); + return isSame; + } + + /// + /// Gets the qualified name of the current node. + /// + public override string Name + { + get + { + DebugEnter("Name"); + string name; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + name = InternalState.XmlFragmentNavigator?.Name ?? string.Empty; + break; + case StatePosition.Attribute: + case StatePosition.PropertyElement: + name = InternalState.FieldIndex == -1 ? "id" : InternalState.CurrentFieldType?.Name ?? string.Empty; + break; + case StatePosition.Element: + name = InternalState.Content?.Type.Name ?? string.Empty; + break; + case StatePosition.PropertyText: + name = string.Empty; + break; + case StatePosition.Root: + name = string.Empty; break; default: throw new InvalidOperationException("Invalid position."); } - DebugReturn(isSame); - return isSame; + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Gets the Name of the current node without any namespace prefix. + /// + public override string LocalName + { + get + { + DebugEnter("LocalName"); + var name = Name; + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Moves the XPathNavigator to the same position as the specified XPathNavigator. + /// + /// The XPathNavigator positioned on the node that you want to move to. + /// + /// Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveTo(XPathNavigator nav) + { + DebugEnter("MoveTo"); + + var other = nav as NavigableNavigator; + var succ = false; + + if (other != null && other._source == _source) + { + InternalState = other.InternalState.Clone(); + DebugState(); + succ = true; } - /// - /// Gets the qualified name of the current node. - /// - public override string Name + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first attribute of the current node. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the first attribute of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToFirstAttribute() + { + DebugEnter("MoveToFirstAttribute"); + bool succ; + + switch (InternalState.Position) { - get - { - DebugEnter("Name"); - string name; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - name = _state.XmlFragmentNavigator?.Name ?? string.Empty; - break; - case StatePosition.Attribute: - case StatePosition.PropertyElement: - name = _state.FieldIndex == -1 ? "id" : _state.CurrentFieldType?.Name ?? string.Empty; - break; - case StatePosition.Element: - name = _state.Content?.Type.Name ?? string.Empty; - break; - case StatePosition.PropertyText: - name = string.Empty; - break; - case StatePosition.Root: - name = string.Empty; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn("\"{0}\"", name); - return name; - } - } - - /// - /// Gets the Name of the current node without any namespace prefix. - /// - public override string LocalName - { - get - { - DebugEnter("LocalName"); - var name = Name; - DebugReturn("\"{0}\"", name); - return name; - } - } - - /// - /// Moves the XPathNavigator to the same position as the specified XPathNavigator. - /// - /// The XPathNavigator positioned on the node that you want to move to. - /// Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveTo(XPathNavigator nav) - { - DebugEnter("MoveTo"); - - var other = nav as NavigableNavigator; - var succ = false; - - if (other != null && other._source == _source) - { - _state = other._state.Clone(); + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToFirstAttribute() ?? false; + break; + case StatePosition.Element: + InternalState.FieldIndex = -1; + InternalState.Position = StatePosition.Attribute; DebugState(); succ = true; - } - - DebugReturn(succ); - return succ; + break; + case StatePosition.Attribute: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); } - /// - /// Moves the XPathNavigator to the first attribute of the current node. - /// - /// Returns true if the XPathNavigator is successful moving to the first attribute of the current node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToFirstAttribute() + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first child node of the current node. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the first child node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToFirstChild() + { + DebugEnter("MoveToFirstChild"); + bool succ; + + switch (InternalState.Position) { - DebugEnter("MoveToFirstAttribute"); - bool succ; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToFirstAttribute() ?? false; - break; - case StatePosition.Element: - _state.FieldIndex = -1; - _state.Position = StatePosition.Attribute; - DebugState(); - succ = true; - break; - case StatePosition.Attribute: - case StatePosition.PropertyElement: - case StatePosition.PropertyText: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - /// - /// Moves the XPathNavigator to the first child node of the current node. - /// - /// Returns true if the XPathNavigator is successful moving to the first child node of the current node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToFirstChild() - { - DebugEnter("MoveToFirstChild"); - bool succ; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToFirstChild() ?? false; - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - succ = false; - break; - case StatePosition.Element: - var firstPropertyIndex = _lastAttributeIndex + 1; - if (_state.FieldsCount > firstPropertyIndex) - { - _state.Position = StatePosition.PropertyElement; - _state.FieldIndex = firstPropertyIndex; - DebugState(); - succ = true; - } - else succ = MoveToFirstChildElement(); - break; - case StatePosition.PropertyElement: - succ = MoveToFirstChildProperty(); - break; - case StatePosition.Root: - _state.Position = StatePosition.Element; - DebugState(); - succ = true; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - private bool MoveToFirstChildElement() - { - var children = _state.GetContentChildIds(_maxDepth); - - if (children.Count > 0) - { - // children may contain IDs that does not correspond to some content in source - // because children contains all child IDs including unpublished children - and - // then if we're not previewing, the source will return null. - var child = children.Select(id => SourceGet(id)).FirstOrDefault(c => c != null); - if (child != null) + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToFirstChild() ?? false; + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + succ = false; + break; + case StatePosition.Element: + var firstPropertyIndex = _lastAttributeIndex + 1; + if (InternalState.FieldsCount > firstPropertyIndex) { - _state.Position = StatePosition.Element; - _state.FieldIndex = -1; - _state = new State(child, _state, children, 0, StatePosition.Element); + InternalState.Position = StatePosition.PropertyElement; + InternalState.FieldIndex = firstPropertyIndex; DebugState(); - return true; - } - } - - return false; - } - - private bool MoveToFirstChildProperty() - { - var valueForXPath = _state.Content?.Value(_state.FieldIndex); - - // value should be - // - an XPathNavigator over a non-empty XML fragment - // - a non-Xml-whitespace string - // - null - - var nav = valueForXPath as XPathNavigator; - if (nav != null) - { - nav = nav.Clone(); // never use the one we got - nav.MoveToFirstChild(); - _state.XmlFragmentNavigator = nav; - _state.Position = StatePosition.PropertyXml; - DebugState(); - return true; - } - - if (valueForXPath == null) - return false; - - if (valueForXPath is string) - { - _state.Position = StatePosition.PropertyText; - DebugState(); - return true; - } - - throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); - } - - /// - /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified. - /// - /// An XPathNamespaceScope value describing the namespace scope. - /// Returns true if the XPathNavigator is successful moving to the first namespace node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) - { - DebugEnter("MoveToFirstNamespace"); - DebugReturn(false); - return false; - } - - /// - /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified. - /// - /// An XPathNamespaceScope value describing the namespace scope. - /// Returns true if the XPathNavigator is successful moving to the next namespace node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) - { - DebugEnter("MoveToNextNamespace"); - DebugReturn(false); - return false; - } - - /// - /// Moves to the node that has an attribute of type ID whose value matches the specified String. - /// - /// A String representing the ID value of the node to which you want to move. - /// true if the XPathNavigator is successful moving; otherwise, false. - /// If false, the position of the navigator is unchanged. - public override bool MoveToId(string id) - { - DebugEnter("MoveToId"); - var succ = false; - - // don't look into fragments, just look for element identifiers - // not sure we actually need to implement it... think of it as - // as exercise of style, always better than throwing NotImplemented. - - // navigator may be rooted below source root - // find the navigator root id - var state = _state; - while (state.Parent != null) // root state has no parent - state = state.Parent; - var navRootId = state.Content?.Id; - - int contentId; - if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentId)) - { - if (contentId == navRootId) - { - _state = new State(state.Content, null, null, 0, StatePosition.Element); succ = true; } else { - var content = SourceGet(contentId); - if (content != null) - { - // walk up to the navigator's root - or the source's root - var s = new Stack(); - while (content != null && content.ParentId != navRootId) - { - s.Push(content); - content = SourceGet(content.ParentId); - } - - if (content != null && s.Count < _maxDepth) - { - _state = new State(state.Content, null, null, 0, StatePosition.Element); - while (content != null) - { - _state = new State(content, _state, _state.Content?.ChildIds, _state.Content?.ChildIds?.IndexOf(content.Id) ?? -1, StatePosition.Element); - content = s.Count == 0 ? null : s.Pop(); - } - DebugState(); - succ = true; - } - } + succ = MoveToFirstChildElement(); } - } - DebugReturn(succ); - return succ; + break; + case StatePosition.PropertyElement: + succ = MoveToFirstChildProperty(); + break; + case StatePosition.Root: + InternalState.Position = StatePosition.Element; + DebugState(); + succ = true; + break; + default: + throw new InvalidOperationException("Invalid position."); } - /// - /// Moves the XPathNavigator to the next sibling node of the current node. - /// - /// true if the XPathNavigator is successful moving to the next sibling node; - /// otherwise, false if there are no more siblings or if the XPathNavigator is currently - /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToNext() + DebugReturn(succ); + return succ; + } + + private bool MoveToFirstChildElement() + { + IList children = InternalState.GetContentChildIds(_maxDepth); + + if (children.Count > 0) { - DebugEnter("MoveToNext"); - bool succ; - - switch (_state.Position) + // children may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + INavigableContent? child = children.Select(id => SourceGet(id)).FirstOrDefault(c => c != null); + if (child != null) { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToNext() ?? false; - break; - case StatePosition.Element: - succ = false; - while (_state.Siblings != null && _state.SiblingIndex < _state.Siblings.Count - 1) - { - // Siblings may contain IDs that does not correspond to some content in source - // because children contains all child IDs including unpublished children - and - // then if we're not previewing, the source will return null. - var node = SourceGet(_state.Siblings[++_state.SiblingIndex]); - if (node == null) continue; - - _state.Content = node; - DebugState(); - succ = true; - break; - } - break; - case StatePosition.PropertyElement: - if (_state.FieldIndex == _state.FieldsCount - 1) - { - // after property elements may come some children elements - // if successful, will push a new state - succ = MoveToFirstChildElement(); - } - else - { - ++_state.FieldIndex; - DebugState(); - succ = true; - } - break; - case StatePosition.PropertyText: - case StatePosition.Attribute: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - /// - /// Moves the XPathNavigator to the previous sibling node of the current node. - /// - /// Returns true if the XPathNavigator is successful moving to the previous sibling node; - /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently - /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToPrevious() - { - DebugEnter("MoveToPrevious"); - bool succ; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToPrevious() ?? false; - break; - case StatePosition.Element: - succ = false; - while (_state.Siblings != null && _state.SiblingIndex > 0) - { - // children may contain IDs that does not correspond to some content in source - // because children contains all child IDs including unpublished children - and - // then if we're not previewing, the source will return null. - var content = SourceGet(_state.Siblings[--_state.SiblingIndex]); - if (content == null) continue; - - _state.Content = content; - DebugState(); - succ = true; - break; - } - if (succ == false && _state.SiblingIndex == 0 && _state.FieldsCount - 1 > _lastAttributeIndex) - { - // before children elements may come some property elements - if (MoveToParentElement()) // pops the state - { - _state.FieldIndex = _state.FieldsCount - 1; - DebugState(); - succ = true; - } - } - break; - case StatePosition.PropertyElement: - succ = false; - if (_state.FieldIndex > _lastAttributeIndex) - { - --_state.FieldIndex; - DebugState(); - succ = true; - } - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - /// - /// Moves the XPathNavigator to the next attribute. - /// - /// Returns true if the XPathNavigator is successful moving to the next attribute; - /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToNextAttribute() - { - DebugEnter("MoveToNextAttribute"); - bool succ; - - switch (_state.Position) - { - case StatePosition.PropertyXml: - succ = _state.XmlFragmentNavigator?.MoveToNextAttribute() ?? false; - break; - case StatePosition.Attribute: - if (_state.FieldIndex == _lastAttributeIndex) - succ = false; - else - { - ++_state.FieldIndex; - DebugState(); - succ = true; - } - break; - case StatePosition.Element: - case StatePosition.PropertyElement: - case StatePosition.PropertyText: - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - /// - /// Moves the XPathNavigator to the parent node of the current node. - /// - /// Returns true if the XPathNavigator is successful moving to the parent node of the current node; - /// otherwise, false. If false, the position of the XPathNavigator is unchanged. - public override bool MoveToParent() - { - DebugEnter("MoveToParent"); - bool succ; - - switch (_state.Position) - { - case StatePosition.Attribute: - case StatePosition.PropertyElement: - _state.Position = StatePosition.Element; - _state.FieldIndex = -1; - DebugState(); - succ = true; - break; - case StatePosition.Element: - succ = MoveToParentElement(); - if (succ == false) - { - _state.Position = StatePosition.Root; - succ = true; - } - break; - case StatePosition.PropertyText: - _state.Position = StatePosition.PropertyElement; - DebugState(); - succ = true; - break; - case StatePosition.PropertyXml: - if (_state.XmlFragmentNavigator?.MoveToParent() == false) - throw new InvalidOperationException("Could not move to parent in fragment."); - if (_state.XmlFragmentNavigator?.NodeType == XPathNodeType.Root) - { - _state.XmlFragmentNavigator = null; - _state.Position = StatePosition.PropertyElement; - DebugState(); - } - succ = true; - break; - case StatePosition.Root: - succ = false; - break; - default: - throw new InvalidOperationException("Invalid position."); - } - - DebugReturn(succ); - return succ; - } - - private bool MoveToParentElement() - { - var p = _state.Parent; - if (p != null) - { - _state = p; + InternalState.Position = StatePosition.Element; + InternalState.FieldIndex = -1; + InternalState = new State(child, InternalState, children, 0, StatePosition.Element); DebugState(); return true; } + } + return false; + } + + private bool MoveToFirstChildProperty() + { + var valueForXPath = InternalState.Content?.Value(InternalState.FieldIndex); + + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + var nav = valueForXPath as XPathNavigator; + if (nav != null) + { + nav = nav.Clone(); // never use the one we got + nav.MoveToFirstChild(); + InternalState.XmlFragmentNavigator = nav; + InternalState.Position = StatePosition.PropertyXml; + DebugState(); + return true; + } + + if (valueForXPath == null) + { return false; } - /// - /// Moves the XPathNavigator to the root node that the current node belongs to. - /// - public override void MoveToRoot() + if (valueForXPath is string) { - DebugEnter("MoveToRoot"); - - while (_state.Parent != null) - _state = _state.Parent; + InternalState.Position = StatePosition.PropertyText; DebugState(); - - DebugReturn(); + return true; } - /// - /// Gets the base URI for the current node. - /// - public override string BaseURI => string.Empty; + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); + } - /// - /// Gets the XmlNameTable of the XPathNavigator. - /// - public override XmlNameTable NameTable => _nameTable; + /// + /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// + /// Returns true if the XPathNavigator is successful moving to the first namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToFirstNamespace"); + DebugReturn(false); + return false; + } - /// - /// Gets the namespace URI of the current node. - /// - public override string NamespaceURI => string.Empty; + /// + /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// + /// Returns true if the XPathNavigator is successful moving to the next namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToNextNamespace"); + DebugReturn(false); + return false; + } - /// - /// Gets the XPathNodeType of the current node. - /// - public override XPathNodeType NodeType + /// + /// Moves to the node that has an attribute of type ID whose value matches the specified String. + /// + /// A String representing the ID value of the node to which you want to move. + /// + /// true if the XPathNavigator is successful moving; otherwise, false. + /// If false, the position of the navigator is unchanged. + /// + public override bool MoveToId(string id) + { + DebugEnter("MoveToId"); + var succ = false; + + // don't look into fragments, just look for element identifiers + // not sure we actually need to implement it... think of it as + // as exercise of style, always better than throwing NotImplemented. + + // navigator may be rooted below source root + // find the navigator root id + State state = InternalState; + + // root state has no parent + while (state.Parent != null) { - get + state = state.Parent; + } + + var navRootId = state.Content?.Id; + + int contentId; + if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentId)) + { + if (contentId == navRootId) { - DebugEnter("NodeType"); - XPathNodeType type; - - switch (_state.Position) + InternalState = new State(state.Content, null, null, 0, StatePosition.Element); + succ = true; + } + else + { + INavigableContent? content = SourceGet(contentId); + if (content != null) { - case StatePosition.PropertyXml: - type = _state.XmlFragmentNavigator?.NodeType ?? XPathNodeType.Root; - break; - case StatePosition.Attribute: - type = XPathNodeType.Attribute; - break; - case StatePosition.Element: - case StatePosition.PropertyElement: - type = XPathNodeType.Element; - break; - case StatePosition.PropertyText: - type = XPathNodeType.Text; - break; - case StatePosition.Root: - type = XPathNodeType.Root; - break; - default: - throw new InvalidOperationException("Invalid position."); - } + // walk up to the navigator's root - or the source's root + var s = new Stack(); + while (content != null && content.ParentId != navRootId) + { + s.Push(content); + content = SourceGet(content.ParentId); + } - DebugReturn("\'{0}\'", type); - return type; + if (content != null && s.Count < _maxDepth) + { + InternalState = new State(state.Content, null, null, 0, StatePosition.Element); + while (content != null) + { + InternalState = new State(content, InternalState, InternalState.Content?.ChildIds, InternalState.Content?.ChildIds?.IndexOf(content.Id) ?? -1, StatePosition.Element); + content = s.Count == 0 ? null : s.Pop(); + } + + DebugState(); + succ = true; + } + } } } - /// - /// Gets the namespace prefix associated with the current node. - /// - public override string Prefix => string.Empty; + DebugReturn(succ); + return succ; + } - /// - /// Gets the string value of the item. - /// - /// Does not fully behave as per the specs, as we report empty value on content elements, and we start - /// reporting values only on property elements. This is because, otherwise, we would dump the whole database - /// and it probably does not make sense at Umbraco level. - public override string Value + /// + /// Moves the XPathNavigator to the next sibling node of the current node. + /// + /// + /// true if the XPathNavigator is successful moving to the next sibling node; + /// otherwise, false if there are no more siblings or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToNext() + { + DebugEnter("MoveToNext"); + bool succ; + + switch (InternalState.Position) { - get - { - DebugEnter("Value"); - string value; - - switch (_state.Position) + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToNext() ?? false; + break; + case StatePosition.Element: + succ = false; + while (InternalState.Siblings != null && InternalState.SiblingIndex < InternalState.Siblings.Count - 1) { - case StatePosition.PropertyXml: - value = _state.XmlFragmentNavigator?.Value ?? string.Empty; - break; - case StatePosition.Attribute: - case StatePosition.PropertyText: - case StatePosition.PropertyElement: - if (_state.FieldIndex == -1) + // Siblings may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + INavigableContent? node = SourceGet(InternalState.Siblings[++InternalState.SiblingIndex]); + if (node == null) + { + continue; + } + + InternalState.Content = node; + DebugState(); + succ = true; + break; + } + + break; + case StatePosition.PropertyElement: + if (InternalState.FieldIndex == InternalState.FieldsCount - 1) + { + // after property elements may come some children elements + // if successful, will push a new state + succ = MoveToFirstChildElement(); + } + else + { + ++InternalState.FieldIndex; + DebugState(); + succ = true; + } + + break; + case StatePosition.PropertyText: + case StatePosition.Attribute: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the previous sibling node of the current node. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the previous sibling node; + /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToPrevious() + { + DebugEnter("MoveToPrevious"); + bool succ; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToPrevious() ?? false; + break; + case StatePosition.Element: + succ = false; + while (InternalState.Siblings != null && InternalState.SiblingIndex > 0) + { + // children may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + INavigableContent? content = SourceGet(InternalState.Siblings[--InternalState.SiblingIndex]); + if (content == null) + { + continue; + } + + InternalState.Content = content; + DebugState(); + succ = true; + break; + } + + if (succ == false && InternalState.SiblingIndex == 0 && + InternalState.FieldsCount - 1 > _lastAttributeIndex) + { + // before children elements may come some property elements + // pops the state + if (MoveToParentElement()) + { + InternalState.FieldIndex = InternalState.FieldsCount - 1; + DebugState(); + succ = true; + } + } + + break; + case StatePosition.PropertyElement: + succ = false; + if (InternalState.FieldIndex > _lastAttributeIndex) + { + --InternalState.FieldIndex; + DebugState(); + succ = true; + } + + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the next attribute. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the next attribute; + /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToNextAttribute() + { + DebugEnter("MoveToNextAttribute"); + bool succ; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + succ = InternalState.XmlFragmentNavigator?.MoveToNextAttribute() ?? false; + break; + case StatePosition.Attribute: + if (InternalState.FieldIndex == _lastAttributeIndex) + { + succ = false; + } + else + { + ++InternalState.FieldIndex; + DebugState(); + succ = true; + } + + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the parent node of the current node. + /// + /// + /// Returns true if the XPathNavigator is successful moving to the parent node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + /// + public override bool MoveToParent() + { + DebugEnter("MoveToParent"); + bool succ; + + switch (InternalState.Position) + { + case StatePosition.Attribute: + case StatePosition.PropertyElement: + InternalState.Position = StatePosition.Element; + InternalState.FieldIndex = -1; + DebugState(); + succ = true; + break; + case StatePosition.Element: + succ = MoveToParentElement(); + if (succ == false) + { + InternalState.Position = StatePosition.Root; + succ = true; + } + + break; + case StatePosition.PropertyText: + InternalState.Position = StatePosition.PropertyElement; + DebugState(); + succ = true; + break; + case StatePosition.PropertyXml: + if (InternalState.XmlFragmentNavigator?.MoveToParent() == false) + { + throw new InvalidOperationException("Could not move to parent in fragment."); + } + + if (InternalState.XmlFragmentNavigator?.NodeType == XPathNodeType.Root) + { + InternalState.XmlFragmentNavigator = null; + InternalState.Position = StatePosition.PropertyElement; + DebugState(); + } + + succ = true; + break; + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + private bool MoveToParentElement() + { + State? p = InternalState.Parent; + if (p != null) + { + InternalState = p; + DebugState(); + return true; + } + + return false; + } + + /// + /// Moves the XPathNavigator to the root node that the current node belongs to. + /// + public override void MoveToRoot() + { + DebugEnter("MoveToRoot"); + + while (InternalState.Parent != null) + { + InternalState = InternalState.Parent; + } + + DebugState(); + + DebugReturn(); + } + + /// + /// Gets the base URI for the current node. + /// + public override string BaseURI => string.Empty; + + /// + /// Gets the XmlNameTable of the XPathNavigator. + /// + public override XmlNameTable NameTable => _nameTable; + + /// + /// Gets the namespace URI of the current node. + /// + public override string NamespaceURI => string.Empty; + + /// + /// Gets the XPathNodeType of the current node. + /// + public override XPathNodeType NodeType + { + get + { + DebugEnter("NodeType"); + XPathNodeType type; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + type = InternalState.XmlFragmentNavigator?.NodeType ?? XPathNodeType.Root; + break; + case StatePosition.Attribute: + type = XPathNodeType.Attribute; + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + type = XPathNodeType.Element; + break; + case StatePosition.PropertyText: + type = XPathNodeType.Text; + break; + case StatePosition.Root: + type = XPathNodeType.Root; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\'{0}\'", type); + return type; + } + } + + /// + /// Gets the namespace prefix associated with the current node. + /// + public override string Prefix => string.Empty; + + /// + /// Gets the string value of the item. + /// + /// + /// Does not fully behave as per the specs, as we report empty value on content elements, and we start + /// reporting values only on property elements. This is because, otherwise, we would dump the whole database + /// and it probably does not make sense at Umbraco level. + /// + public override string Value + { + get + { + DebugEnter("Value"); + string value; + + switch (InternalState.Position) + { + case StatePosition.PropertyXml: + value = InternalState.XmlFragmentNavigator?.Value ?? string.Empty; + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.PropertyElement: + if (InternalState.FieldIndex == -1) + { + value = InternalState.Content?.Id.ToString(CultureInfo.InvariantCulture) ?? string.Empty; + } + else + { + var valueForXPath = InternalState.Content?.Value(InternalState.FieldIndex); + + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + var nav = valueForXPath as XPathNavigator; + var s = valueForXPath as string; + if (valueForXPath == null) { - value = _state.Content?.Id.ToString(CultureInfo.InvariantCulture) ?? string.Empty; + value = string.Empty; + } + else if (nav != null) + { + nav = nav.Clone(); // never use the one we got + value = nav.Value; + } + else if (s != null) + { + value = s; } else { - var valueForXPath = _state.Content?.Value(_state.FieldIndex); - - // value should be - // - an XPathNavigator over a non-empty XML fragment - // - a non-Xml-whitespace string - // - null - - var nav = valueForXPath as XPathNavigator; - var s = valueForXPath as string; - if (valueForXPath == null) - { - value = string.Empty; - } - else if (nav != null) - { - nav = nav.Clone(); // never use the one we got - value = nav.Value; - } - else if (s != null) - { - value = s; - } - else - { - throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); - } + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); } - break; - case StatePosition.Element: - case StatePosition.Root: - value = string.Empty; - break; - default: - throw new InvalidOperationException("Invalid position."); - } + } - DebugReturn("\"{0}\"", value); - return value; + break; + case StatePosition.Element: + case StatePosition.Root: + value = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); } + + DebugReturn("\"{0}\"", value); + return value; } - - #region State management - - // the possible state positions - public enum StatePosition - { - Root, - Element, - Attribute, - PropertyElement, - PropertyText, - PropertyXml - }; - - // gets the state - // for unit tests only - public State InternalState => _state; - - // represents the XPathNavigator state - public class State - { - public StatePosition Position { get; set; } - - // initialize a new state - private State(StatePosition position) - { - Position = position; - FieldIndex = -1; - } - - // initialize a new state - // used for creating the very first state - // and also when moving to a child element - public State(INavigableContent? content, State? parent, IList? siblings, int siblingIndex, StatePosition position) - : this(position) - { - Content = content; - Parent = parent; - Depth = parent?.Depth + 1 ?? 0; - Siblings = siblings; - SiblingIndex = siblingIndex; - } - - // initialize a clone state - private State(State other, bool recurse = false) - { - Position = other.Position; - - _content = other._content; - SiblingIndex = other.SiblingIndex; - Siblings = other.Siblings; - FieldsCount = other.FieldsCount; - FieldIndex = other.FieldIndex; - Depth = other.Depth; - - if (Position == StatePosition.PropertyXml) - XmlFragmentNavigator = other.XmlFragmentNavigator?.Clone(); - - // NielsK did - //Parent = other.Parent; - // but that creates corrupted stacks of states when cloning - // because clones share the parents : have to clone the whole - // stack of states. Avoid recursion. - - if (recurse) return; - - var clone = this; - while (other.Parent != null) - { - clone.Parent = new State(other.Parent, true); - clone = clone.Parent; - other = other.Parent; - } - } - - public State Clone() - { - return new State(this); - } - - // the parent state - public State? Parent { get; private set; } - - // the depth - public int Depth { get; } - - // the current content - private INavigableContent? _content; - - // the current content - public INavigableContent? Content - { - get - { - return _content; - } - set - { - FieldsCount = value?.Type.FieldTypes.Length ?? 0; - _content = value; - } - } - - private static readonly int[] NoChildIds = new int[0]; - - // the current content child ids - public IList GetContentChildIds(int maxDepth) - { - return Depth < maxDepth && _content?.ChildIds != null ? _content.ChildIds : NoChildIds; - } - - // the index of the current content within Siblings - public int SiblingIndex { get; set; } - - // the list of content identifiers for all children of the current content's parent - public IList? Siblings { get; } - - // the number of fields of the current content - // properties include attributes and properties - public int FieldsCount { get; private set; } - - // the index of the current field - // index -1 means special attribute "id" - public int FieldIndex { get; set; } - - // the current field type - // beware, no check on the index - public INavigableFieldType? CurrentFieldType => Content?.Type.FieldTypes[FieldIndex]; - - // gets or sets the xml fragment navigator - public XPathNavigator? XmlFragmentNavigator { get; set; } - - // gets a value indicating whether this state is at the same position as another one. - public bool IsSamePosition(State other) - { - if (other.XmlFragmentNavigator is null || XmlFragmentNavigator is null) - { - return false; - } - return other.Position == Position - && (Position != StatePosition.PropertyXml || other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator)) - && other.Content == Content - && other.FieldIndex == FieldIndex; - } - } - - #endregion } + + #region State management + + // the possible state positions + public enum StatePosition + { + Root, + Element, + Attribute, + PropertyElement, + PropertyText, + PropertyXml, + } + + // gets the state + // for unit tests only + public State InternalState { get; private set; } + + // represents the XPathNavigator state + public class State + { + private static readonly int[] NoChildIds = new int[0]; + + // the current content + private INavigableContent? _content; + + // initialize a new state + private State(StatePosition position) + { + Position = position; + FieldIndex = -1; + } + + // initialize a new state + // used for creating the very first state + // and also when moving to a child element + public State(INavigableContent? content, State? parent, IList? siblings, int siblingIndex, StatePosition position) + : this(position) + { + Content = content; + Parent = parent; + Depth = parent?.Depth + 1 ?? 0; + Siblings = siblings; + SiblingIndex = siblingIndex; + } + + // initialize a clone state + private State(State other, bool recurse = false) + { + Position = other.Position; + + _content = other._content; + SiblingIndex = other.SiblingIndex; + Siblings = other.Siblings; + FieldsCount = other.FieldsCount; + FieldIndex = other.FieldIndex; + Depth = other.Depth; + + if (Position == StatePosition.PropertyXml) + { + XmlFragmentNavigator = other.XmlFragmentNavigator?.Clone(); + } + + // NielsK did + // Parent = other.Parent; + // but that creates corrupted stacks of states when cloning + // because clones share the parents : have to clone the whole + // stack of states. Avoid recursion. + if (recurse) + { + return; + } + + State clone = this; + while (other.Parent != null) + { + clone.Parent = new State(other.Parent, true); + clone = clone.Parent; + other = other.Parent; + } + } + + public StatePosition Position { get; set; } + + // the parent state + public State? Parent { get; private set; } + + // the depth + public int Depth { get; } + + // the current content + public INavigableContent? Content + { + get => _content; + set + { + FieldsCount = value?.Type.FieldTypes.Length ?? 0; + _content = value; + } + } + + // the index of the current content within Siblings + public int SiblingIndex { get; set; } + + // the list of content identifiers for all children of the current content's parent + public IList? Siblings { get; } + + // the number of fields of the current content + // properties include attributes and properties + public int FieldsCount { get; private set; } + + // the index of the current field + // index -1 means special attribute "id" + public int FieldIndex { get; set; } + + // the current field type + // beware, no check on the index + public INavigableFieldType? CurrentFieldType => Content?.Type.FieldTypes[FieldIndex]; + + // gets or sets the xml fragment navigator + public XPathNavigator? XmlFragmentNavigator { get; set; } + + public State Clone() => new State(this); + + // the current content child ids + public IList GetContentChildIds(int maxDepth) => + Depth < maxDepth && _content?.ChildIds != null ? _content.ChildIds : NoChildIds; + + // gets a value indicating whether this state is at the same position as another one. + public bool IsSamePosition(State other) + { + if (other.XmlFragmentNavigator is null || XmlFragmentNavigator is null) + { + return false; + } + + return other.Position == Position + && (Position != StatePosition.PropertyXml || + other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator)) + && other.Content == Content + && other.FieldIndex == FieldIndex; + } + } + + #endregion } diff --git a/src/Umbraco.Core/Xml/XPath/RenamedRootNavigator.cs b/src/Umbraco.Core/Xml/XPath/RenamedRootNavigator.cs index 364560ebee..1b710c8db5 100644 --- a/src/Umbraco.Core/Xml/XPath/RenamedRootNavigator.cs +++ b/src/Umbraco.Core/Xml/XPath/RenamedRootNavigator.cs @@ -1,119 +1,88 @@ -using System.Xml; +using System.Xml; using System.Xml.XPath; -namespace Umbraco.Cms.Core.Xml.XPath +namespace Umbraco.Cms.Core.Xml.XPath; + +public class RenamedRootNavigator : XPathNavigator { - public class RenamedRootNavigator : XPathNavigator + private readonly XPathNavigator _navigator; + private readonly string _rootName; + + public RenamedRootNavigator(XPathNavigator navigator, string rootName) { - private readonly XPathNavigator _navigator; - private readonly string _rootName; - - public RenamedRootNavigator(XPathNavigator navigator, string rootName) - { - _navigator = navigator; - _rootName = rootName; - } - - public override string BaseURI => _navigator.BaseURI; - - public override XPathNavigator Clone() - { - return new RenamedRootNavigator(_navigator.Clone(), _rootName); - } - - public override bool IsEmptyElement => _navigator.IsEmptyElement; - - public override bool IsSamePosition(XPathNavigator other) - { - return _navigator.IsSamePosition(other); - } - - public override string LocalName - { - get - { - // local name without prefix - - var nav = _navigator.Clone(); - if (nav.MoveToParent() && nav.MoveToParent()) - return _navigator.LocalName; - return _rootName; - } - } - - public override bool MoveTo(XPathNavigator other) - { - return _navigator.MoveTo(other); - } - - public override bool MoveToFirstAttribute() - { - return _navigator.MoveToFirstAttribute(); - } - - public override bool MoveToFirstChild() - { - return _navigator.MoveToFirstChild(); - } - - public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) - { - return _navigator.MoveToFirstNamespace(namespaceScope); - } - - public override bool MoveToId(string id) - { - return _navigator.MoveToId(id); - } - - public override bool MoveToNext() - { - return _navigator.MoveToNext(); - } - - public override bool MoveToNextAttribute() - { - return _navigator.MoveToNextAttribute(); - } - - public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) - { - return _navigator.MoveToNextNamespace(namespaceScope); - } - - public override bool MoveToParent() - { - return _navigator.MoveToParent(); - } - - public override bool MoveToPrevious() - { - return _navigator.MoveToPrevious(); - } - - public override string Name - { - get - { - // qualified name with prefix - - var nav = _navigator.Clone(); - if (nav.MoveToParent() && nav.MoveToParent()) - return _navigator.Name; - var name = _navigator.Name; - var pos = name.IndexOf(':'); - return pos < 0 ? _rootName : (name.Substring(0, pos + 1) + _rootName); - } - } - - public override XmlNameTable NameTable => _navigator.NameTable; - - public override string NamespaceURI => _navigator.NamespaceURI; - - public override XPathNodeType NodeType => _navigator.NodeType; - - public override string Prefix => _navigator.Prefix; - - public override string Value => _navigator.Value; + _navigator = navigator; + _rootName = rootName; } + + public override string BaseURI => _navigator.BaseURI; + + public override bool IsEmptyElement => _navigator.IsEmptyElement; + + public override string LocalName + { + get + { + // local name without prefix + XPathNavigator nav = _navigator.Clone(); + if (nav.MoveToParent() && nav.MoveToParent()) + { + return _navigator.LocalName; + } + + return _rootName; + } + } + + public override string Name + { + get + { + // qualified name with prefix + XPathNavigator nav = _navigator.Clone(); + if (nav.MoveToParent() && nav.MoveToParent()) + { + return _navigator.Name; + } + + var name = _navigator.Name; + var pos = name.IndexOf(':'); + return pos < 0 ? _rootName : name[..(pos + 1)] + _rootName; + } + } + + public override XmlNameTable NameTable => _navigator.NameTable; + + public override string NamespaceURI => _navigator.NamespaceURI; + + public override XPathNodeType NodeType => _navigator.NodeType; + + public override string Prefix => _navigator.Prefix; + + public override string Value => _navigator.Value; + + public override XPathNavigator Clone() => new RenamedRootNavigator(_navigator.Clone(), _rootName); + + public override bool IsSamePosition(XPathNavigator other) => _navigator.IsSamePosition(other); + + public override bool MoveTo(XPathNavigator other) => _navigator.MoveTo(other); + + public override bool MoveToFirstAttribute() => _navigator.MoveToFirstAttribute(); + + public override bool MoveToFirstChild() => _navigator.MoveToFirstChild(); + + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) => + _navigator.MoveToFirstNamespace(namespaceScope); + + public override bool MoveToId(string id) => _navigator.MoveToId(id); + + public override bool MoveToNext() => _navigator.MoveToNext(); + + public override bool MoveToNextAttribute() => _navigator.MoveToNextAttribute(); + + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) => + _navigator.MoveToNextNamespace(namespaceScope); + + public override bool MoveToParent() => _navigator.MoveToParent(); + + public override bool MoveToPrevious() => _navigator.MoveToPrevious(); } diff --git a/src/Umbraco.Core/Xml/XPathNavigatorExtensions.cs b/src/Umbraco.Core/Xml/XPathNavigatorExtensions.cs index 8006d26da6..44cda2c691 100644 --- a/src/Umbraco.Core/Xml/XPathNavigatorExtensions.cs +++ b/src/Umbraco.Core/Xml/XPathNavigatorExtensions.cs @@ -1,61 +1,70 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.Xml.XPath; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extensions to XPathNavigator. +/// +public static class XPathNavigatorExtensions { /// - /// Provides extensions to XPathNavigator. + /// Selects a node set, using the specified XPath expression. /// - public static class XPathNavigatorExtensions + /// A source XPathNavigator. + /// An XPath expression. + /// A set of XPathVariables. + /// An iterator over the nodes matching the specified expression. + public static XPathNodeIterator Select(this XPathNavigator navigator, string expression, params XPathVariable[] variables) { - /// - /// Selects a node set, using the specified XPath expression. - /// - /// A source XPathNavigator. - /// An XPath expression. - /// A set of XPathVariables. - /// An iterator over the nodes matching the specified expression. - public static XPathNodeIterator Select(this XPathNavigator navigator, string expression, params XPathVariable[] variables) + if (variables == null || variables.Length == 0 || variables[0] == null) { - if (variables == null || variables.Length == 0 || variables[0] == null) - return navigator.Select(expression); - - // Reflector shows that the standard XPathNavigator.Compile method just does - // return XPathExpression.Compile(xpath); - // only difference is, XPathNavigator.Compile is virtual so it could be overridden - // by a class inheriting from XPathNavigator... there does not seem to be any - // doing it in the Framework, though... so we'll assume it's much cleaner to use - // the static compile: - var compiled = XPathExpression.Compile(expression); - - var context = new DynamicContext(); - foreach (var variable in variables) - context.AddVariable(variable.Name, variable.Value); - compiled.SetContext(context); - return navigator.Select(compiled); + return navigator.Select(expression); } - /// - /// Selects a node set, using the specified XPath expression. - /// - /// A source XPathNavigator. - /// An XPath expression. - /// A set of XPathVariables. - /// An iterator over the nodes matching the specified expression. - public static XPathNodeIterator Select(this XPathNavigator navigator, XPathExpression expression, params XPathVariable[] variables) - { - if (variables == null || variables.Length == 0 || variables[0] == null) - return navigator.Select(expression); + // Reflector shows that the standard XPathNavigator.Compile method just does + // return XPathExpression.Compile(xpath); + // only difference is, XPathNavigator.Compile is virtual so it could be overridden + // by a class inheriting from XPathNavigator... there does not seem to be any + // doing it in the Framework, though... so we'll assume it's much cleaner to use + // the static compile: + var compiled = XPathExpression.Compile(expression); - var compiled = expression.Clone(); // clone for thread-safety - var context = new DynamicContext(); - foreach (var variable in variables) - context.AddVariable(variable.Name, variable.Value); - compiled.SetContext(context); - return navigator.Select(compiled); + var context = new DynamicContext(); + foreach (XPathVariable variable in variables) + { + context.AddVariable(variable.Name, variable.Value); } + + compiled.SetContext(context); + return navigator.Select(compiled); + } + + /// + /// Selects a node set, using the specified XPath expression. + /// + /// A source XPathNavigator. + /// An XPath expression. + /// A set of XPathVariables. + /// An iterator over the nodes matching the specified expression. + public static XPathNodeIterator Select(this XPathNavigator navigator, XPathExpression expression, params XPathVariable[] variables) + { + if (variables == null || variables.Length == 0 || variables[0] == null) + { + return navigator.Select(expression); + } + + XPathExpression compiled = expression.Clone(); // clone for thread-safety + var context = new DynamicContext(); + foreach (XPathVariable variable in variables) + { + context.AddVariable(variable.Name, variable.Value); + } + + compiled.SetContext(context); + return navigator.Select(compiled); } } diff --git a/src/Umbraco.Core/Xml/XPathVariable.cs b/src/Umbraco.Core/Xml/XPathVariable.cs index 9bfed8e98d..4c2d2d0f4e 100644 --- a/src/Umbraco.Core/Xml/XPathVariable.cs +++ b/src/Umbraco.Core/Xml/XPathVariable.cs @@ -1,32 +1,31 @@ -// source: mvpxml.codeplex.com +// source: mvpxml.codeplex.com -namespace Umbraco.Cms.Core.Xml +namespace Umbraco.Cms.Core.Xml; + +/// +/// Represents a variable in an XPath query. +/// +/// The name must be foo in the constructor and $foo in the XPath query. +public class XPathVariable { /// - /// Represents a variable in an XPath query. + /// Initializes a new instance of the class with a name and a value. /// - /// The name must be foo in the constructor and $foo in the XPath query. - public class XPathVariable + /// + /// + public XPathVariable(string name, string value) { - /// - /// Gets or sets the name of the variable. - /// - public string Name { get; private set; } - - /// - /// Gets or sets the value of the variable. - /// - public string Value { get; private set; } - - /// - /// Initializes a new instance of the class with a name and a value. - /// - /// - /// - public XPathVariable(string name, string value) - { - Name = name; - Value = value; - } + Name = name; + Value = value; } + + /// + /// Gets or sets the name of the variable. + /// + public string Name { get; } + + /// + /// Gets or sets the value of the variable. + /// + public string Value { get; } } diff --git a/src/Umbraco.Core/Xml/XmlHelper.cs b/src/Umbraco.Core/Xml/XmlHelper.cs index 4de056e223..ad97120c93 100644 --- a/src/Umbraco.Core/Xml/XmlHelper.cs +++ b/src/Umbraco.Core/Xml/XmlHelper.cs @@ -1,392 +1,527 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using System.Xml; using System.Xml.XPath; using Umbraco.Cms.Core.Hosting; -namespace Umbraco.Cms.Core.Xml +namespace Umbraco.Cms.Core.Xml; + +/// +/// The XmlHelper class contains general helper methods for working with xml in umbraco. +/// +public class XmlHelper { /// - /// The XmlHelper class contains general helper methods for working with xml in umbraco. + /// Creates or sets an attribute on the XmlNode if an Attributes collection is available /// - public class XmlHelper + /// + /// + /// + /// + public static void SetAttribute(XmlDocument xml, XmlNode n, string name, string value) { - /// - /// Creates or sets an attribute on the XmlNode if an Attributes collection is available - /// - /// - /// - /// - /// - public static void SetAttribute(XmlDocument xml, XmlNode n, string name, string value) + if (xml == null) { - if (xml == null) throw new ArgumentNullException(nameof(xml)); - if (n == null) throw new ArgumentNullException(nameof(n)); - 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 (n.Attributes == null) - { - return; - } - if (n.Attributes[name] == null) - { - var a = xml.CreateAttribute(name); - a.Value = value; - n.Attributes.Append(a); - } - else - { - n.Attributes[name]!.Value = value; - } + throw new ArgumentNullException(nameof(xml)); } - /// - /// Gets a value indicating whether a specified string contains only xml whitespace characters. - /// - /// The string. - /// true if the string contains only xml whitespace characters. - /// As per XML 1.1 specs, space, \t, \r and \n. - public static bool IsXmlWhitespace(string s) + if (n == null) { - // as per xml 1.1 specs - anything else is significant whitespace - s = s.Trim(Constants.CharArrays.XmlWhitespaceChars); - return s.Length == 0; + throw new ArgumentNullException(nameof(n)); } - /// - /// Creates a new XPathDocument from an xml string. - /// - /// The xml string. - /// An XPathDocument created from the xml string. - public static XPathDocument CreateXPathDocument(string xml) + if (name == null) { - return new XPathDocument(new XmlTextReader(new StringReader(xml))); + throw new ArgumentNullException(nameof(name)); } - /// - /// Tries to create a new XPathDocument from an xml string. - /// - /// The xml string. - /// The XPath document. - /// A value indicating whether it has been possible to create the document. - public static bool TryCreateXPathDocument(string xml, out XPathDocument? doc) + if (string.IsNullOrWhiteSpace(name)) { - try - { - doc = CreateXPathDocument(xml); - return true; - } - catch (Exception) - { - doc = null; - return false; - } + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Tries to create a new XPathDocument from a property value. - /// - /// The value of the property. - /// The XPath document. - /// A value indicating whether it has been possible to create the document. - /// The value can be anything... Performance-wise, this is bad. - public static bool TryCreateXPathDocumentFromPropertyValue(object value, out XPathDocument? doc) + if (n.Attributes == null) { - // DynamicNode.ConvertPropertyValueByDataType first cleans the value by calling - // XmlHelper.StripDashesInElementOrAttributeName - this is because the XML is - // to be returned as a DynamicXml and element names such as "value-item" are - // invalid and must be converted to "valueitem". But we don't have that sort of - // problem here - and we don't need to bother with dashes nor dots, etc. + return; + } - doc = null; - var xml = value as string; - if (xml == null) return false; // no a string - if (CouldItBeXml(xml) == false) return false; // string does not look like it's xml - if (IsXmlWhitespace(xml)) return false; // string is whitespace, xml-wise - if (TryCreateXPathDocument(xml, out doc) == false) return false; // string can't be parsed into xml + if (n.Attributes[name] == null) + { + XmlAttribute a = xml.CreateAttribute(name); + a.Value = value; + n.Attributes.Append(a); + } + else + { + n.Attributes[name]!.Value = value; + } + } - var nav = doc!.CreateNavigator(); - if (nav.MoveToFirstChild()) - { - //SD: This used to do this but the razor macros and the entire razor macros section is gone, it was all legacy, it seems this method isn't even - // used apart from for tests so don't think this matters. In any case, we no longer check for this! + /// + /// Gets a value indicating whether a specified string contains only xml whitespace characters. + /// + /// The string. + /// true if the string contains only xml whitespace characters. + /// As per XML 1.1 specs, space, \t, \r and \n. + public static bool IsXmlWhitespace(string s) + { + // as per xml 1.1 specs - anything else is significant whitespace + s = s.Trim(Constants.CharArrays.XmlWhitespaceChars); + return s.Length == 0; + } - //var name = nav.LocalName; // must not match an excluded tag - //if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) return true; - - return true; - } + /// + /// Creates a new XPathDocument from an xml string. + /// + /// The xml string. + /// An XPathDocument created from the xml string. + public static XPathDocument CreateXPathDocument(string xml) => + new XPathDocument(new XmlTextReader(new StringReader(xml))); + /// + /// Tries to create a new XPathDocument from an xml string. + /// + /// The xml string. + /// The XPath document. + /// A value indicating whether it has been possible to create the document. + public static bool TryCreateXPathDocument(string xml, out XPathDocument? doc) + { + try + { + doc = CreateXPathDocument(xml); + return true; + } + catch (Exception) + { doc = null; return false; } + } - - /// - /// Sorts the children of a parentNode. - /// - /// The parent node. - /// An XPath expression to select children of to sort. - /// A function returning the value to order the nodes by. - public static void SortNodes( - XmlNode parentNode, - string childNodesXPath, - Func orderBy) + /// + /// Tries to create a new XPathDocument from a property value. + /// + /// The value of the property. + /// The XPath document. + /// A value indicating whether it has been possible to create the document. + /// The value can be anything... Performance-wise, this is bad. + public static bool TryCreateXPathDocumentFromPropertyValue(object value, out XPathDocument? doc) + { + // DynamicNode.ConvertPropertyValueByDataType first cleans the value by calling + // XmlHelper.StripDashesInElementOrAttributeName - this is because the XML is + // to be returned as a DynamicXml and element names such as "value-item" are + // invalid and must be converted to "valueitem". But we don't have that sort of + // problem here - and we don't need to bother with dashes nor dots, etc. + doc = null; + if (value is not string xml) { - var sortedChildNodes = parentNode.SelectNodes(childNodesXPath)?.Cast() - .OrderBy(orderBy) - .ToArray(); - - // append child nodes to last position, in sort-order - // so all child nodes will go after the property nodes - if (sortedChildNodes is not null) - { - foreach (var node in sortedChildNodes) - parentNode.AppendChild(node); // moves the node to the last position - } + return false; // no a string } - - /// - /// Sorts a single child node of a parentNode. - /// - /// The parent node. - /// An XPath expression to select children of to sort. - /// The child node to sort. - /// A function returning the value to order the nodes by. - /// A value indicating whether sorting was needed. - /// Assuming all nodes but are sorted, this will move the node to - /// the right position without moving all the nodes (as SortNodes would do) - should improve perfs. - public static bool SortNode( - XmlNode parentNode, - string childNodesXPath, - XmlNode node, - Func orderBy) + if (CouldItBeXml(xml) == false) { - var nodeSortOrder = orderBy(node); - var childNodesAndOrder = parentNode.SelectNodes(childNodesXPath)?.Cast() - .Select(x => Tuple.Create(x, orderBy(x))).ToArray(); + return false; // string does not look like it's xml + } - // only one node = node is in the right place already, obviously - if (childNodesAndOrder is null || childNodesAndOrder.Length == 1) return false; + if (IsXmlWhitespace(xml)) + { + return false; // string is whitespace, xml-wise + } - // find the first node with a sortOrder > node.sortOrder - var i = 0; - while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder) - i++; + if (TryCreateXPathDocument(xml, out doc) == false) + { + return false; // string can't be parsed into xml + } - // if one was found - if (i < childNodesAndOrder.Length) + XPathNavigator nav = doc!.CreateNavigator(); + if (nav.MoveToFirstChild()) + { + // SD: This used to do this but the razor macros and the entire razor macros section is gone, it was all legacy, it seems this method isn't even + // used apart from for tests so don't think this matters. In any case, we no longer check for this! + + // var name = nav.LocalName; // must not match an excluded tag + // if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) return true; + return true; + } + + doc = null; + return false; + } + + /// + /// Sorts the children of a parentNode. + /// + /// The parent node. + /// An XPath expression to select children of to sort. + /// A function returning the value to order the nodes by. + public static void SortNodes( + XmlNode parentNode, + string childNodesXPath, + Func orderBy) + { + XmlNode[]? sortedChildNodes = parentNode.SelectNodes(childNodesXPath)?.Cast() + .OrderBy(orderBy) + .ToArray(); + + // append child nodes to last position, in sort-order + // so all child nodes will go after the property nodes + if (sortedChildNodes is not null) + { + foreach (XmlNode node in sortedChildNodes) { - // and node is just before, we're done already - // else we need to move it right before the node that was found - if (i == 0 || childNodesAndOrder[i - 1].Item1 != node) - { - parentNode.InsertBefore(node, childNodesAndOrder[i].Item1); - return true; - } - } - else // i == childNodesAndOrder.Length && childNodesAndOrder.Length > 1 - { - // and node is the last one, we're done already - // else we need to append it as the last one - // (and i > 1, see above) - if (childNodesAndOrder[i - 1].Item1 != node) - { - parentNode.AppendChild(node); - return true; - } + parentNode.AppendChild(node); // moves the node to the last position } + } + } + + /// + /// Sorts a single child node of a parentNode. + /// + /// The parent node. + /// An XPath expression to select children of to sort. + /// The child node to sort. + /// A function returning the value to order the nodes by. + /// A value indicating whether sorting was needed. + /// + /// Assuming all nodes but are sorted, this will move the node to + /// the right position without moving all the nodes (as SortNodes would do) - should improve perfs. + /// + public static bool SortNode( + XmlNode parentNode, + string childNodesXPath, + XmlNode node, + Func orderBy) + { + var nodeSortOrder = orderBy(node); + Tuple[]? childNodesAndOrder = parentNode.SelectNodes(childNodesXPath)?.Cast() + .Select(x => Tuple.Create(x, orderBy(x))).ToArray(); + + // only one node = node is in the right place already, obviously + if (childNodesAndOrder is null || childNodesAndOrder.Length == 1) + { return false; } - - /// - /// Opens a file as a XmlDocument. - /// - /// The relative file path. ie. /config/umbraco.config - /// - /// Returns a XmlDocument class - public static XmlDocument OpenAsXmlDocument(string filePath, IHostingEnvironment hostingEnvironment) + // find the first node with a sortOrder > node.sortOrder + var i = 0; + while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder) { - using (var reader = new XmlTextReader(hostingEnvironment.MapPathContentRoot(filePath)) {WhitespaceHandling = WhitespaceHandling.All}) - { - var xmlDoc = new XmlDocument(); - //Load the file into the XmlDocument - xmlDoc.Load(reader); + i++; + } - return xmlDoc; + // if one was found + if (i < childNodesAndOrder.Length) + { + // and node is just before, we're done already + // else we need to move it right before the node that was found + if (i == 0 || childNodesAndOrder[i - 1].Item1 != node) + { + parentNode.InsertBefore(node, childNodesAndOrder[i].Item1); + return true; + } + } + else // i == childNodesAndOrder.Length && childNodesAndOrder.Length > 1 + { + // and node is the last one, we're done already + // else we need to append it as the last one + // (and i > 1, see above) + if (childNodesAndOrder[i - 1].Item1 != node) + { + parentNode.AppendChild(node); + return true; } } - /// - /// creates a XmlAttribute with the specified name and value - /// - /// The xmldocument. - /// The name of the attribute. - /// The value of the attribute. - /// a XmlAttribute - public static XmlAttribute AddAttribute(XmlDocument xd, string name, string value) - { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - 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)); + return false; + } - var temp = xd.CreateAttribute(name); - temp.Value = value; - return temp; + /// + /// Opens a file as a XmlDocument. + /// + /// The relative file path. ie. /config/umbraco.config + /// + /// Returns a XmlDocument class + public static XmlDocument OpenAsXmlDocument(string filePath, IHostingEnvironment hostingEnvironment) + { + using (var reader = + new XmlTextReader(hostingEnvironment.MapPathContentRoot(filePath)) + { + WhitespaceHandling = WhitespaceHandling.All, + }) + { + var xmlDoc = new XmlDocument(); + + // Load the file into the XmlDocument + xmlDoc.Load(reader); + + return xmlDoc; + } + } + + /// + /// creates a XmlAttribute with the specified name and value + /// + /// The xmldocument. + /// The name of the attribute. + /// The value of the attribute. + /// a XmlAttribute + public static XmlAttribute AddAttribute(XmlDocument xd, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); } - /// - /// Creates a text XmlNode with the specified name and value - /// - /// The xmldocument. - /// The node name. - /// The node value. - /// a XmlNode - public static XmlNode AddTextNode(XmlDocument xd, string name, string value) + if (name == null) { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - 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)); - - var temp = xd.CreateNode(XmlNodeType.Element, name, ""); - temp.AppendChild(xd.CreateTextNode(value)); - return temp; + throw new ArgumentNullException(nameof(name)); } - /// - /// Sets or Creates a text XmlNode with the specified name and value - /// - /// The xmldocument. - /// The node to set or create the child text node on - /// The node name. - /// The node value. - /// a XmlNode - public static XmlNode SetTextNode(XmlDocument xd, XmlNode parent, string name, string value) + if (string.IsNullOrWhiteSpace(name)) { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (parent == null) throw new ArgumentNullException(nameof(parent)); - 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)); - - var child = parent.SelectSingleNode(name); - if (child != null) - { - child.InnerText = value; - return child; - } - return AddTextNode(xd, name, value); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Sets or creates an Xml node from its inner Xml. - /// - /// The xmldocument. - /// The node to set or create the child text node on - /// The node name. - /// The node inner Xml. - /// a XmlNode - public static XmlNode SetInnerXmlNode(XmlDocument xd, XmlNode parent, string name, string value) - { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (parent == null) throw new ArgumentNullException(nameof(parent)); - 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)); + XmlAttribute temp = xd.CreateAttribute(name); + temp.Value = value; + return temp; + } - var child = parent.SelectSingleNode(name) ?? xd.CreateNode(XmlNodeType.Element, name, ""); - child.InnerXml = value; + /// + /// Creates a text XmlNode with the specified name and value + /// + /// The xmldocument. + /// The node name. + /// The node value. + /// a XmlNode + public static XmlNode AddTextNode(XmlDocument xd, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); + } + + 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)); + } + + XmlNode temp = xd.CreateNode(XmlNodeType.Element, name, string.Empty); + temp.AppendChild(xd.CreateTextNode(value)); + return temp; + } + + /// + /// Sets or Creates a text XmlNode with the specified name and value + /// + /// The xmldocument. + /// The node to set or create the child text node on + /// The node name. + /// The node value. + /// a XmlNode + public static XmlNode SetTextNode(XmlDocument xd, XmlNode parent, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); + } + + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } + + 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)); + } + + XmlNode? child = parent.SelectSingleNode(name); + if (child != null) + { + child.InnerText = value; return child; } - /// - /// Creates a cdata XmlNode with the specified name and value - /// - /// The xmldocument. - /// The node name. - /// The node value. - /// A XmlNode - public static XmlNode AddCDataNode(XmlDocument xd, string name, string value) - { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - 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)); + return AddTextNode(xd, name, value); + } - var temp = xd.CreateNode(XmlNodeType.Element, name, ""); - temp.AppendChild(xd.CreateCDataSection(value)); - return temp; + /// + /// Sets or creates an Xml node from its inner Xml. + /// + /// The xmldocument. + /// The node to set or create the child text node on + /// The node name. + /// The node inner Xml. + /// a XmlNode + public static XmlNode SetInnerXmlNode(XmlDocument xd, XmlNode parent, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); } - /// - /// Sets or Creates a cdata XmlNode with the specified name and value - /// - /// The xmldocument. - /// The node to set or create the child text node on - /// The node name. - /// The node value. - /// a XmlNode - public static XmlNode SetCDataNode(XmlDocument xd, XmlNode parent, string name, string value) + if (parent == null) { - if (xd == null) throw new ArgumentNullException(nameof(xd)); - if (parent == null) throw new ArgumentNullException(nameof(parent)); - 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)); - - var child = parent.SelectSingleNode(name); - if (child != null) - { - child.InnerXml = ""; - return child; - } - return AddCDataNode(xd, name, value); + throw new ArgumentNullException(nameof(parent)); } - /// - /// Gets the value of a XmlNode - /// - /// The XmlNode. - /// the value as a string - public static string GetNodeValue(XmlNode n) + if (name == null) { - var value = string.Empty; - if (n == null || n.FirstChild == null) - return value; - value = n.FirstChild.Value ?? n.InnerXml; - return value.Replace("", "", "]]>"); + throw new ArgumentNullException(nameof(name)); } - /// - /// Determines whether the specified string appears to be XML. - /// - /// The XML string. - /// - /// true if the specified string appears to be XML; otherwise, false. - /// - public static bool CouldItBeXml(string? xml) + if (string.IsNullOrWhiteSpace(name)) { - if (string.IsNullOrEmpty(xml)) return false; - - xml = xml.Trim(); - return xml.StartsWith("<") && xml.EndsWith(">") && xml.Contains('/'); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); } - /// - /// Return a dictionary of attributes found for a string based tag - /// - /// - /// - public static Dictionary GetAttributesFromElement(string tag) + XmlNode child = parent.SelectSingleNode(name) ?? xd.CreateNode(XmlNodeType.Element, name, string.Empty); + child.InnerXml = value; + return child; + } + + /// + /// Creates a cdata XmlNode with the specified name and value + /// + /// The xmldocument. + /// The node name. + /// The node value. + /// A XmlNode + public static XmlNode AddCDataNode(XmlDocument xd, string name, string value) + { + if (xd == null) { - var m = - Regex.Matches(tag, "(?\\S*)=\"(?[^\"]*)\"", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - // fix for issue 14862: return lowercase attributes for case insensitive matching - var d = m.Cast().ToDictionary(attributeSet => attributeSet.Groups["attributeName"].Value.ToString().ToLower(), attributeSet => attributeSet.Groups["attributeValue"].Value.ToString()); - return d; + throw new ArgumentNullException(nameof(xd)); } + + 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)); + } + + XmlNode temp = xd.CreateNode(XmlNodeType.Element, name, string.Empty); + temp.AppendChild(xd.CreateCDataSection(value)); + return temp; + } + + /// + /// Sets or Creates a cdata XmlNode with the specified name and value + /// + /// The xmldocument. + /// The node to set or create the child text node on + /// The node name. + /// The node value. + /// a XmlNode + public static XmlNode SetCDataNode(XmlDocument xd, XmlNode parent, string name, string value) + { + if (xd == null) + { + throw new ArgumentNullException(nameof(xd)); + } + + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } + + 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)); + } + + XmlNode? child = parent.SelectSingleNode(name); + if (child != null) + { + child.InnerXml = ""; + return child; + } + + return AddCDataNode(xd, name, value); + } + + /// + /// Gets the value of a XmlNode + /// + /// The XmlNode. + /// the value as a string + public static string GetNodeValue(XmlNode n) + { + var value = string.Empty; + if (n == null || n.FirstChild == null) + { + return value; + } + + value = n.FirstChild.Value ?? n.InnerXml; + return value.Replace("", "", "]]>"); + } + + /// + /// Determines whether the specified string appears to be XML. + /// + /// The XML string. + /// + /// true if the specified string appears to be XML; otherwise, false. + /// + public static bool CouldItBeXml(string? xml) + { + if (string.IsNullOrEmpty(xml)) + { + return false; + } + + xml = xml.Trim(); + return xml.StartsWith("<") && xml.EndsWith(">") && xml.Contains('/'); + } + + /// + /// Return a dictionary of attributes found for a string based tag + /// + /// + /// + public static Dictionary GetAttributesFromElement(string tag) + { + MatchCollection m = + Regex.Matches(tag, "(?\\S*)=\"(?[^\"]*)\"", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + // fix for issue 14862: return lowercase attributes for case insensitive matching + var d = m.ToDictionary( + attributeSet => attributeSet.Groups["attributeName"].Value.ToString().ToLower(), + attributeSet => attributeSet.Groups["attributeValue"].Value.ToString()); + return d; } } diff --git a/src/Umbraco.Core/Xml/XmlNamespaces.cs b/src/Umbraco.Core/Xml/XmlNamespaces.cs index 1721de253f..55a23736ff 100644 --- a/src/Umbraco.Core/Xml/XmlNamespaces.cs +++ b/src/Umbraco.Core/Xml/XmlNamespaces.cs @@ -1,41 +1,40 @@ -// source: mvpxml.codeplex.com +// source: mvpxml.codeplex.com -namespace Umbraco.Cms.Core.Xml +namespace Umbraco.Cms.Core.Xml; + +/// +/// Provides public constants for wellknown XML namespaces. +/// +/// Author: Daniel Cazzulino, blog +public static class XmlNamespaces { /// - /// Provides public constants for wellknown XML namespaces. + /// The public XML 1.0 namespace. /// - /// Author: Daniel Cazzulino, blog - public static class XmlNamespaces - { - /// - /// The public XML 1.0 namespace. - /// - /// See http://www.w3.org/TR/2004/REC-xml-20040204/ - public const string Xml = "http://www.w3.org/XML/1998/namespace"; + /// See http://www.w3.org/TR/2004/REC-xml-20040204/ + public const string Xml = "http://www.w3.org/XML/1998/namespace"; - /// - /// Public Xml Namespaces specification namespace. - /// - /// See http://www.w3.org/TR/REC-xml-names/ - public const string XmlNs = "http://www.w3.org/2000/xmlns/"; + /// + /// Public Xml Namespaces specification namespace. + /// + /// See http://www.w3.org/TR/REC-xml-names/ + public const string XmlNs = "http://www.w3.org/2000/xmlns/"; - /// - /// Public Xml Namespaces prefix. - /// - /// See http://www.w3.org/TR/REC-xml-names/ - public const string XmlNsPrefix = "xmlns"; + /// + /// Public Xml Namespaces prefix. + /// + /// See http://www.w3.org/TR/REC-xml-names/ + public const string XmlNsPrefix = "xmlns"; - /// - /// XML Schema instance namespace. - /// - /// See http://www.w3.org/TR/xmlschema-1/ - public const string Xsi = "http://www.w3.org/2001/XMLSchema-instance"; + /// + /// XML Schema instance namespace. + /// + /// See http://www.w3.org/TR/xmlschema-1/ + public const string Xsi = "http://www.w3.org/2001/XMLSchema-instance"; - /// - /// XML 1.0 Schema namespace. - /// - /// See http://www.w3.org/TR/xmlschema-1/ - public const string Xsd = "http://www.w3.org/2001/XMLSchema"; - } + /// + /// XML 1.0 Schema namespace. + /// + /// See http://www.w3.org/TR/xmlschema-1/ + public const string Xsd = "http://www.w3.org/2001/XMLSchema"; } diff --git a/src/Umbraco.Core/Xml/XmlNodeListFactory.cs b/src/Umbraco.Core/Xml/XmlNodeListFactory.cs index 29797fc59a..17c2f41843 100644 --- a/src/Umbraco.Core/Xml/XmlNodeListFactory.cs +++ b/src/Umbraco.Core/Xml/XmlNodeListFactory.cs @@ -1,178 +1,166 @@ -using System; -using System.Collections.Generic; +using System.Collections; using System.Xml; using System.Xml.XPath; // source: mvpxml.codeplex.com +namespace Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core.Xml +public class XmlNodeListFactory { - public class XmlNodeListFactory + private XmlNodeListFactory() { - private XmlNodeListFactory() { } + } - #region Public members + #region Public members + + /// + /// Creates an instance of a that allows + /// enumerating elements in the iterator. + /// + /// + /// The result of a previous node selection + /// through an query. + /// + /// An initialized list ready to be enumerated. + /// + /// The underlying XML store used to issue the query must be + /// an object inheriting , such as + /// . + /// + public static XmlNodeList CreateNodeList(XPathNodeIterator? iterator) => new XmlNodeListIterator(iterator); + + #endregion Public members + + #region XmlNodeListIterator + + private class XmlNodeListIterator : XmlNodeList + { + private readonly XPathNodeIterator? _iterator; + private readonly IList _nodes = new List(); + + public XmlNodeListIterator(XPathNodeIterator? iterator) => _iterator = iterator?.Clone(); + + public override int Count + { + get + { + if (!Done) + { + ReadToEnd(); + } + + return _nodes.Count; + } + } /// - /// Creates an instance of a that allows - /// enumerating elements in the iterator. + /// Flags that the iterator has been consumed. /// - /// The result of a previous node selection - /// through an query. - /// An initialized list ready to be enumerated. - /// The underlying XML store used to issue the query must be - /// an object inheriting , such as - /// . - public static XmlNodeList CreateNodeList(XPathNodeIterator? iterator) + private bool Done { get; set; } + + /// + /// Current count of nodes in the iterator (read so far). + /// + private int CurrentPosition => _nodes.Count; + + public override IEnumerator GetEnumerator() => new XmlNodeListEnumerator(this); + + public override XmlNode? Item(int index) { - return new XmlNodeListIterator(iterator); + if (index >= _nodes.Count) + { + ReadTo(index); + } + + // Compatible behavior with .NET + if (index >= _nodes.Count || index < 0) + { + return null; + } + + return _nodes[index]; } - #endregion Public members - - #region XmlNodeListIterator - - private class XmlNodeListIterator : XmlNodeList + /// + /// Reads the entire iterator. + /// + private void ReadToEnd() { - readonly XPathNodeIterator? _iterator; - readonly IList _nodes = new List(); - - public XmlNodeListIterator(XPathNodeIterator? iterator) + while (_iterator is not null && _iterator.MoveNext()) { - _iterator = iterator?.Clone(); - } - - public override System.Collections.IEnumerator GetEnumerator() - { - return new XmlNodeListEnumerator(this); - } - - public override XmlNode? Item(int index) - { - - if (index >= _nodes.Count) - ReadTo(index); - // Compatible behavior with .NET - if (index >= _nodes.Count || index < 0) - return null; - return _nodes[index]; - } - - public override int Count - { - get + // Check IHasXmlNode interface. + if (_iterator.Current is not IHasXmlNode node) { - if (!_done) ReadToEnd(); - return _nodes.Count; + throw new ArgumentException("IHasXmlNode is missing."); } + + _nodes.Add(node.GetNode()); } + Done = true; + } - /// - /// Reads the entire iterator. - /// - private void ReadToEnd() + /// + /// Reads up to the specified index, or until the + /// iterator is consumed. + /// + private void ReadTo(int to) + { + while (_nodes.Count <= to) { - while (_iterator is not null && _iterator.MoveNext()) + if (_iterator is not null && _iterator.MoveNext()) { - var node = _iterator.Current as IHasXmlNode; // Check IHasXmlNode interface. - if (node == null) + if (_iterator.Current is not IHasXmlNode node) + { throw new ArgumentException("IHasXmlNode is missing."); + } + _nodes.Add(node.GetNode()); } - _done = true; - } - - /// - /// Reads up to the specified index, or until the - /// iterator is consumed. - /// - private void ReadTo(int to) - { - while (_nodes.Count <= to) + else { - if (_iterator is not null && _iterator.MoveNext()) - { - var node = _iterator.Current as IHasXmlNode; - // Check IHasXmlNode interface. - if (node == null) - throw new ArgumentException("IHasXmlNode is missing."); - _nodes.Add(node.GetNode()); - } - else - { - _done = true; - return; - } + Done = true; + return; } } - - /// - /// Flags that the iterator has been consumed. - /// - private bool Done - { - get { return _done; } - } - - bool _done; - - /// - /// Current count of nodes in the iterator (read so far). - /// - private int CurrentPosition - { - get { return _nodes.Count; } - } - - #region XmlNodeListEnumerator - - private class XmlNodeListEnumerator : System.Collections.IEnumerator - { - readonly XmlNodeListIterator _iterator; - int _position = -1; - - public XmlNodeListEnumerator(XmlNodeListIterator iterator) - { - _iterator = iterator; - } - - #region IEnumerator Members - - void System.Collections.IEnumerator.Reset() - { - _position = -1; - } - - - bool System.Collections.IEnumerator.MoveNext() - { - _position++; - _iterator.ReadTo(_position); - - // If we reached the end and our index is still - // bigger, there are no more items. - if (_iterator.Done && _position >= _iterator.CurrentPosition) - return false; - - return true; - } - - object? System.Collections.IEnumerator.Current - { - get - { - return _iterator[_position]; - } - } - - #endregion - } - - #endregion XmlNodeListEnumerator } - #endregion XmlNodeListIterator + #region XmlNodeListEnumerator + + private class XmlNodeListEnumerator : IEnumerator + { + private readonly XmlNodeListIterator _iterator; + private int _position = -1; + + public XmlNodeListEnumerator(XmlNodeListIterator iterator) => _iterator = iterator; + + object? IEnumerator.Current => _iterator[_position]; + + #region IEnumerator Members + + void IEnumerator.Reset() => _position = -1; + + bool IEnumerator.MoveNext() + { + _position++; + _iterator.ReadTo(_position); + + // If we reached the end and our index is still + // bigger, there are no more items. + if (_iterator.Done && _position >= _iterator.CurrentPosition) + { + return false; + } + + return true; + } + + #endregion + } + + #endregion XmlNodeListEnumerator } + + #endregion XmlNodeListIterator }